Compare commits

..

171 Commits

Author SHA1 Message Date
Owen
3af1e0ef56 Delete all before migrating 2025-10-17 11:56:19 -07:00
Owen
08b7d6735c Priority needs to be def 2025-10-16 14:52:14 -07:00
Milo Schwartz
a91ebd1e91 Update README.md 2025-10-16 17:45:11 -04:00
Owen
312e03b4eb Fix typo 2025-10-16 14:43:11 -07:00
miloschwartz
e8a57e432c hide path match and rewrite in raw resource 2025-10-16 14:30:22 -07:00
Owen
bca2eef2e8 Show ssl toggle 2025-10-16 14:24:36 -07:00
Owen
ec7211a15d Handle updating exit node and fix raw resource issues 2025-10-16 13:55:08 -07:00
Owen
46807c6477 Fix various bugs 2025-10-16 10:23:25 -07:00
miloschwartz
b578786e62 add empty state to sites table cols 2025-10-16 10:11:50 -07:00
miloschwartz
2e0ad8d262 branding only works when licensed 2025-10-15 22:07:33 -07:00
miloschwartz
003f0cfa6d fix target validation on create site 2025-10-15 20:43:59 -07:00
Owen
ee3df081ef Fix docker button and positioning 2025-10-15 20:21:15 -07:00
Owen
08eeb12519 Fix going away when creating target
cd8062ada3
2025-10-15 17:48:31 -07:00
Owen
e66c6b2505 remove volumes for remote nodes 2025-10-15 17:44:03 -07:00
miloschwartz
d2a880d9c8 update docker command in makefile 2025-10-15 17:36:09 -07:00
miloschwartz
edc0b86470 add translation and update url 2025-10-15 17:32:39 -07:00
Owen
aebe6b80b7 Make private file optional 2025-10-15 17:22:43 -07:00
Owen
4d87333b43 Merge branch 'main' into dev 2025-10-15 17:15:48 -07:00
Owen
ef32f3ed5a Load encryption file dynamically 2025-10-15 17:14:24 -07:00
Owen
216ded3034 Merge branch 'main' of github.com:fosrl/pangolin 2025-10-15 17:14:14 -07:00
miloschwartz
cb59fe2cee update readme 2025-10-15 16:34:06 -07:00
miloschwartz
7776f6d09c disable branding 2025-10-15 16:32:16 -07:00
Owen
c50392c947 Remove logging 2025-10-15 13:57:42 -07:00
Owen
ceee978fcd Merge branch 'dev' 2025-10-15 12:13:15 -07:00
Owen
c5a73dc87e Try to handle the certs better 2025-10-15 12:12:59 -07:00
Owen
7198ef2774 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-10-15 11:12:38 -07:00
miloschwartz
7e9a066797 update form 2025-10-15 11:10:37 -07:00
Milo Schwartz
ba96332313 Update README.md 2025-10-15 14:02:28 -04:00
Owen
e2d0338b0b Merge branch 'dev' 2025-10-15 10:39:50 -07:00
Owen
59ecab5738 Dont ping remote nodes; handle certs better 2025-10-15 10:39:45 -07:00
miloschwartz
721bf3403d fix form 2025-10-15 10:21:00 -07:00
Owen
3b8ba47377 Update package lock 2025-10-14 18:00:46 -07:00
Milo Schwartz
e752929f69 Update README.md 2025-10-14 20:50:41 -04:00
Milo Schwartz
e41c3e6f54 Update README.md 2025-10-14 20:48:44 -04:00
Milo Schwartz
9dedd1a8de Update README.md 2025-10-14 20:41:14 -04:00
Owen
c4a5fae28f Update workflow and add runner 2025-10-14 17:34:47 -07:00
Owen
5f95a3233f Merge branch 'dev' 2025-10-14 17:05:40 -07:00
Owen Schwartz
d3174d0196 Merge pull request #1671 from fosrl/crowdin_dev
New Crowdin updates
2025-10-14 17:03:22 -07:00
Owen Schwartz
3710d71974 New translations en-us.json (Spanish) 2025-10-14 17:02:54 -07:00
Owen Schwartz
f62e88eb67 New translations en-us.json (Norwegian Bokmal) 2025-10-14 17:02:53 -07:00
Owen Schwartz
904b302fb6 New translations en-us.json (Chinese Simplified) 2025-10-14 17:02:52 -07:00
Owen Schwartz
5fc096f2d5 New translations en-us.json (Turkish) 2025-10-14 17:02:50 -07:00
Owen Schwartz
87668c492f New translations en-us.json (Russian) 2025-10-14 17:02:49 -07:00
Owen Schwartz
6d7a8b97ad New translations en-us.json (Portuguese) 2025-10-14 17:02:48 -07:00
Owen Schwartz
282d444933 New translations en-us.json (Polish) 2025-10-14 17:02:46 -07:00
Owen Schwartz
f3d7d97fb9 New translations en-us.json (Dutch) 2025-10-14 17:02:45 -07:00
Owen Schwartz
de857a7c4e New translations en-us.json (Korean) 2025-10-14 17:02:44 -07:00
Owen Schwartz
20a0ebfc9d New translations en-us.json (Italian) 2025-10-14 17:02:43 -07:00
Owen Schwartz
ba8166bdeb New translations en-us.json (German) 2025-10-14 17:02:41 -07:00
Owen Schwartz
2b634fc6c5 New translations en-us.json (Czech) 2025-10-14 17:02:40 -07:00
Owen Schwartz
5429bc03ab New translations en-us.json (Bulgarian) 2025-10-14 17:02:38 -07:00
Owen Schwartz
a558b34608 New translations en-us.json (French) 2025-10-14 17:02:37 -07:00
Owen Schwartz
1850d56977 Merge pull request #1669 from fosrl/dev
Dev
2025-10-14 16:57:01 -07:00
Owen
61b4c62824 Merge branch 'main' into dev 2025-10-14 16:55:12 -07:00
Owen
10e5ccfe86 Handle tsconfig 2025-10-14 16:34:11 -07:00
Owen
9f5d475e80 Migrations work 2025-10-14 16:34:11 -07:00
Milo Schwartz
9bb9a3acbe Update README.md 2025-10-14 19:04:09 -04:00
Milo Schwartz
0923b7e3c5 Update README.md 2025-10-14 18:59:31 -04:00
Owen
ccd81f6fe2 Adjust migration 2025-10-14 15:31:56 -07:00
miloschwartz
0f74107e86 add links to license 2025-10-14 14:39:05 -07:00
Owen
8377434c08 Add update database to installer 2025-10-14 14:23:18 -07:00
Owen
1fbf2bfb8d Remove managed add maxmind 2025-10-14 14:15:33 -07:00
Owen
42facf8e12 Add pg migration 2025-10-14 12:11:17 -07:00
Owen
4bb3d85c25 Add sqlite migration 2025-10-14 12:04:02 -07:00
Owen
c0039190bd Fix frontend type imports 2025-10-14 11:28:56 -07:00
Owen
a8d00a47cd Remote nodes working 2025-10-14 10:58:51 -07:00
Owen
57bcbf6c48 Include traefik config when sending to remote nodes 2025-10-14 10:38:41 -07:00
Owen
c57db1479e Update language for local sites 2025-10-14 10:25:03 -07:00
Owen
cd8062ada3 Fix various bugs 2025-10-14 10:25:03 -07:00
Owen
244d05adb1 Import the right customer 2025-10-14 10:25:03 -07:00
miloschwartz
812bd64325 improve docker container selector button placement 2025-10-13 18:33:55 -07:00
miloschwartz
276d1361ac move billing and and licenses up in sidebar 2025-10-13 18:07:00 -07:00
miloschwartz
881eac4722 fix tier and remove test interval 2025-10-13 17:01:32 -07:00
Owen
2a2a550a6a Merge branch 'distribution' of github.com:fosrl/pangolin-saas into distribution 2025-10-13 17:00:37 -07:00
miloschwartz
e75001080a update license terminateAt and update word mark 2025-10-13 16:45:19 -07:00
miloschwartz
6fbba38a76 fix license type and default selected domain type 2025-10-13 16:45:19 -07:00
Owen
902b413881 Path rewriting working? 2025-10-13 16:41:14 -07:00
Owen
8b2f8ad3ef Add rewriting to traefik config 2025-10-13 15:53:17 -07:00
Owen
377cb77307 Returning unauthorized 2025-10-13 15:34:26 -07:00
miloschwartz
733bf0b169 set wildcard domain verified to true 2025-10-13 15:31:34 -07:00
miloschwartz
8faff3e075 hide provided domains if not using dns 2025-10-13 15:21:59 -07:00
Owen
48af91c976 Return unauthorized if header auth is the only one 2025-10-13 15:20:53 -07:00
Owen
6664efaa13 Fix up UI around resource auth headers 2025-10-13 15:20:53 -07:00
miloschwartz
e5ee96cf52 fix create domain 2025-10-13 15:08:57 -07:00
Owen
38faf1f905 Add header auth so it does not allow passing 2025-10-13 14:59:54 -07:00
Owen
2cff142266 Use Pangolin DNS fix 2025-10-13 14:42:40 -07:00
miloschwartz
2c99cfacc0 fix header auth formatting 2025-10-13 14:39:41 -07:00
miloschwartz
0c63ea1f50 remove log 2025-10-13 14:28:23 -07:00
Owen
f50df66e3a Fix use_pangolin_dns 2025-10-13 14:27:51 -07:00
Owen
4b93491160 rename generateOwnCertificates and check in resource header 2025-10-13 14:26:36 -07:00
Owen
19210cbf7d Hide cname and ns if not using dns 2025-10-13 14:22:06 -07:00
miloschwartz
9af206b69a move schemas to folder 2025-10-13 14:13:26 -07:00
Owen
b6b9c71c5e Pass this middleware correctly in saas 2025-10-13 12:27:45 -07:00
Owen
c000c4502f Fix instance name 2025-10-13 12:13:04 -07:00
Owen
b6c1d9a592 Merge branch 'dev' into distribution 2025-10-13 12:04:41 -07:00
Owen Schwartz
7a75fe0cad Merge pull request #1658 from fosrl/dependabot/npm_and_yarn/dev-patch-updates-6f2a42a27f
Bump @types/node from 24.7.0 to 24.7.2 in the dev-patch-updates group
2025-10-13 12:03:21 -07:00
Owen Schwartz
a83e660902 Merge pull request #1659 from fosrl/dependabot/npm_and_yarn/prod-minor-updates-9b5291575b
Bump the prod-minor-updates group with 2 updates
2025-10-13 12:03:09 -07:00
Owen Schwartz
65eb3e4b95 Merge pull request #1612 from Pallavikumarimdb/fix/UI-adjustment
UI Adjustments
2025-10-13 12:02:55 -07:00
Pallavi Kumari
093fb419f3 add en-US 2025-10-14 00:28:00 +05:30
Pallavi Kumari
026e56aead fix lint 2025-10-14 00:28:00 +05:30
Pallavi Kumari
fa9bc59f62 match create resource ui with proxy ui 2025-10-14 00:28:00 +05:30
Pallavi Kumari
06ec80db42 replace dialog with credenza 2025-10-14 00:28:00 +05:30
miloschwartz
24d564b79b add advanced toggle to targets table 2025-10-14 00:28:00 +05:30
Owen
2f5e6248cd Small ui adjustments 2025-10-14 00:27:24 +05:30
Pallavi Kumari
c0cc81ed96 standardizing the targets input table 2025-10-14 00:27:24 +05:30
Pallavi Kumari
b33a54a449 remove unused 2025-10-14 00:27:24 +05:30
Pallavi Kumari
94137e587c change target config ui for create resource 2025-10-14 00:27:24 +05:30
Pallavi Kumari
a6086d3724 address input design 2025-10-14 00:27:24 +05:30
Pallavi Kumari
0a377150e3 reorder columns 2025-10-14 00:27:24 +05:30
Pallavi Kumari
d20e0a228a adjust target config ui inside create resource 2025-10-14 00:27:24 +05:30
Pallavi Kumari
ca146a1b57 adjust target config column 2025-10-14 00:27:24 +05:30
Pallavi Kumari
c7c3e3ee73 refresh button inside admin 2025-10-14 00:27:24 +05:30
Pallavi Kumari
cd27f6459c refresh button 2025-10-14 00:27:24 +05:30
Pallavi Kumari
b1e212721e refresh button for role, user, share-link, invitation table 2025-10-14 00:27:24 +05:30
Pallavi Kumari
ccd2773331 refresh button on resources page 2025-10-14 00:27:23 +05:30
Pallavi Kumari
cfa82b51fb refresh button in clients page 2025-10-14 00:27:23 +05:30
Owen
9c91a8db46 Update build process 2025-10-13 11:49:48 -07:00
miloschwartz
b160eee8d2 Merge branch 'dev' into distribution 2025-10-13 11:06:14 -07:00
miloschwartz
37ceabdf5d add enterprise license system 2025-10-13 10:41:10 -07:00
Owen
e7828a43fa Add flag for generate own certs 2025-10-13 10:32:41 -07:00
dependabot[bot]
ccb1f04ad8 Bump the prod-minor-updates group with 2 updates
Bumps the prod-minor-updates group with 2 updates: [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) and [react-hook-form](https://github.com/react-hook-form/react-hook-form).


Updates `@aws-sdk/client-s3` from 3.906.0 to 3.908.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.908.0/clients/client-s3)

Updates `react-hook-form` from 7.64.0 to 7.65.0
- [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.64.0...v7.65.0)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.908.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react-hook-form
  dependency-version: 7.65.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-13 01:38:51 +00:00
dependabot[bot]
4c14ccbb63 Bump @types/node from 24.7.0 to 24.7.2 in the dev-patch-updates group
Bumps the dev-patch-updates group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@types/node` from 24.7.0 to 24.7.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.7.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-13 01:33:47 +00:00
Owen Schwartz
25c24ca9cf Merge pull request #1639 from fosrl/dependabot/go_modules/install/prod-minor-updates-cf68330517
Bump golang.org/x/term from 0.35.0 to 0.36.0 in /install in the prod-minor-updates group
2025-10-12 17:08:31 -07:00
Owen Schwartz
787869fe21 Merge pull request #1641 from fosrl/dependabot/npm_and_yarn/prod-minor-updates-7acd695279
Bump the prod-minor-updates group with 2 updates
2025-10-12 17:08:20 -07:00
Owen Schwartz
b51c27a823 Merge pull request #1646 from fosrl/dependabot/npm_and_yarn/prod-patch-updates-942db9cd59
Bump the prod-patch-updates group across 1 directory with 3 updates
2025-10-12 17:08:11 -07:00
Owen
5917881b47 Remove dev image for now #1625 2025-10-12 17:06:41 -07:00
Owen
c7a40d59b7 Seperate managed node code to fosrl/pangolin-node 2025-10-12 16:34:36 -07:00
Owen
a50c0d84e9 Make easier to run in dev - fix a couple of things 2025-10-12 16:23:38 -07:00
Owen
f17a957058 Cleaning up more imports 2025-10-11 20:46:49 -07:00
Owen
2c63851130 Separate types & fix #private import 2025-10-11 19:02:15 -07:00
miloschwartz
6b125bba7c reject user if no policies match and remove root user in auto provision 2025-10-10 11:52:45 -07:00
Owen
d92b87b7c8 Chungus 2.0 2025-10-10 11:27:15 -07:00
miloschwartz
f64a477c3d fix spacing issue in strategy select 2025-10-09 20:21:16 -07:00
dependabot[bot]
b6f8ed1e4a Bump the prod-patch-updates group across 1 directory with 3 updates
Bumps the prod-patch-updates group with 3 updates in the / directory: [next-intl](https://github.com/amannn/next-intl), [npm](https://github.com/npm/cli) and [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node).


Updates `next-intl` from 4.3.11 to 4.3.12
- [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.3.11...v4.3.12)

Updates `npm` from 11.6.1 to 11.6.2
- [Release notes](https://github.com/npm/cli/releases)
- [Changelog](https://github.com/npm/cli/blob/latest/CHANGELOG.md)
- [Commits](https://github.com/npm/cli/compare/v11.6.1...v11.6.2)

Updates `posthog-node` from 5.9.3 to 5.9.5
- [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.9.5/packages/node)

---
updated-dependencies:
- dependency-name: next-intl
  dependency-version: 4.3.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: npm
  dependency-version: 11.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: posthog-node
  dependency-version: 5.9.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-10 01:21:19 +00:00
dependabot[bot]
bad88e4741 Bump the prod-minor-updates group with 2 updates
Bumps the prod-minor-updates group with 2 updates: [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) and [react-easy-sort](https://github.com/ValentinH/react-easy-sort).


Updates `@aws-sdk/client-s3` from 3.901.0 to 3.906.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.906.0/clients/client-s3)

Updates `react-easy-sort` from 1.7.0 to 1.8.0
- [Release notes](https://github.com/ValentinH/react-easy-sort/releases)
- [Commits](https://github.com/ValentinH/react-easy-sort/compare/v1.7.0...v1.8.0)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.906.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react-easy-sort
  dependency-version: 1.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-09 01:26:12 +00:00
dependabot[bot]
01db519691 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.35.0 to 0.36.0
- [Commits](https://github.com/golang/term/compare/v0.35.0...v0.36.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-09 01:22:20 +00:00
miloschwartz
e601038c0f fix role extraction in idp form 2025-10-08 17:49:30 -07:00
miloschwartz
e0996a17ef rename managed nodes 2025-10-08 17:35:08 -07:00
Owen
526307e192 Fix ssl undefined issue 2025-10-08 16:43:40 -07:00
miloschwartz
1b01c4f053 fix idp infinite redirect closes #1540 2025-10-08 14:00:26 -07:00
Owen Schwartz
a184e23f16 Merge pull request #1634 from fosrl/dependabot/npm_and_yarn/prod-minor-updates-f2d0e72ffc
Bump the prod-minor-updates group with 8 updates
2025-10-08 13:57:14 -07:00
Owen Schwartz
06156e0ca6 Merge pull request #1633 from fosrl/dependabot/npm_and_yarn/prod-patch-updates-831eaa71e3
Bump the prod-patch-updates group with 3 updates
2025-10-08 13:56:33 -07:00
Owen
02b1de3266 Make sure siteIds are numbers
Fixes PAN-145
2025-10-08 12:06:48 -07:00
Owen
c5b3d92466 Update lock 2025-10-07 21:11:29 -07:00
miloschwartz
186a78b064 Merge branch 'dev' of https://github.com/fosrl/pangolin into dev 2025-10-07 20:33:42 -07:00
miloschwartz
9a808dc139 fix invite flow 2025-10-07 20:32:44 -07:00
dependabot[bot]
977404b8c3 Bump the prod-minor-updates group with 8 updates
Bumps the prod-minor-updates group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.837.0` | `3.901.0` |
| [eslint](https://github.com/eslint/eslint) | `9.35.0` | `9.37.0` |
| [ioredis](https://github.com/luin/ioredis) | `5.6.1` | `5.8.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.544.0` | `0.545.0` |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.1.1` | `19.2.0` |
| [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.1.1` | `19.2.0` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.62.0` | `7.64.0` |
| [winston](https://github.com/winstonjs/winston) | `3.17.0` | `3.18.3` |


Updates `@aws-sdk/client-s3` from 3.837.0 to 3.901.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.901.0/clients/client-s3)

Updates `eslint` from 9.35.0 to 9.37.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.35.0...v9.37.0)

Updates `ioredis` from 5.6.1 to 5.8.1
- [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.6.1...v5.8.1)

Updates `lucide-react` from 0.544.0 to 0.545.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.545.0/packages/lucide-react)

Updates `react` from 19.1.1 to 19.2.0
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.0/packages/react)

Updates `react-dom` from 19.1.1 to 19.2.0
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.0/packages/react-dom)

Updates `react-hook-form` from 7.62.0 to 7.64.0
- [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.62.0...v7.64.0)

Updates `winston` from 3.17.0 to 3.18.3
- [Release notes](https://github.com/winstonjs/winston/releases)
- [Changelog](https://github.com/winstonjs/winston/blob/master/CHANGELOG.md)
- [Commits](https://github.com/winstonjs/winston/compare/v3.17.0...v3.18.3)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.901.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: eslint
  dependency-version: 9.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: ioredis
  dependency-version: 5.8.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: lucide-react
  dependency-version: 0.545.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react
  dependency-version: 19.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react-dom
  dependency-version: 19.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react-hook-form
  dependency-version: 7.64.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: winston
  dependency-version: 3.18.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-08 01:29:00 +00:00
dependabot[bot]
b00143ce9b Bump the prod-patch-updates group with 3 updates
Bumps the prod-patch-updates group with 3 updates: [next-intl](https://github.com/amannn/next-intl), [nodemailer](https://github.com/nodemailer/nodemailer) and [semver](https://github.com/npm/node-semver).


Updates `next-intl` from 4.3.9 to 4.3.11
- [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.3.9...v4.3.11)

Updates `nodemailer` from 7.0.7 to 7.0.9
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v7.0.7...v7.0.9)

Updates `semver` from 7.7.2 to 7.7.3
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.7.2...v7.7.3)

---
updated-dependencies:
- dependency-name: next-intl
  dependency-version: 4.3.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: nodemailer
  dependency-version: 7.0.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: semver
  dependency-version: 7.7.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-08 01:25:17 +00:00
Owen
4435d9a248 Merge branch 'dev' 2025-10-07 15:08:32 -07:00
Owen
7d0303e2be Add postgres pool info to config 2025-10-07 15:06:42 -07:00
Owen Schwartz
a0da9c1129 Merge pull request #1625 from Lokowitz/add-dev-images
Add docker dev image creation workflow for PRs
2025-10-07 12:15:54 -07:00
Owen Schwartz
5e73690570 Merge pull request #1627 from fosrl/dependabot/npm_and_yarn/prod-patch-updates-d783df5103
Bump the prod-patch-updates group with 7 updates
2025-10-06 21:32:14 -07:00
Owen Schwartz
b0409b7d52 Merge pull request #1626 from fosrl/dependabot/npm_and_yarn/dev-minor-updates-75f37cbce1
Bump the dev-minor-updates group with 4 updates
2025-10-06 21:32:02 -07:00
dependabot[bot]
fe474b3989 Bump the prod-patch-updates group with 7 updates
Bumps the prod-patch-updates group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@react-email/components](https://github.com/resend/react-email/tree/HEAD/packages/components) | `0.5.5` | `0.5.6` |
| [@react-email/render](https://github.com/resend/react-email/tree/HEAD/packages/render) | `1.3.1` | `1.3.2` |
| [@simplewebauthn/browser](https://github.com/MasterKale/SimpleWebAuthn/tree/HEAD/packages/browser) | `13.2.0` | `13.2.2` |
| [@simplewebauthn/server](https://github.com/MasterKale/SimpleWebAuthn/tree/HEAD/packages/server) | `13.2.1` | `13.2.2` |
| [nodemailer](https://github.com/nodemailer/nodemailer) | `7.0.6` | `7.0.7` |
| [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.9.2` | `5.9.3` |
| [resend](https://github.com/resendlabs/resend-node) | `6.1.1` | `6.1.2` |


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

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

Updates `@simplewebauthn/browser` from 13.2.0 to 13.2.2
- [Release notes](https://github.com/MasterKale/SimpleWebAuthn/releases)
- [Changelog](https://github.com/MasterKale/SimpleWebAuthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/MasterKale/SimpleWebAuthn/commits/v13.2.2/packages/browser)

Updates `@simplewebauthn/server` from 13.2.1 to 13.2.2
- [Release notes](https://github.com/MasterKale/SimpleWebAuthn/releases)
- [Changelog](https://github.com/MasterKale/SimpleWebAuthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/MasterKale/SimpleWebAuthn/commits/v13.2.2/packages/server)

Updates `nodemailer` from 7.0.6 to 7.0.7
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v7.0.6...v7.0.7)

Updates `posthog-node` from 5.9.2 to 5.9.3
- [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.9.3/packages/node)

Updates `resend` from 6.1.1 to 6.1.2
- [Release notes](https://github.com/resendlabs/resend-node/releases)
- [Commits](https://github.com/resendlabs/resend-node/compare/v6.1.1...v6.1.2)

---
updated-dependencies:
- dependency-name: "@react-email/components"
  dependency-version: 0.5.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@react-email/render"
  dependency-version: 1.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@simplewebauthn/browser"
  dependency-version: 13.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@simplewebauthn/server"
  dependency-version: 13.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: nodemailer
  dependency-version: 7.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: posthog-node
  dependency-version: 5.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: resend
  dependency-version: 6.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-07 01:28:25 +00:00
dependabot[bot]
5154d5d3ee Bump the dev-minor-updates group with 4 updates
Bumps the dev-minor-updates group with 4 updates: [@react-email/preview-server](https://github.com/resend/react-email/tree/HEAD/packages/preview-server), [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [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 `@react-email/preview-server` from 4.2.12 to 4.3.0
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/preview-server/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/preview-server@4.3.0/packages/preview-server)

Updates `@types/node` from 24.6.2 to 24.7.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `react-email` from 4.2.12 to 4.3.0
- [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@4.3.0/packages/react-email)

Updates `typescript-eslint` from 8.45.0 to 8.46.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.46.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@react-email/preview-server"
  dependency-version: 4.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: "@types/node"
  dependency-version: 24.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: react-email
  dependency-version: 4.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: typescript-eslint
  dependency-version: 8.46.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-07 01:23:26 +00:00
Marvin
62df92f63a Update dev-image.yml 2025-10-06 21:37:22 +02:00
Marvin
e2534af40e Create dev-image.yml 2025-10-06 20:42:24 +02:00
Owen Schwartz
2ee3f10e02 Merge pull request #1522 from jln-brtn/feature-header-authentication
Feature HTTP Basic Authentication support  #226 #937
2025-10-06 11:14:46 -07:00
Owen
5a3bf2f758 Fix import issue 2025-10-06 11:06:41 -07:00
Owen
e121dd0d1d Add to blueprints 2025-10-06 11:02:08 -07:00
Owen
2c46a37a53 Include in hybrid 2025-10-06 10:31:31 -07:00
Owen
23f05d7f4e Add translations to EN 2025-10-06 10:20:01 -07:00
Owen
6105eea7a9 Fix rebase 2025-10-06 10:16:29 -07:00
Owen
850e9a734a Adding HTTP Header Authentication 2025-10-06 10:14:02 -07:00
Owen Schwartz
cb7c57fd03 Merge pull request #1621 from fosrl/dependabot/npm_and_yarn/dev-patch-updates-5e2570e910
Bump @types/node from 24.6.1 to 24.6.2 in the dev-patch-updates group
2025-10-06 09:52:18 -07:00
Owen Schwartz
494d0f7c14 Merge pull request #1622 from fosrl/dependabot/npm_and_yarn/dev-minor-updates-44a7c5045b
Bump @react-email/preview-server from 4.1.0 to 4.2.12 in the dev-minor-updates group
2025-10-06 09:52:07 -07:00
Owen Schwartz
a4e480e02b Merge pull request #1539 from OddMagnet/feature-add-resource-name-to-resource-id
[Feature] Update traefik dynamic config to also use resource name
2025-10-06 09:51:22 -07:00
Owen
cd285cc019 Clean up and copy to getTraefikConfig 2025-10-06 09:50:18 -07:00
OddMagnet
9e8e00d4bb Update traefik dynamic config to also use resource name 2025-10-06 17:33:08 +02:00
dependabot[bot]
389834f735 Bump @react-email/preview-server in the dev-minor-updates group
Bumps the dev-minor-updates group with 1 update: [@react-email/preview-server](https://github.com/resend/react-email/tree/HEAD/packages/preview-server).


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

---
updated-dependencies:
- dependency-name: "@react-email/preview-server"
  dependency-version: 4.2.12
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-06 01:26:52 +00:00
dependabot[bot]
b14ddc07fb Bump @types/node from 24.6.1 to 24.6.2 in the dev-patch-updates group
Bumps the dev-patch-updates group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@types/node` from 24.6.1 to 24.6.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.6.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-06 01:24:59 +00:00
407 changed files with 15143 additions and 15440 deletions

View File

@@ -29,4 +29,6 @@ CONTRIBUTING.md
dist dist
.git .git
migrations/ migrations/
config/ config/
build.ts
tsconfig.json

View File

@@ -8,7 +8,7 @@ on:
jobs: jobs:
release: release:
name: Build and Release name: Build and Release
runs-on: ubuntu-latest runs-on: amd64-runner
steps: steps:
- name: Checkout code - name: Checkout code

4
.gitignore vendored
View File

@@ -47,4 +47,6 @@ server/db/index.ts
server/build.ts server/build.ts
postgres/ postgres/
dynamic/ dynamic/
*.mmdb *.mmdb
scratch/
tsconfig.json

View File

@@ -15,9 +15,29 @@ RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts
RUN echo "export const build = \"$BUILD\" as any;" > server/build.ts RUN echo "export const build = \"$BUILD\" as any;" > server/build.ts
RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema.ts --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema.ts --out init; fi # Copy the appropriate TypeScript configuration based on build type
RUN if [ "$BUILD" = "oss" ]; then cp tsconfig.oss.json tsconfig.json; \
elif [ "$BUILD" = "saas" ]; then cp tsconfig.saas.json tsconfig.json; \
elif [ "$BUILD" = "enterprise" ]; then cp tsconfig.enterprise.json tsconfig.json; \
fi
# if the build is oss then remove the server/private directory
RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi
RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema --out init; fi
RUN mkdir -p dist
RUN npm run next:build
RUN node esbuild.mjs -e server/index.ts -o dist/server.mjs -b $BUILD
RUN if [ "$DATABASE" = "pg" ]; then \
node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs; \
else \
node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs; \
fi
# test to make sure the build output is there and error if not
RUN test -f dist/server.mjs
RUN npm run build:$DATABASE
RUN npm run build:cli RUN npm run build:cli
FROM node:22-alpine AS runner FROM node:22-alpine AS runner

View File

@@ -8,6 +8,7 @@ build-release:
exit 1; \ exit 1; \
fi fi
docker buildx build \ docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=sqlite \ --build-arg DATABASE=sqlite \
--platform linux/arm64,linux/amd64 \ --platform linux/arm64,linux/amd64 \
--tag fosrl/pangolin:latest \ --tag fosrl/pangolin:latest \
@@ -16,6 +17,7 @@ build-release:
--tag fosrl/pangolin:$(tag) \ --tag fosrl/pangolin:$(tag) \
--push . --push .
docker buildx build \ docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=pg \ --build-arg DATABASE=pg \
--platform linux/arm64,linux/amd64 \ --platform linux/arm64,linux/amd64 \
--tag fosrl/pangolin:postgresql-latest \ --tag fosrl/pangolin:postgresql-latest \
@@ -23,6 +25,24 @@ build-release:
--tag fosrl/pangolin:postgresql-$(minor_tag) \ --tag fosrl/pangolin:postgresql-$(minor_tag) \
--tag fosrl/pangolin:postgresql-$(tag) \ --tag fosrl/pangolin:postgresql-$(tag) \
--push . --push .
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=sqlite \
--platform linux/arm64,linux/amd64 \
--tag fosrl/pangolin:ee-latest \
--tag fosrl/pangolin:ee-$(major_tag) \
--tag fosrl/pangolin:ee-$(minor_tag) \
--tag fosrl/pangolin:ee-$(tag) \
--push .
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=pg \
--platform linux/arm64,linux/amd64 \
--tag fosrl/pangolin:ee-postgresql-latest \
--tag fosrl/pangolin:ee-postgresql-$(major_tag) \
--tag fosrl/pangolin:ee-postgresql-$(minor_tag) \
--tag fosrl/pangolin:ee-postgresql-$(tag) \
--push .
build-arm: build-arm:
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest . docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .

141
README.md
View File

@@ -1,46 +1,36 @@
<div align="center"> <div align="center">
<h2> <h2>
<picture> <a href="https://digpangolin.com">
<source media="(prefers-color-scheme: dark)" srcset="public/logo/word_mark_white.png"> <picture>
<img alt="Pangolin Logo" src="public/logo/word_mark_black.png" width="250"> <source media="(prefers-color-scheme: dark)" srcset="public/logo/word_mark_white.png">
<img alt="Pangolin Logo" src="public/logo/word_mark_black.png" width="350">
</picture> </picture>
</a>
</h2> </h2>
</div> </div>
<h4 align="center">Secure gateway to your private networks</h4>
<div align="center">
_Pangolin tunnels your services to the internet so you can access anything from anywhere._
</div>
<div align="center"> <div align="center">
<h5> <h5>
<a href="https://digpangolin.com"> <a href="https://digpangolin.com">
Website Website
</a> </a>
<span> | </span> <span> | </span>
<a href="https://docs.digpangolin.com/self-host/quick-install-managed"> <a href="https://docs.digpangolin.com/">
Quick Install Guide Documentation
</a> </a>
<span> | </span> <span> | </span>
<a href="mailto:contact@fossorial.io"> <a href="mailto:contact@fossorial.io">
Contact Us Contact Us
</a> </a>
<span> | </span>
<a href="https://digpangolin.com/slack">
Slack
</a>
<span> | </span>
<a href="https://discord.gg/HCJR8Xhme4">
Discord
</a>
</h5> </h5>
</div>
<div align="center">
[![Discord](https://img.shields.io/discord/1325658630518865980?logo=discord&style=flat-square)](https://discord.gg/HCJR8Xhme4)
[![Slack](https://img.shields.io/badge/chat-slack-yellow?style=flat-square&logo=slack)](https://digpangolin.com/slack) [![Slack](https://img.shields.io/badge/chat-slack-yellow?style=flat-square&logo=slack)](https://digpangolin.com/slack)
[![Docker](https://img.shields.io/docker/pulls/fosrl/pangolin?style=flat-square)](https://hub.docker.com/r/fosrl/pangolin) [![Docker](https://img.shields.io/docker/pulls/fosrl/pangolin?style=flat-square)](https://hub.docker.com/r/fosrl/pangolin)
![Stars](https://img.shields.io/github/stars/fosrl/pangolin?style=flat-square) ![Stars](https://img.shields.io/github/stars/fosrl/pangolin?style=flat-square)
[![Discord](https://img.shields.io/discord/1325658630518865980?logo=discord&style=flat-square)](https://discord.gg/HCJR8Xhme4)
[![YouTube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@fossorial-app) [![YouTube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@fossorial-app)
</div> </div>
@@ -51,107 +41,48 @@ _Pangolin tunnels your services to the internet so you can access anything from
</strong> </strong>
</p> </p>
Pangolin is a self-hosted tunneled reverse proxy server with identity and access control, designed to securely expose private resources on distributed networks. Acting as a central hub, it connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports. Pangolin is a self-hosted tunneled reverse proxy server with identity and context aware access control, designed to easily expose and protect applications running anywhere. Pangolin acts as a central hub and connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports or requiring a VPN.
<img src="public/screenshots/hero.png" alt="Preview"/> ## Installation
![gif](public/clip.gif) Check out the [quick install guide](https://docs.digpangolin.com/self-host/quick-install) for how to install and set up Pangolin.
## Key Features
### Reverse Proxy Through WireGuard Tunnel
- Expose private resources on your network **without opening ports** (firewall punching).
- Secure and easy to configure private connectivity via a custom **user space WireGuard client**, [Newt](https://github.com/fosrl/newt).
- Built-in support for any WireGuard client.
- Automated **SSL certificates** (https) via [LetsEncrypt](https://letsencrypt.org/).
- Support for HTTP/HTTPS and **raw TCP/UDP services**.
- Load balancing.
- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin) and [Geoblock](https://github.com/PascalMinder/geoblock).
- **Automatically install and configure Crowdsec via Pangolin's installer script.**
- Attach as many sites to the central server as you wish.
### Identity & Access Management
- Centralized authentication system using platform SSO. **Users will only have to manage one login.**
- **Define access control rules for IPs, IP ranges, and URL paths per resource.**
- TOTP with backup codes for two-factor authentication.
- Create organizations, each with multiple sites, users, and roles.
- **Role-based access control** to manage resource access permissions.
- Additional authentication options include:
- Email whitelisting with **one-time passcodes.**
- **Temporary, self-destructing share links.**
- Resource specific pin codes.
- Resource specific passwords.
- Passkeys
- External identity provider (IdP) support with OAuth2/OIDC, such as Authentik, Keycloak, Okta, and others.
- Auto-provision users and roles from your IdP.
<img src="public/auth-diagram1.png" alt="Auth and diagram"/>
## Use Cases
### Manage Access to Internal Apps
- Grant users access to your apps from anywhere using just a web browser. No client software required.
### Developers and DevOps
- Expose and test internal tools and dashboards like **Grafana**. Bring localhost or private IPs online for easy access.
### Secure API Gateway
- One application load balancer across multiple clouds and on-premises.
### IoT and Edge Devices
- Easily expose **IoT devices**, **edge servers**, or **Raspberry Pi** to the internet for field equipment monitoring.
<img src="public/screenshots/sites.png" alt="Sites"/>
## Deployment Options ## Deployment Options
### Fully Self Hosted | <img width=500 /> | Description |
|-----------------|--------------|
| **Self-Host: Community Edition** | Free, open source, and licensed under AGPL-3. |
| **Self-Host: Enterprise Edition** | Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses earning under \$100K USD annually. |
| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing — no infrastructure required. Or, self-host your own [remote node](https://docs.digpangolin.com/manage/remote-node/nodes) and connect to our control plane. |
Host the full application on your own server or on the cloud with a VPS. Take a look at the [documentation](https://docs.digpangolin.com/self-host/quick-install) to get started. ## Key Features
> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can get a [**VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**](https://my.racknerd.com/aff.php?aff=13788&pid=912). That's a great deal! Pangolin packages everything you need for seamless application access and exposure into one cohesive platform.
### Pangolin Cloud | <img width=500 /> | <img width=500 /> |
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
| **Manage applications in one place**<br /><br /> Pangolin provides a unified dashboard where you can monitor, configure, and secure all of your services regardless of where they are hosted. | <img src="public/screenshots/hero.png" width=500 /><tr></tr> |
| **Reverse proxy across networks anywhere**<br /><br />Route traffic via tunnels to any private network. Pangolin works like a reverse proxy that spans multiple networks and handles routing, load balancing, health checking, and more to the right services on the other end. | <img src="public/screenshots/sites.png" width=500 /><tr></tr> |
| **Enforce identity and context aware rules**<br /><br />Protect your applications with identity and context aware rules such as SSO, OIDC, PIN, password, temporary share links, geolocation, IP, and more. | <img src="public/auth-diagram1.png" width=500 /><tr></tr> |
| **Quickly connect Pangolin sites**<br /><br />Pangolin's lightweight [Newt](https://github.com/fosrl/newt) client runs in userspace and can run anywhere. Use it as a site connector to route traffic to backends across all of your environments. | <img src="public/clip.gif" width=500 /><tr></tr> |
Easy to use with simple [pay as you go pricing](https://digpangolin.com/pricing). [Check it out here](https://pangolin.fossorial.io/auth/signup). ## Get Started
- Everything you get with self hosted Pangolin, but fully managed for you. ### Check out the docs
### Managed & High Availability We encourage everyone to read the full documentation first, which is
available at [docs.digpangolin.com](https://docs.digpangolin.com). This README provides only a very brief subset of
the docs to illustrate some basic ideas.
Managed control plane, your infrastructure ### Sign up and try now
- We manage database and control plane. For Pangolin's managed service, you will first need to create an account at
- You self-host lightweight exit-node. [pangolin.fossorial.io](https://pangolin.fossorial.io). We have a generous free tier to get started.
- Traffic flows through your infra.
- We coordinate failover between your nodes or to Cloud when things go bad.
Try it out using [Pangolin Cloud](https://pangolin.fossorial.io)
### Full Enterprise On-Premises
[Contact us](mailto:numbat@fossorial.io) for a full distributed and enterprise deployments on your infrastructure controlled by your team.
## Project Development / Roadmap
We want to hear your feature requests! Add them to the [discussion board](https://github.com/orgs/fosrl/discussions/categories/feature-requests).
## Licensing ## Licensing
Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io). Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://digpangolin.com/fcl.html). For inquiries about commercial licensing, please contact us at [contact@fossorial.io](mailto:contact@fossorial.io).
## Contributions ## Contributions
Looking for something to contribute? Take a look at issues marked with [help wanted](https://github.com/fosrl/pangolin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22). Also take a look through the freature requests in Discussions - any are available and some are marked as a good first issue.
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices. Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
Please post bug reports and other functional issues in the [Issues](https://github.com/fosrl/pangolin/issues) section of the repository.
If you are looking to help with translations, please contribute [on Crowdin](https://crowdin.com/project/fossorial-pangolin) or open a PR with changes to the translations files found in `messages/`.

View File

@@ -20,7 +20,7 @@ services:
pangolin: pangolin:
condition: service_healthy condition: service_healthy
command: command:
- --reachableAt=http://gerbil:3003 - --reachableAt=http://gerbil:3004
- --generateAndSaveKeyTo=/var/config/key - --generateAndSaveKeyTo=/var/config/key
- --remoteConfig=http://pangolin:3001/api/v1/ - --remoteConfig=http://pangolin:3001/api/v1/
volumes: volumes:

View File

@@ -1,16 +1,9 @@
import { defineConfig } from "drizzle-kit"; import { defineConfig } from "drizzle-kit";
import path from "path"; import path from "path";
import { build } from "@server/build";
let schema; const schema = [
if (build === "oss") { path.join("server", "db", "pg", "schema"),
schema = [path.join("server", "db", "pg", "schema.ts")]; ];
} else {
schema = [
path.join("server", "db", "pg", "schema.ts"),
path.join("server", "db", "pg", "privateSchema.ts")
];
}
export default defineConfig({ export default defineConfig({
dialect: "postgresql", dialect: "postgresql",

View File

@@ -1,17 +1,10 @@
import { build } from "@server/build";
import { APP_PATH } from "@server/lib/consts"; import { APP_PATH } from "@server/lib/consts";
import { defineConfig } from "drizzle-kit"; import { defineConfig } from "drizzle-kit";
import path from "path"; import path from "path";
let schema; const schema = [
if (build === "oss") { path.join("server", "db", "sqlite", "schema"),
schema = [path.join("server", "db", "sqlite", "schema.ts")]; ];
} else {
schema = [
path.join("server", "db", "sqlite", "schema.ts"),
path.join("server", "db", "sqlite", "privateSchema.ts")
];
}
export default defineConfig({ export default defineConfig({
dialect: "sqlite", dialect: "sqlite",

View File

@@ -2,8 +2,9 @@ import esbuild from "esbuild";
import yargs from "yargs"; import yargs from "yargs";
import { hideBin } from "yargs/helpers"; import { hideBin } from "yargs/helpers";
import { nodeExternalsPlugin } from "esbuild-node-externals"; import { nodeExternalsPlugin } from "esbuild-node-externals";
import path from "path";
import fs from "fs";
// import { glob } from "glob"; // import { glob } from "glob";
// import path from "path";
const banner = ` const banner = `
// patch __dirname // patch __dirname
@@ -18,7 +19,7 @@ const require = topLevelCreateRequire(import.meta.url);
`; `;
const argv = yargs(hideBin(process.argv)) const argv = yargs(hideBin(process.argv))
.usage("Usage: $0 -entry [string] -out [string]") .usage("Usage: $0 -entry [string] -out [string] -build [string]")
.option("entry", { .option("entry", {
alias: "e", alias: "e",
describe: "Entry point file", describe: "Entry point file",
@@ -31,6 +32,13 @@ const argv = yargs(hideBin(process.argv))
type: "string", type: "string",
demandOption: true, demandOption: true,
}) })
.option("build", {
alias: "b",
describe: "Build type (oss, saas, enterprise)",
type: "string",
choices: ["oss", "saas", "enterprise"],
default: "oss",
})
.help() .help()
.alias("help", "h").argv; .alias("help", "h").argv;
@@ -46,6 +54,179 @@ function getPackagePaths() {
return ["package.json"]; return ["package.json"];
} }
// Plugin to guard against bad imports from #private
function privateImportGuardPlugin() {
return {
name: "private-import-guard",
setup(build) {
const violations = [];
build.onResolve({ filter: /^#private\// }, (args) => {
const importingFile = args.importer;
// Check if the importing file is NOT in server/private
const normalizedImporter = path.normalize(importingFile);
const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private"));
if (!isInServerPrivate) {
const violation = {
file: importingFile,
importPath: args.path,
resolveDir: args.resolveDir
};
violations.push(violation);
console.log(`PRIVATE IMPORT VIOLATION:`);
console.log(` File: ${importingFile}`);
console.log(` Import: ${args.path}`);
console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`);
console.log('');
}
// Return null to let the default resolver handle it
return null;
});
build.onEnd((result) => {
if (violations.length > 0) {
console.log(`\nSUMMARY: Found ${violations.length} private import violation(s):`);
violations.forEach((v, i) => {
console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`);
});
console.log('');
result.errors.push({
text: `Private import violations detected: ${violations.length} violation(s) found`,
location: null,
notes: violations.map(v => ({
text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
location: null
}))
});
}
});
}
};
}
// Plugin to guard against bad imports from #private
function dynamicImportGuardPlugin() {
return {
name: "dynamic-import-guard",
setup(build) {
const violations = [];
build.onResolve({ filter: /^#dynamic\// }, (args) => {
const importingFile = args.importer;
// Check if the importing file is NOT in server/private
const normalizedImporter = path.normalize(importingFile);
const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private"));
if (isInServerPrivate) {
const violation = {
file: importingFile,
importPath: args.path,
resolveDir: args.resolveDir
};
violations.push(violation);
console.log(`DYNAMIC IMPORT VIOLATION:`);
console.log(` File: ${importingFile}`);
console.log(` Import: ${args.path}`);
console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`);
console.log('');
}
// Return null to let the default resolver handle it
return null;
});
build.onEnd((result) => {
if (violations.length > 0) {
console.log(`\nSUMMARY: Found ${violations.length} dynamic import violation(s):`);
violations.forEach((v, i) => {
console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`);
});
console.log('');
result.errors.push({
text: `Dynamic import violations detected: ${violations.length} violation(s) found`,
location: null,
notes: violations.map(v => ({
text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
location: null
}))
});
}
});
}
};
}
// Plugin to dynamically switch imports based on build type
function dynamicImportSwitcherPlugin(buildValue) {
return {
name: "dynamic-import-switcher",
setup(build) {
const switches = [];
build.onStart(() => {
console.log(`Dynamic import switcher using build type: ${buildValue}`);
});
build.onResolve({ filter: /^#dynamic\// }, (args) => {
// Extract the path after #dynamic/
const dynamicPath = args.path.replace(/^#dynamic\//, '');
// Determine the replacement based on build type
let replacement;
if (buildValue === "oss") {
replacement = `#open/${dynamicPath}`;
} else if (buildValue === "saas" || buildValue === "enterprise") {
replacement = `#closed/${dynamicPath}`; // We use #closed here so that the route guards dont complain after its been changed but this is the same as #private
} else {
console.warn(`Unknown build type '${buildValue}', defaulting to #open/`);
replacement = `#open/${dynamicPath}`;
}
const switchInfo = {
file: args.importer,
originalPath: args.path,
replacementPath: replacement,
buildType: buildValue
};
switches.push(switchInfo);
console.log(`DYNAMIC IMPORT SWITCH:`);
console.log(` File: ${args.importer}`);
console.log(` Original: ${args.path}`);
console.log(` Switched to: ${replacement} (build: ${buildValue})`);
console.log('');
// Rewrite the import path and let the normal resolution continue
return build.resolve(replacement, {
importer: args.importer,
namespace: args.namespace,
resolveDir: args.resolveDir,
kind: args.kind
});
});
build.onEnd((result) => {
if (switches.length > 0) {
console.log(`\nDYNAMIC IMPORT SUMMARY: Switched ${switches.length} import(s) for build type '${buildValue}':`);
switches.forEach((s, i) => {
console.log(` ${i + 1}. ${path.relative(process.cwd(), s.file)}`);
console.log(` ${s.originalPath}${s.replacementPath}`);
});
console.log('');
}
});
}
};
}
esbuild esbuild
.build({ .build({
entryPoints: [argv.entry], entryPoints: [argv.entry],
@@ -59,6 +240,9 @@ esbuild
platform: "node", platform: "node",
external: ["body-parser"], external: ["body-parser"],
plugins: [ plugins: [
privateImportGuardPlugin(),
dynamicImportGuardPlugin(),
dynamicImportSwitcherPlugin(argv.build),
nodeExternalsPlugin({ nodeExternalsPlugin({
packagePath: getPackagePaths(), packagePath: getPackagePaths(),
}), }),
@@ -66,7 +250,27 @@ esbuild
sourcemap: "inline", sourcemap: "inline",
target: "node22", target: "node22",
}) })
.then(() => { .then((result) => {
// Check if there were any errors in the build result
if (result.errors && result.errors.length > 0) {
console.error(`Build failed with ${result.errors.length} error(s):`);
result.errors.forEach((error, i) => {
console.error(`${i + 1}. ${error.text}`);
if (error.notes) {
error.notes.forEach(note => {
console.error(` - ${note.text}`);
});
}
});
// remove the output file if it was created
if (fs.existsSync(argv.out)) {
fs.unlinkSync(argv.out);
}
process.exit(1);
}
console.log("Build completed successfully"); console.log("Build completed successfully");
}) })
.catch((error) => { .catch((error) => {

View File

@@ -1,15 +1,10 @@
# To see all available options, please visit the docs: # To see all available options, please visit the docs:
# https://docs.digpangolin.com/self-host/advanced/config-file # https://docs.digpangolin.com/
gerbil: gerbil:
start_port: 51820 start_port: 51820
base_endpoint: "{{.DashboardDomain}}" base_endpoint: "{{.DashboardDomain}}"
{{if .HybridMode}}
managed:
id: "{{.HybridId}}"
secret: "{{.HybridSecret}}"
{{else}}
app: app:
dashboard_url: "https://{{.DashboardDomain}}" dashboard_url: "https://{{.DashboardDomain}}"
log_level: "info" log_level: "info"
@@ -28,6 +23,7 @@ server:
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"] methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
allowed_headers: ["X-CSRF-Token", "Content-Type"] allowed_headers: ["X-CSRF-Token", "Content-Type"]
credentials: false credentials: false
{{if .EnableGeoblocking}}maxmind_db_path: "./config/GeoLite2-Country.mmdb"{{end}}
{{if .EnableEmail}} {{if .EnableEmail}}
email: email:
smtp_host: "{{.EmailSMTPHost}}" smtp_host: "{{.EmailSMTPHost}}"
@@ -40,5 +36,4 @@ flags:
require_email_verification: {{.EnableEmail}} require_email_verification: {{.EnableEmail}}
disable_signup_without_invite: true disable_signup_without_invite: true
disable_user_create_org: false disable_user_create_org: false
allow_raw_resources: true allow_raw_resources: true
{{end}}

View File

@@ -6,8 +6,6 @@ services:
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ./config:/app/config - ./config:/app/config
- pangolin-data:/var/certificates
- pangolin-data:/var/dynamic
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"] test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
interval: "10s" interval: "10s"
@@ -22,7 +20,7 @@ services:
pangolin: pangolin:
condition: service_healthy condition: service_healthy
command: command:
- --reachableAt=http://gerbil:3003 - --reachableAt=http://gerbil:3004
- --generateAndSaveKeyTo=/var/config/key - --generateAndSaveKeyTo=/var/config/key
- --remoteConfig=http://pangolin:3001/api/v1/ - --remoteConfig=http://pangolin:3001/api/v1/
volumes: volumes:
@@ -33,7 +31,7 @@ services:
ports: ports:
- 51820:51820/udp - 51820:51820/udp
- 21820:21820/udp - 21820:21820/udp
- 443:{{if .HybridMode}}8443{{else}}443{{end}} - 443:443
- 80:80 - 80:80
{{end}} {{end}}
traefik: traefik:
@@ -56,15 +54,9 @@ services:
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration - ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates - ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs - ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
# Shared volume for certificates and dynamic config in file mode
- pangolin-data:/var/certificates:ro
- pangolin-data:/var/dynamic:ro
networks: networks:
default: default:
driver: bridge driver: bridge
name: pangolin name: pangolin
{{if .EnableIPv6}} enable_ipv6: true{{end}} {{if .EnableIPv6}} enable_ipv6: true{{end}}
volumes:
pangolin-data:

View File

@@ -3,17 +3,12 @@ api:
dashboard: true dashboard: true
providers: providers:
{{if not .HybridMode}}
http: http:
endpoint: "http://pangolin:3001/api/v1/traefik-config" endpoint: "http://pangolin:3001/api/v1/traefik-config"
pollInterval: "5s" pollInterval: "5s"
file: file:
filename: "/etc/traefik/dynamic_config.yml" filename: "/etc/traefik/dynamic_config.yml"
{{else}}
file:
directory: "/var/dynamic"
watch: true
{{end}}
experimental: experimental:
plugins: plugins:
badger: badger:
@@ -27,7 +22,7 @@ log:
maxBackups: 3 maxBackups: 3
maxAge: 3 maxAge: 3
compress: true compress: true
{{if not .HybridMode}}
certificatesResolvers: certificatesResolvers:
letsencrypt: letsencrypt:
acme: acme:
@@ -36,22 +31,18 @@ certificatesResolvers:
email: "{{.LetsEncryptEmail}}" email: "{{.LetsEncryptEmail}}"
storage: "/letsencrypt/acme.json" storage: "/letsencrypt/acme.json"
caServer: "https://acme-v02.api.letsencrypt.org/directory" caServer: "https://acme-v02.api.letsencrypt.org/directory"
{{end}}
entryPoints: entryPoints:
web: web:
address: ":80" address: ":80"
websecure: websecure:
address: ":443" address: ":443"
{{if .HybridMode}} proxyProtocol:
trustedIPs:
- 0.0.0.0/0
- ::1/128{{end}}
transport: transport:
respondingTimeouts: respondingTimeouts:
readTimeout: "30m" readTimeout: "30m"
{{if not .HybridMode}} http: http:
tls: tls:
certResolver: "letsencrypt"{{end}} certResolver: "letsencrypt"
serversTransport: serversTransport:
insecureSkipVerify: true insecureSkipVerify: true

180
install/get-installer.sh Normal file
View File

@@ -0,0 +1,180 @@
#!/bin/bash
# Get installer - Cross-platform installation script
# Usage: curl -fsSL https://raw.githubusercontent.com/fosrl/installer/refs/heads/main/get-installer.sh | bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# GitHub repository info
REPO="fosrl/pangolin"
GITHUB_API_URL="https://api.github.com/repos/${REPO}/releases/latest"
# Function to print colored output
print_status() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Function to get latest version from GitHub API
get_latest_version() {
local latest_info
if command -v curl >/dev/null 2>&1; then
latest_info=$(curl -fsSL "$GITHUB_API_URL" 2>/dev/null)
elif command -v wget >/dev/null 2>&1; then
latest_info=$(wget -qO- "$GITHUB_API_URL" 2>/dev/null)
else
print_error "Neither curl nor wget is available. Please install one of them." >&2
exit 1
fi
if [ -z "$latest_info" ]; then
print_error "Failed to fetch latest version information" >&2
exit 1
fi
# Extract version from JSON response (works without jq)
local version=$(echo "$latest_info" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')
if [ -z "$version" ]; then
print_error "Could not parse version from GitHub API response" >&2
exit 1
fi
# Remove 'v' prefix if present
version=$(echo "$version" | sed 's/^v//')
echo "$version"
}
# Detect OS and architecture
detect_platform() {
local os arch
# Detect OS - only support Linux
case "$(uname -s)" in
Linux*) os="linux" ;;
*)
print_error "Unsupported operating system: $(uname -s). Only Linux is supported."
exit 1
;;
esac
# Detect architecture - only support amd64 and arm64
case "$(uname -m)" in
x86_64|amd64) arch="amd64" ;;
arm64|aarch64) arch="arm64" ;;
*)
print_error "Unsupported architecture: $(uname -m). Only amd64 and arm64 are supported on Linux."
exit 1
;;
esac
echo "${os}_${arch}"
}
# Get installation directory
get_install_dir() {
# Install to the current directory
local install_dir="$(pwd)"
if [ ! -d "$install_dir" ]; then
print_error "Installation directory does not exist: $install_dir"
exit 1
fi
echo "$install_dir"
}
# Download and install installer
install_installer() {
local platform="$1"
local install_dir="$2"
local binary_name="installer_${platform}"
local download_url="${BASE_URL}/${binary_name}"
local temp_file="/tmp/installer"
local final_path="${install_dir}/installer"
print_status "Downloading installer from ${download_url}"
# Download the binary
if command -v curl >/dev/null 2>&1; then
curl -fsSL "$download_url" -o "$temp_file"
elif command -v wget >/dev/null 2>&1; then
wget -q "$download_url" -O "$temp_file"
else
print_error "Neither curl nor wget is available. Please install one of them."
exit 1
fi
# Create install directory if it doesn't exist
mkdir -p "$install_dir"
# Move binary to install directory
mv "$temp_file" "$final_path"
# Make executable
chmod +x "$final_path"
print_status "Installer downloaded to ${final_path}"
}
# Verify installation
verify_installation() {
local install_dir="$1"
local installer_path="${install_dir}/installer"
if [ -f "$installer_path" ] && [ -x "$installer_path" ]; then
print_status "Installation successful!"
return 0
else
print_error "Installation failed. Binary not found or not executable."
return 1
fi
}
# Main installation process
main() {
print_status "Installing latest version of installer..."
# Get latest version
print_status "Fetching latest version from GitHub..."
VERSION=$(get_latest_version)
print_status "Latest version: v${VERSION}"
# Set base URL with the fetched version
BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}"
# Detect platform
PLATFORM=$(detect_platform)
print_status "Detected platform: ${PLATFORM}"
# Get install directory
INSTALL_DIR=$(get_install_dir)
print_status "Install directory: ${INSTALL_DIR}"
# Install installer
install_installer "$PLATFORM" "$INSTALL_DIR"
# Verify installation
if verify_installation "$INSTALL_DIR"; then
print_status "Installer is ready to use!"
else
exit 1
fi
}
# Run main function
main "$@"

View File

@@ -3,8 +3,8 @@ module installer
go 1.24.0 go 1.24.0
require ( require (
golang.org/x/term v0.35.0 golang.org/x/term v0.36.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require golang.org/x/sys v0.36.0 // indirect require golang.org/x/sys v0.37.0 // indirect

View File

@@ -1,7 +1,7 @@
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
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=

View File

@@ -2,7 +2,6 @@ package main
import ( import (
"bufio" "bufio"
"bytes"
"embed" "embed"
"fmt" "fmt"
"io" "io"
@@ -48,10 +47,8 @@ type Config struct {
InstallGerbil bool InstallGerbil bool
TraefikBouncerKey string TraefikBouncerKey string
DoCrowdsecInstall bool DoCrowdsecInstall bool
EnableGeoblocking bool
Secret string Secret string
HybridMode bool
HybridId string
HybridSecret string
} }
type SupportedContainer string type SupportedContainer string
@@ -98,24 +95,6 @@ func main() {
fmt.Println("\n=== Generating Configuration Files ===") fmt.Println("\n=== Generating Configuration Files ===")
// If the secret and id are not generated then generate them
if config.HybridMode && (config.HybridId == "" || config.HybridSecret == "") {
// fmt.Println("Requesting hybrid credentials from cloud...")
credentials, err := requestHybridCredentials()
if err != nil {
fmt.Printf("Error requesting hybrid credentials: %v\n", err)
fmt.Println("Please obtain credentials manually from the dashboard and run the installer again.")
os.Exit(1)
}
config.HybridId = credentials.RemoteExitNodeId
config.HybridSecret = credentials.Secret
fmt.Printf("Your managed credentials have been obtained successfully.\n")
fmt.Printf(" ID: %s\n", config.HybridId)
fmt.Printf(" Secret: %s\n", config.HybridSecret)
fmt.Println("Take these to the Pangolin dashboard https://pangolin.fossorial.io to adopt your node.")
readBool(reader, "Have you adopted your node?", true)
}
if err := createConfigFiles(config); err != nil { if err := createConfigFiles(config); err != nil {
fmt.Printf("Error creating config files: %v\n", err) fmt.Printf("Error creating config files: %v\n", err)
os.Exit(1) os.Exit(1)
@@ -125,6 +104,15 @@ func main() {
fmt.Println("\nConfiguration files created successfully!") fmt.Println("\nConfiguration files created successfully!")
// Download MaxMind database if requested
if config.EnableGeoblocking {
fmt.Println("\n=== Downloading MaxMind Database ===")
if err := downloadMaxMindDatabase(); err != nil {
fmt.Printf("Error downloading MaxMind database: %v\n", err)
fmt.Println("You can download it manually later if needed.")
}
}
fmt.Println("\n=== Starting installation ===") fmt.Println("\n=== Starting installation ===")
if readBool(reader, "Would you like to install and start the containers?", true) { if readBool(reader, "Would you like to install and start the containers?", true) {
@@ -172,9 +160,34 @@ func main() {
} else { } else {
alreadyInstalled = true alreadyInstalled = true
fmt.Println("Looks like you already installed Pangolin!") fmt.Println("Looks like you already installed Pangolin!")
// Check if MaxMind database exists and offer to update it
fmt.Println("\n=== MaxMind Database Update ===")
if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil {
fmt.Println("MaxMind GeoLite2 Country database found.")
if readBool(reader, "Would you like to update the MaxMind database to the latest version?", false) {
if err := downloadMaxMindDatabase(); err != nil {
fmt.Printf("Error updating MaxMind database: %v\n", err)
fmt.Println("You can try updating it manually later if needed.")
}
}
} else {
fmt.Println("MaxMind GeoLite2 Country database not found.")
if readBool(reader, "Would you like to download the MaxMind GeoLite2 database for geoblocking functionality?", false) {
if err := downloadMaxMindDatabase(); err != nil {
fmt.Printf("Error downloading MaxMind database: %v\n", err)
fmt.Println("You can try downloading it manually later if needed.")
}
// Now you need to update your config file accordingly to enable geoblocking
fmt.Println("Please remember to update your config/config.yml file to enable geoblocking! \n")
// add maxmind_db_path: "./config/GeoLite2-Country.mmdb" under server
fmt.Println("Add the following line under the 'server' section:")
fmt.Println(" maxmind_db_path: \"./config/GeoLite2-Country.mmdb\"")
}
}
} }
if !checkIsCrowdsecInstalledInCompose() && !checkIsPangolinInstalledWithHybrid() { if !checkIsCrowdsecInstalledInCompose() {
fmt.Println("\n=== CrowdSec Install ===") fmt.Println("\n=== CrowdSec Install ===")
// check if crowdsec is installed // check if crowdsec is installed
if readBool(reader, "Would you like to install CrowdSec?", false) { if readBool(reader, "Would you like to install CrowdSec?", false) {
@@ -230,7 +243,7 @@ func main() {
} }
} }
if !config.HybridMode && !alreadyInstalled { if !alreadyInstalled {
// Setup Token Section // Setup Token Section
fmt.Println("\n=== Setup Token ===") fmt.Println("\n=== Setup Token ===")
@@ -251,9 +264,7 @@ func main() {
fmt.Println("\nInstallation complete!") fmt.Println("\nInstallation complete!")
if !config.HybridMode && !checkIsPangolinInstalledWithHybrid() { fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
}
} }
func podmanOrDocker(reader *bufio.Reader) SupportedContainer { func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
@@ -328,66 +339,38 @@ func collectUserInput(reader *bufio.Reader) Config {
// Basic configuration // Basic configuration
fmt.Println("\n=== Basic Configuration ===") fmt.Println("\n=== Basic Configuration ===")
for {
response := readString(reader, "Do you want to install Pangolin as a cloud-managed (beta) node? (yes/no)", "") config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
if strings.EqualFold(response, "yes") || strings.EqualFold(response, "y") {
config.HybridMode = true // Set default dashboard domain after base domain is collected
break defaultDashboardDomain := ""
} else if strings.EqualFold(response, "no") || strings.EqualFold(response, "n") { if config.BaseDomain != "" {
config.HybridMode = false defaultDashboardDomain = "pangolin." + config.BaseDomain
break }
} config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
fmt.Println("Please answer 'yes' or 'no'") config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
// Email configuration
fmt.Println("\n=== Email Configuration ===")
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
if config.EnableEmail {
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
} }
if config.HybridMode { // Validate required fields
alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard? If not, we will create them later", false) if config.BaseDomain == "" {
fmt.Println("Error: Domain name is required")
if alreadyHaveCreds { os.Exit(1)
config.HybridId = readString(reader, "Enter your ID", "") }
config.HybridSecret = readString(reader, "Enter your secret", "") if config.LetsEncryptEmail == "" {
} fmt.Println("Error: Let's Encrypt email is required")
os.Exit(1)
// Try to get public IP as default
publicIP := getPublicIP()
if publicIP != "" {
fmt.Printf("Detected public IP: %s\n", publicIP)
}
config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", publicIP)
config.InstallGerbil = true
} else {
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
// Set default dashboard domain after base domain is collected
defaultDashboardDomain := ""
if config.BaseDomain != "" {
defaultDashboardDomain = "pangolin." + config.BaseDomain
}
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
// Email configuration
fmt.Println("\n=== Email Configuration ===")
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
if config.EnableEmail {
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
}
// Validate required fields
if config.BaseDomain == "" {
fmt.Println("Error: Domain name is required")
os.Exit(1)
}
if config.LetsEncryptEmail == "" {
fmt.Println("Error: Let's Encrypt email is required")
os.Exit(1)
}
} }
// Advanced configuration // Advanced configuration
@@ -395,6 +378,7 @@ func collectUserInput(reader *bufio.Reader) Config {
fmt.Println("\n=== Advanced Configuration ===") fmt.Println("\n=== Advanced Configuration ===")
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true) config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
config.EnableGeoblocking = readBool(reader, "Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", false)
if config.DashboardDomain == "" { if config.DashboardDomain == "" {
fmt.Println("Error: Dashboard Domain name is required") fmt.Println("Error: Dashboard Domain name is required")
@@ -429,11 +413,6 @@ func createConfigFiles(config Config) error {
return nil return nil
} }
// the hybrid does not need the dynamic config
if config.HybridMode && strings.Contains(path, "dynamic_config.yml") {
return nil
}
// skip .DS_Store // skip .DS_Store
if strings.Contains(path, ".DS_Store") { if strings.Contains(path, ".DS_Store") {
return nil return nil
@@ -663,18 +642,30 @@ func checkPortsAvailable(port int) error {
return nil return nil
} }
func checkIsPangolinInstalledWithHybrid() bool { func downloadMaxMindDatabase() error {
// Check if config/config.yml exists and contains hybrid section fmt.Println("Downloading MaxMind GeoLite2 Country database...")
if _, err := os.Stat("config/config.yml"); err != nil {
return false // Download the GeoLite2 Country database
if err := run("curl", "-L", "-o", "GeoLite2-Country.tar.gz",
"https://github.com/GitSquared/node-geolite2-redist/raw/refs/heads/master/redist/GeoLite2-Country.tar.gz"); err != nil {
return fmt.Errorf("failed to download GeoLite2 database: %v", err)
} }
// Read config file to check for hybrid section // Extract the database
content, err := os.ReadFile("config/config.yml") if err := run("tar", "-xzf", "GeoLite2-Country.tar.gz"); err != nil {
if err != nil { return fmt.Errorf("failed to extract GeoLite2 database: %v", err)
return false
} }
// Check for hybrid section // Find the .mmdb file and move it to the config directory
return bytes.Contains(content, []byte("managed:")) if err := run("bash", "-c", "mv GeoLite2-Country_*/GeoLite2-Country.mmdb config/"); err != nil {
return fmt.Errorf("failed to move GeoLite2 database to config directory: %v", err)
}
// Clean up the downloaded files
if err := run("rm", "-rf", "GeoLite2-Country.tar.gz", "GeoLite2-Country_*"); err != nil {
fmt.Printf("Warning: failed to clean up temporary files: %v\n", err)
}
fmt.Println("MaxMind GeoLite2 Country database downloaded successfully!")
return nil
} }

View File

@@ -1,110 +0,0 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
const (
FRONTEND_SECRET_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e"
// CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start"
CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start"
)
// HybridCredentials represents the response from the cloud API
type HybridCredentials struct {
RemoteExitNodeId string `json:"remoteExitNodeId"`
Secret string `json:"secret"`
}
// APIResponse represents the full response structure from the cloud API
type APIResponse struct {
Data HybridCredentials `json:"data"`
}
// RequestPayload represents the request body structure
type RequestPayload struct {
Token string `json:"token"`
}
func generateValidationToken() string {
timestamp := time.Now().UnixMilli()
data := fmt.Sprintf("%s|%d", FRONTEND_SECRET_KEY, timestamp)
obfuscated := make([]byte, len(data))
for i, char := range []byte(data) {
obfuscated[i] = char + 5
}
return base64.StdEncoding.EncodeToString(obfuscated)
}
// requestHybridCredentials makes an HTTP POST request to the cloud API
// to get hybrid credentials (ID and secret)
func requestHybridCredentials() (*HybridCredentials, error) {
// Generate validation token
token := generateValidationToken()
// Create request payload
payload := RequestPayload{
Token: token,
}
// Marshal payload to JSON
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("failed to marshal request payload: %v", err)
}
// Create HTTP request
req, err := http.NewRequest("POST", CLOUD_API_URL, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create HTTP request: %v", err)
}
// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-CSRF-Token", "x-csrf-protection")
// Create HTTP client with timeout
client := &http.Client{
Timeout: 30 * time.Second,
}
// Make the request
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make HTTP request: %v", err)
}
defer resp.Body.Close()
// Check response status
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API request failed with status code: %d", resp.StatusCode)
}
// Read response body for debugging
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %v", err)
}
// Print the raw JSON response for debugging
// fmt.Printf("Raw JSON response: %s\n", string(body))
// Parse response
var apiResponse APIResponse
if err := json.Unmarshal(body, &apiResponse); err != nil {
return nil, fmt.Errorf("failed to decode API response: %v", err)
}
// Validate response data
if apiResponse.Data.RemoteExitNodeId == "" || apiResponse.Data.Secret == "" {
return nil, fmt.Errorf("invalid response: missing remoteExitNodeId or secret")
}
return &apiResponse.Data, nil
}

View File

@@ -96,7 +96,7 @@
"siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES", "siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES",
"siteWgDescriptionSaas": "Използвайте всеки WireGuard клиент за установяване на тунел. Ръчно нат задаване е необходимо. РАБОТИ САМО НА СОБСТВЕНИ УЗЛИ.", "siteWgDescriptionSaas": "Използвайте всеки WireGuard клиент за установяване на тунел. Ръчно нат задаване е необходимо. РАБОТИ САМО НА СОБСТВЕНИ УЗЛИ.",
"siteLocalDescription": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES", "siteLocalDescription": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES",
"siteLocalDescriptionSaas": "Само локални ресурси. Без тунелиране. РАБОТИ САМО НА СОБСТВЕНИ УЗЛИ.", "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
"siteSeeAll": "Вижте всички сайтове", "siteSeeAll": "Вижте всички сайтове",
"siteTunnelDescription": "Определете как искате да се свържете с вашия сайт", "siteTunnelDescription": "Определете как искате да се свържете с вашия сайт",
"siteNewtCredentials": "Newt Удостоверения", "siteNewtCredentials": "Newt Удостоверения",
@@ -468,7 +468,10 @@
"createdAt": "Създаден на", "createdAt": "Създаден на",
"proxyErrorInvalidHeader": "Невалидна стойност за заглавие на хоста. Използвайте формат на име на домейн, или оставете празно поле за да премахнете персонализирано заглавие на хост.", "proxyErrorInvalidHeader": "Невалидна стойност за заглавие на хоста. Използвайте формат на име на домейн, или оставете празно поле за да премахнете персонализирано заглавие на хост.",
"proxyErrorTls": "Невалидно име на TLS сървър. Използвайте формат на име на домейн, или оставете празно за да премахнете името на TLS сървъра.", "proxyErrorTls": "Невалидно име на TLS сървър. Използвайте формат на име на домейн, или оставете празно за да премахнете името на TLS сървъра.",
"proxyEnableSSL": "Активиране на SSL (https)", "proxyEnableSSL": "Активиране на SSL",
"proxyEnableSSLDescription": "Активиране на SSL/TLS криптиране за сигурни HTTPS връзки към вашите цели.",
"target": "Цел",
"configureTarget": "Конфигуриране на цели",
"targetErrorFetch": "Неуспешно извличане на цели", "targetErrorFetch": "Неуспешно извличане на цели",
"targetErrorFetchDescription": "Възникна грешка при извличане на целите", "targetErrorFetchDescription": "Възникна грешка при извличане на целите",
"siteErrorFetch": "Неуспешно извличане на ресурс", "siteErrorFetch": "Неуспешно извличане на ресурс",
@@ -495,7 +498,7 @@
"targetTlsSettings": "Конфигурация на защитена връзка", "targetTlsSettings": "Конфигурация на защитена връзка",
"targetTlsSettingsDescription": "Конфигурирайте SSL/TLS настройките за вашия ресурс", "targetTlsSettingsDescription": "Конфигурирайте SSL/TLS настройките за вашия ресурс",
"targetTlsSettingsAdvanced": "Разширени TLS настройки", "targetTlsSettingsAdvanced": "Разширени TLS настройки",
"targetTlsSni": "Име на TLS сървър (SNI)", "targetTlsSni": "Имя на TLS сървър",
"targetTlsSniDescription": "Името на TLS сървъра за използване за SNI. Оставете празно, за да използвате подразбиране.", "targetTlsSniDescription": "Името на TLS сървъра за използване за SNI. Оставете празно, за да използвате подразбиране.",
"targetTlsSubmit": "Запазване на настройките", "targetTlsSubmit": "Запазване на настройките",
"targets": "Конфигурация на целите", "targets": "Конфигурация на целите",
@@ -504,9 +507,21 @@
"targetStickySessionsDescription": "Запазване на връзките със същото задно целево място за цялата сесия.", "targetStickySessionsDescription": "Запазване на връзките със същото задно целево място за цялата сесия.",
"methodSelect": "Изберете метод", "methodSelect": "Изберете метод",
"targetSubmit": "Добавяне на цел", "targetSubmit": "Добавяне на цел",
"targetNoOne": "Няма цели. Добавете цел чрез формата.", "targetNoOne": "Този ресурс няма цели. Добавете цел, за да конфигурирате къде да изпращате заявки към вашия бекенд.",
"targetNoOneDescription": "Добавянето на повече от една цел ще активира натоварването на баланса.", "targetNoOneDescription": "Добавянето на повече от една цел ще активира натоварването на баланса.",
"targetsSubmit": "Запазване на целите", "targetsSubmit": "Запазване на целите",
"addTarget": "Добавете цел",
"targetErrorInvalidIp": "Невалиден IP адрес",
"targetErrorInvalidIpDescription": "Моля, въведете валиден IP адрес или име на хост",
"targetErrorInvalidPort": "Невалиден порт",
"targetErrorInvalidPortDescription": "Моля, въведете валиден номер на порт",
"targetErrorNoSite": "Няма избран сайт",
"targetErrorNoSiteDescription": "Моля, изберете сайт за целта",
"targetCreated": "Целта е създадена",
"targetCreatedDescription": "Целта беше успешно създадена",
"targetErrorCreate": "Неуспешно създаване на целта",
"targetErrorCreateDescription": "Възникна грешка при създаването на целта",
"save": "Запази",
"proxyAdditional": "Допълнителни настройки на прокси", "proxyAdditional": "Допълнителни настройки на прокси",
"proxyAdditionalDescription": "Конфигурирайте как вашият ресурс обработва прокси настройки", "proxyAdditionalDescription": "Конфигурирайте как вашият ресурс обработва прокси настройки",
"proxyCustomHeader": "Персонализиран хост заглавие", "proxyCustomHeader": "Персонализиран хост заглавие",
@@ -715,7 +730,7 @@
"pangolinServerAdmin": "Администратор на сървър - Панголин", "pangolinServerAdmin": "Администратор на сървър - Панголин",
"licenseTierProfessional": "Професионален лиценз", "licenseTierProfessional": "Професионален лиценз",
"licenseTierEnterprise": "Предприятие лиценз", "licenseTierEnterprise": "Предприятие лиценз",
"licenseTierCommercial": "Търговски лиценз", "licenseTierPersonal": "Personal License",
"licensed": "Лицензиран", "licensed": "Лицензиран",
"yes": "Да", "yes": "Да",
"no": "Не", "no": "Не",
@@ -750,7 +765,7 @@
"idpDisplayName": "Име за показване за този доставчик на идентичност", "idpDisplayName": "Име за показване за този доставчик на идентичност",
"idpAutoProvisionUsers": "Автоматично потребителско създаване", "idpAutoProvisionUsers": "Автоматично потребителско създаване",
"idpAutoProvisionUsersDescription": "Когато е активирано, потребителите ще бъдат автоматично създадени в системата при първо влизане с възможност за свързване на потребителите с роли и организации.", "idpAutoProvisionUsersDescription": "Когато е активирано, потребителите ще бъдат автоматично създадени в системата при първо влизане с възможност за свързване на потребителите с роли и организации.",
"licenseBadge": "Професионален", "licenseBadge": "EE",
"idpType": "Тип доставчик", "idpType": "Тип доставчик",
"idpTypeDescription": "Изберете типа доставчик на идентичност, който искате да конфигурирате", "idpTypeDescription": "Изберете типа доставчик на идентичност, който искате да конфигурирате",
"idpOidcConfigure": "Конфигурация на OAuth2/OIDC", "idpOidcConfigure": "Конфигурация на OAuth2/OIDC",
@@ -1084,7 +1099,6 @@
"navbar": "Навигационно меню", "navbar": "Навигационно меню",
"navbarDescription": "Главно навигационно меню за приложението", "navbarDescription": "Главно навигационно меню за приложението",
"navbarDocsLink": "Документация", "navbarDocsLink": "Документация",
"commercialEdition": "Търговско издание",
"otpErrorEnable": "Не може да се активира 2FA", "otpErrorEnable": "Не може да се активира 2FA",
"otpErrorEnableDescription": "Възникна грешка при активиране на 2FA", "otpErrorEnableDescription": "Възникна грешка при активиране на 2FA",
"otpSetupCheckCode": "Моля, въведете 6-цифрен код", "otpSetupCheckCode": "Моля, въведете 6-цифрен код",
@@ -1140,7 +1154,7 @@
"sidebarAllUsers": "Всички потребители", "sidebarAllUsers": "Всички потребители",
"sidebarIdentityProviders": "Идентификационни доставчици", "sidebarIdentityProviders": "Идентификационни доставчици",
"sidebarLicense": "Лиценз", "sidebarLicense": "Лиценз",
"sidebarClients": "Клиенти (Бета)", "sidebarClients": "Clients",
"sidebarDomains": "Домейни", "sidebarDomains": "Домейни",
"enableDockerSocket": "Активиране на Docker Чернова", "enableDockerSocket": "Активиране на Docker Чернова",
"enableDockerSocketDescription": "Активиране на Docker Socket маркировка за изтегляне на етикети на чернова. Пътят на гнездото трябва да бъде предоставен на Newt.", "enableDockerSocketDescription": "Активиране на Docker Socket маркировка за изтегляне на етикети на чернова. Пътят на гнездото трябва да бъде предоставен на Newt.",
@@ -1333,7 +1347,6 @@
"twoFactorRequired": "Двуфакторното удостоверяване е необходимо за регистрация на ключ за защита.", "twoFactorRequired": "Двуфакторното удостоверяване е необходимо за регистрация на ключ за защита.",
"twoFactor": "Двуфакторно удостоверяване", "twoFactor": "Двуфакторно удостоверяване",
"adminEnabled2FaOnYourAccount": "Вашият администратор е активирал двуфакторно удостоверяване за {email}. Моля, завършете процеса по настройка, за да продължите.", "adminEnabled2FaOnYourAccount": "Вашият администратор е активирал двуфакторно удостоверяване за {email}. Моля, завършете процеса по настройка, за да продължите.",
"continueToApplication": "Продължете към приложението",
"securityKeyAdd": "Добавяне на ключ за сигурност", "securityKeyAdd": "Добавяне на ключ за сигурност",
"securityKeyRegisterTitle": "Регистриране на нов ключ за сигурност", "securityKeyRegisterTitle": "Регистриране на нов ключ за сигурност",
"securityKeyRegisterDescription": "Свържете ключа за сигурност и въведете име, по което да го идентифицирате", "securityKeyRegisterDescription": "Свържете ключа за сигурност и въведете име, по което да го идентифицирате",
@@ -1411,6 +1424,7 @@
"externalProxyEnabled": "Външен прокси разрешен", "externalProxyEnabled": "Външен прокси разрешен",
"addNewTarget": "Добави нова цел", "addNewTarget": "Добави нова цел",
"targetsList": "Списък с цели", "targetsList": "Списък с цели",
"advancedMode": "Разширен режим",
"targetErrorDuplicateTargetFound": "Дублирана цел намерена", "targetErrorDuplicateTargetFound": "Дублирана цел намерена",
"healthCheckHealthy": "Здрав", "healthCheckHealthy": "Здрав",
"healthCheckUnhealthy": "Нездрав", "healthCheckUnhealthy": "Нездрав",
@@ -1543,8 +1557,8 @@
"autoLoginError": "Грешка при автоматично влизане", "autoLoginError": "Грешка при автоматично влизане",
"autoLoginErrorNoRedirectUrl": "Не е получен URL за пренасочване от доставчика на идентификационни данни.", "autoLoginErrorNoRedirectUrl": "Не е получен URL за пренасочване от доставчика на идентификационни данни.",
"autoLoginErrorGeneratingUrl": "Неуспешно генериране на URL за удостоверяване.", "autoLoginErrorGeneratingUrl": "Неуспешно генериране на URL за удостоверяване.",
"remoteExitNodeManageRemoteExitNodes": "Управление на самостоятелно хоствани", "remoteExitNodeManageRemoteExitNodes": "Отдалечени възли",
"remoteExitNodeDescription": "Управление на възли за разширяване на мрежовата ви свързаност", "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
"remoteExitNodes": "Възли", "remoteExitNodes": "Възли",
"searchRemoteExitNodes": "Търсене на възли...", "searchRemoteExitNodes": "Търсене на възли...",
"remoteExitNodeAdd": "Добавяне на възел", "remoteExitNodeAdd": "Добавяне на възел",
@@ -1554,7 +1568,7 @@
"remoteExitNodeMessageConfirm": "За потвърждение, моля въведете името на възела по-долу.", "remoteExitNodeMessageConfirm": "За потвърждение, моля въведете името на възела по-долу.",
"remoteExitNodeConfirmDelete": "Потвърдете изтриването на възела (\"Confirm Delete Site\" match)", "remoteExitNodeConfirmDelete": "Потвърдете изтриването на възела (\"Confirm Delete Site\" match)",
"remoteExitNodeDelete": "Изтрийте възела (\"Delete Site\" match)", "remoteExitNodeDelete": "Изтрийте възела (\"Delete Site\" match)",
"sidebarRemoteExitNodes": "Възли (\"Local\" match)", "sidebarRemoteExitNodes": "Отдалечени възли",
"remoteExitNodeCreate": { "remoteExitNodeCreate": {
"title": "Създаване на възел", "title": "Създаване на възел",
"description": "Създайте нов възел, за да разширите мрежовата си свързаност", "description": "Създайте нов възел, за да разширите мрежовата си свързаност",
@@ -1723,5 +1737,161 @@
"authPageUpdated": "Страницата за удостоверяване е актуализирана успешно", "authPageUpdated": "Страницата за удостоверяване е актуализирана успешно",
"healthCheckNotAvailable": "Локална", "healthCheckNotAvailable": "Локална",
"rewritePath": "Пренапиши път", "rewritePath": "Пренапиши път",
"rewritePathDescription": "По избор пренапиши пътя преди пренасочване към целта." "rewritePathDescription": "По избор пренапиши пътя преди пренасочване към целта.",
"continueToApplication": "Продължете до приложението",
"checkingInvite": "Проверка на поканата",
"setResourceHeaderAuth": "setResourceHeaderAuth",
"resourceHeaderAuthRemove": "Премахване на автентикация в заглавката",
"resourceHeaderAuthRemoveDescription": "Автентикацията в заглавката беше премахната успешно.",
"resourceErrorHeaderAuthRemove": "Неуспешно премахване на автентикация в заглавката",
"resourceErrorHeaderAuthRemoveDescription": "Не беше възможно премахването на автентикацията в заглавката за ресурса.",
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
"headerAuthRemove": "Remove Header Auth",
"headerAuthAdd": "Add Header Auth",
"resourceErrorHeaderAuthSetup": "Неуспешно задаване на автентикация в заглавката",
"resourceErrorHeaderAuthSetupDescription": "Не беше възможно задаването на автентикация в заглавката за ресурса.",
"resourceHeaderAuthSetup": "Автентикацията в заглавката беше зададена успешно",
"resourceHeaderAuthSetupDescription": "Автентикацията в заглавката беше успешно зададена.",
"resourceHeaderAuthSetupTitle": "Задаване на автентикация в заглавката",
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
"resourceHeaderAuthSubmit": "Задаване на автентикация в заглавката",
"actionSetResourceHeaderAuth": "Задаване на автентикация в заглавката",
"enterpriseEdition": "Enterprise Edition",
"unlicensed": "Unlicensed",
"beta": "Beta",
"manageClients": "Manage Clients",
"manageClientsDescription": "Clients are devices that can connect to your sites",
"licenseTableValidUntil": "Valid Until",
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
"sidebarEnterpriseLicenses": "Licenses",
"generateLicenseKey": "Generate License Key",
"generateLicenseKeyForm": {
"validation": {
"emailRequired": "Please enter a valid email address",
"useCaseTypeRequired": "Please select a use case type",
"firstNameRequired": "First name is required",
"lastNameRequired": "Last name is required",
"primaryUseRequired": "Please describe your primary use",
"jobTitleRequiredBusiness": "Job title is required for business use",
"industryRequiredBusiness": "Industry is required for business use",
"stateProvinceRegionRequired": "State/Province/Region is required",
"postalZipCodeRequired": "Postal/ZIP Code is required",
"companyNameRequiredBusiness": "Company name is required for business use",
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
"countryRequiredPersonal": "Country is required for personal use",
"agreeToTermsRequired": "You must agree to the terms",
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
},
"useCaseOptions": {
"personal": {
"title": "Personal Use",
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
},
"business": {
"title": "Business Use",
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
}
},
"steps": {
"emailLicenseType": {
"title": "Email & License Type",
"description": "Enter your email and choose your license type"
},
"personalInformation": {
"title": "Personal Information",
"description": "Tell us about yourself"
},
"contactInformation": {
"title": "Contact Information",
"description": "Your contact details"
},
"termsGenerate": {
"title": "Terms & Generate",
"description": "Review and accept terms to generate your license"
}
},
"alerts": {
"commercialUseDisclosure": {
"title": "Usage Disclosure",
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
},
"trialPeriodInformation": {
"title": "Trial Period Information",
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
}
},
"form": {
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
"firstName": "First Name",
"lastName": "Last Name",
"jobTitle": "Job Title",
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
"industryQuestion": "What is your industry?",
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
"companyName": "Company name",
"countryOfResidence": "Country of residence",
"stateProvinceRegion": "State / Province / Region",
"postalZipCode": "Postal / ZIP Code",
"companyWebsite": "Company website",
"companyPhoneNumber": "Company phone number",
"country": "Country",
"phoneNumberOptional": "Phone number (optional)",
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
},
"buttons": {
"close": "Close",
"previous": "Previous",
"next": "Next",
"generateLicenseKey": "Generate License Key"
},
"toasts": {
"success": {
"title": "License key generated successfully",
"description": "Your license key has been generated and is ready to use."
},
"error": {
"title": "Failed to generate license key",
"description": "An error occurred while generating the license key."
}
}
},
"priority": "Приоритет",
"priorityDescription": "По-високите приоритетни маршрути се оценяват първи. Приоритет = 100 означава автоматично подреждане (системата решава). Използвайте друго число, за да наложите ръчен приоритет.",
"instanceName": "Instance Name",
"pathMatchModalTitle": "Configure Path Matching",
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
"pathMatchType": "Match Type",
"pathMatchPrefix": "Prefix",
"pathMatchExact": "Exact",
"pathMatchRegex": "Regex",
"pathMatchValue": "Path Value",
"clear": "Clear",
"saveChanges": "Save Changes",
"pathMatchRegexPlaceholder": "^/api/.*",
"pathMatchDefaultPlaceholder": "/path",
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
"pathMatchExactHelp": "Example: /api matches only /api",
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
"pathRewriteModalTitle": "Configure Path Rewriting",
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
"pathRewriteType": "Rewrite Type",
"pathRewritePrefixOption": "Prefix - Replace prefix",
"pathRewriteExactOption": "Exact - Replace entire path",
"pathRewriteRegexOption": "Regex - Pattern replacement",
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
"pathRewriteValue": "Rewrite Value",
"pathRewriteRegexPlaceholder": "/new/$1",
"pathRewriteDefaultPlaceholder": "/new-path",
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
"pathRewritePrefix": "Prefix",
"pathRewriteExact": "Exact",
"pathRewriteRegex": "Regex",
"pathRewriteStrip": "Strip",
"pathRewriteStripLabel": "strip"
} }

View File

@@ -96,7 +96,7 @@
"siteWgDescription": "Použijte jakéhokoli klienta WireGuard abyste sestavili tunel. Vyžaduje se ruční nastavení NAT.", "siteWgDescription": "Použijte jakéhokoli klienta WireGuard abyste sestavili tunel. Vyžaduje se ruční nastavení NAT.",
"siteWgDescriptionSaas": "Použijte jakéhokoli klienta WireGuard abyste sestavili tunel. Vyžaduje se ruční nastavení NAT. FUNGUJE POUZE NA SELF-HOSTED SERVERECH", "siteWgDescriptionSaas": "Použijte jakéhokoli klienta WireGuard abyste sestavili tunel. Vyžaduje se ruční nastavení NAT. FUNGUJE POUZE NA SELF-HOSTED SERVERECH",
"siteLocalDescription": "Pouze lokální zdroje. Žádný tunel.", "siteLocalDescription": "Pouze lokální zdroje. Žádný tunel.",
"siteLocalDescriptionSaas": "Pouze lokální zdroje. Žádný tunel. FUNGUJE POUZE NA SELF-HOSTED SERVERECH", "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
"siteSeeAll": "Zobrazit všechny lokality", "siteSeeAll": "Zobrazit všechny lokality",
"siteTunnelDescription": "Určete jak se chcete připojit k vaší lokalitě", "siteTunnelDescription": "Určete jak se chcete připojit k vaší lokalitě",
"siteNewtCredentials": "Přihlašovací údaje Newt", "siteNewtCredentials": "Přihlašovací údaje Newt",
@@ -468,7 +468,10 @@
"createdAt": "Vytvořeno v", "createdAt": "Vytvořeno v",
"proxyErrorInvalidHeader": "Neplatná hodnota hlavičky hostitele. Použijte formát názvu domény, nebo uložte prázdné pro zrušení vlastního hlavičky hostitele.", "proxyErrorInvalidHeader": "Neplatná hodnota hlavičky hostitele. Použijte formát názvu domény, nebo uložte prázdné pro zrušení vlastního hlavičky hostitele.",
"proxyErrorTls": "Neplatné jméno TLS serveru. Použijte formát doménového jména nebo uložte prázdné pro odstranění názvu TLS serveru.", "proxyErrorTls": "Neplatné jméno TLS serveru. Použijte formát doménového jména nebo uložte prázdné pro odstranění názvu TLS serveru.",
"proxyEnableSSL": "Povolit SSL (https)", "proxyEnableSSL": "Povolit SSL",
"proxyEnableSSLDescription": "Povolit šifrování SSL/TLS pro zabezpečená HTTPS připojení k vašim cílům.",
"target": "Target",
"configureTarget": "Konfigurace cílů",
"targetErrorFetch": "Nepodařilo se načíst cíle", "targetErrorFetch": "Nepodařilo se načíst cíle",
"targetErrorFetchDescription": "Při načítání cílů došlo k chybě", "targetErrorFetchDescription": "Při načítání cílů došlo k chybě",
"siteErrorFetch": "Nepodařilo se načíst zdroj", "siteErrorFetch": "Nepodařilo se načíst zdroj",
@@ -495,7 +498,7 @@
"targetTlsSettings": "Nastavení bezpečného připojení", "targetTlsSettings": "Nastavení bezpečného připojení",
"targetTlsSettingsDescription": "Konfigurace nastavení SSL/TLS pro váš dokument", "targetTlsSettingsDescription": "Konfigurace nastavení SSL/TLS pro váš dokument",
"targetTlsSettingsAdvanced": "Pokročilé nastavení TLS", "targetTlsSettingsAdvanced": "Pokročilé nastavení TLS",
"targetTlsSni": "Název serveru TLS (SNI)", "targetTlsSni": "Název serveru TLS",
"targetTlsSniDescription": "Název serveru TLS pro použití v SNI. Ponechte prázdné pro použití výchozího nastavení.", "targetTlsSniDescription": "Název serveru TLS pro použití v SNI. Ponechte prázdné pro použití výchozího nastavení.",
"targetTlsSubmit": "Uložit nastavení", "targetTlsSubmit": "Uložit nastavení",
"targets": "Konfigurace cílů", "targets": "Konfigurace cílů",
@@ -504,9 +507,21 @@
"targetStickySessionsDescription": "Zachovat spojení na stejném cíli pro celou relaci.", "targetStickySessionsDescription": "Zachovat spojení na stejném cíli pro celou relaci.",
"methodSelect": "Vyberte metodu", "methodSelect": "Vyberte metodu",
"targetSubmit": "Add Target", "targetSubmit": "Add Target",
"targetNoOne": "Žádné cíle. Přidejte cíl pomocí formuláře.", "targetNoOne": "Tento zdroj nemá žádné cíle. Přidejte cíl pro konfiguraci kam poslat žádosti na vaši backend.",
"targetNoOneDescription": "Přidáním více než jednoho cíle se umožní vyvážení zatížení.", "targetNoOneDescription": "Přidáním více než jednoho cíle se umožní vyvážení zatížení.",
"targetsSubmit": "Uložit cíle", "targetsSubmit": "Uložit cíle",
"addTarget": "Add Target",
"targetErrorInvalidIp": "Neplatná IP adresa",
"targetErrorInvalidIpDescription": "Zadejte prosím platnou IP adresu nebo název hostitele",
"targetErrorInvalidPort": "Neplatný port",
"targetErrorInvalidPortDescription": "Zadejte platné číslo portu",
"targetErrorNoSite": "Není vybrán žádný web",
"targetErrorNoSiteDescription": "Vyberte prosím web pro cíl",
"targetCreated": "Cíl byl vytvořen",
"targetCreatedDescription": "Cíl byl úspěšně vytvořen",
"targetErrorCreate": "Nepodařilo se vytvořit cíl",
"targetErrorCreateDescription": "Došlo k chybě při vytváření cíle",
"save": "Uložit",
"proxyAdditional": "Další nastavení proxy", "proxyAdditional": "Další nastavení proxy",
"proxyAdditionalDescription": "Konfigurovat nastavení proxy zpracování vašeho zdroje", "proxyAdditionalDescription": "Konfigurovat nastavení proxy zpracování vašeho zdroje",
"proxyCustomHeader": "Vlastní hlavička hostitele", "proxyCustomHeader": "Vlastní hlavička hostitele",
@@ -715,7 +730,7 @@
"pangolinServerAdmin": "Správce serveru - Pangolin", "pangolinServerAdmin": "Správce serveru - Pangolin",
"licenseTierProfessional": "Profesionální licence", "licenseTierProfessional": "Profesionální licence",
"licenseTierEnterprise": "Podniková licence", "licenseTierEnterprise": "Podniková licence",
"licenseTierCommercial": "Obchodní licence", "licenseTierPersonal": "Personal License",
"licensed": "Licencováno", "licensed": "Licencováno",
"yes": "Ano", "yes": "Ano",
"no": "Ne", "no": "Ne",
@@ -750,7 +765,7 @@
"idpDisplayName": "Zobrazované jméno tohoto poskytovatele identity", "idpDisplayName": "Zobrazované jméno tohoto poskytovatele identity",
"idpAutoProvisionUsers": "Automatická úprava uživatelů", "idpAutoProvisionUsers": "Automatická úprava uživatelů",
"idpAutoProvisionUsersDescription": "Pokud je povoleno, uživatelé budou automaticky vytvářeni v systému při prvním přihlášení, s možností namapovat uživatele na role a organizace.", "idpAutoProvisionUsersDescription": "Pokud je povoleno, uživatelé budou automaticky vytvářeni v systému při prvním přihlášení, s možností namapovat uživatele na role a organizace.",
"licenseBadge": "Profesionální", "licenseBadge": "EE",
"idpType": "Typ poskytovatele", "idpType": "Typ poskytovatele",
"idpTypeDescription": "Vyberte typ poskytovatele identity, který chcete nakonfigurovat", "idpTypeDescription": "Vyberte typ poskytovatele identity, který chcete nakonfigurovat",
"idpOidcConfigure": "Nastavení OAuth2/OIDC", "idpOidcConfigure": "Nastavení OAuth2/OIDC",
@@ -1084,7 +1099,6 @@
"navbar": "Navigation Menu", "navbar": "Navigation Menu",
"navbarDescription": "Hlavní navigační menu aplikace", "navbarDescription": "Hlavní navigační menu aplikace",
"navbarDocsLink": "Dokumentace", "navbarDocsLink": "Dokumentace",
"commercialEdition": "Obchodní vydání",
"otpErrorEnable": "2FA nelze povolit", "otpErrorEnable": "2FA nelze povolit",
"otpErrorEnableDescription": "Došlo k chybě při povolování 2FA", "otpErrorEnableDescription": "Došlo k chybě při povolování 2FA",
"otpSetupCheckCode": "Zadejte 6místný kód", "otpSetupCheckCode": "Zadejte 6místný kód",
@@ -1140,7 +1154,7 @@
"sidebarAllUsers": "Všichni uživatelé", "sidebarAllUsers": "Všichni uživatelé",
"sidebarIdentityProviders": "Poskytovatelé identity", "sidebarIdentityProviders": "Poskytovatelé identity",
"sidebarLicense": "Licence", "sidebarLicense": "Licence",
"sidebarClients": "Klienti (Beta)", "sidebarClients": "Clients",
"sidebarDomains": "Domény", "sidebarDomains": "Domény",
"enableDockerSocket": "Povolit Docker plán", "enableDockerSocket": "Povolit Docker plán",
"enableDockerSocketDescription": "Povolte seškrábání štítků na Docker Socket pro popisky plánů. Nová cesta musí být k dispozici.", "enableDockerSocketDescription": "Povolte seškrábání štítků na Docker Socket pro popisky plánů. Nová cesta musí být k dispozici.",
@@ -1333,7 +1347,6 @@
"twoFactorRequired": "Pro registraci bezpečnostního klíče je nutné dvoufaktorové ověření.", "twoFactorRequired": "Pro registraci bezpečnostního klíče je nutné dvoufaktorové ověření.",
"twoFactor": "Dvoufaktorové ověření", "twoFactor": "Dvoufaktorové ověření",
"adminEnabled2FaOnYourAccount": "Váš správce povolil dvoufaktorové ověřování pro {email}. Chcete-li pokračovat, dokončete proces nastavení.", "adminEnabled2FaOnYourAccount": "Váš správce povolil dvoufaktorové ověřování pro {email}. Chcete-li pokračovat, dokončete proces nastavení.",
"continueToApplication": "Pokračovat v aplikaci",
"securityKeyAdd": "Přidat bezpečnostní klíč", "securityKeyAdd": "Přidat bezpečnostní klíč",
"securityKeyRegisterTitle": "Registrovat nový bezpečnostní klíč", "securityKeyRegisterTitle": "Registrovat nový bezpečnostní klíč",
"securityKeyRegisterDescription": "Připojte svůj bezpečnostní klíč a zadejte jméno pro jeho identifikaci", "securityKeyRegisterDescription": "Připojte svůj bezpečnostní klíč a zadejte jméno pro jeho identifikaci",
@@ -1411,6 +1424,7 @@
"externalProxyEnabled": "Externí proxy povolen", "externalProxyEnabled": "Externí proxy povolen",
"addNewTarget": "Add New Target", "addNewTarget": "Add New Target",
"targetsList": "Seznam cílů", "targetsList": "Seznam cílů",
"advancedMode": "Pokročilý režim",
"targetErrorDuplicateTargetFound": "Byl nalezen duplicitní cíl", "targetErrorDuplicateTargetFound": "Byl nalezen duplicitní cíl",
"healthCheckHealthy": "Zdravé", "healthCheckHealthy": "Zdravé",
"healthCheckUnhealthy": "Nezdravé", "healthCheckUnhealthy": "Nezdravé",
@@ -1543,8 +1557,8 @@
"autoLoginError": "Automatická chyba přihlášení", "autoLoginError": "Automatická chyba přihlášení",
"autoLoginErrorNoRedirectUrl": "Od poskytovatele identity nebyla obdržena žádná adresa URL.", "autoLoginErrorNoRedirectUrl": "Od poskytovatele identity nebyla obdržena žádná adresa URL.",
"autoLoginErrorGeneratingUrl": "Nepodařilo se vygenerovat ověřovací URL.", "autoLoginErrorGeneratingUrl": "Nepodařilo se vygenerovat ověřovací URL.",
"remoteExitNodeManageRemoteExitNodes": "Spravovat vlastní hostování", "remoteExitNodeManageRemoteExitNodes": "Vzdálené uzly",
"remoteExitNodeDescription": "Spravujte uzly pro rozšíření připojení k síti", "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
"remoteExitNodes": "Uzly", "remoteExitNodes": "Uzly",
"searchRemoteExitNodes": "Hledat uzly...", "searchRemoteExitNodes": "Hledat uzly...",
"remoteExitNodeAdd": "Přidat uzel", "remoteExitNodeAdd": "Přidat uzel",
@@ -1554,7 +1568,7 @@
"remoteExitNodeMessageConfirm": "Pro potvrzení zadejte název uzlu níže.", "remoteExitNodeMessageConfirm": "Pro potvrzení zadejte název uzlu níže.",
"remoteExitNodeConfirmDelete": "Potvrdit odstranění uzlu", "remoteExitNodeConfirmDelete": "Potvrdit odstranění uzlu",
"remoteExitNodeDelete": "Odstranit uzel", "remoteExitNodeDelete": "Odstranit uzel",
"sidebarRemoteExitNodes": "Uzly", "sidebarRemoteExitNodes": "Vzdálené uzly",
"remoteExitNodeCreate": { "remoteExitNodeCreate": {
"title": "Vytvořit uzel", "title": "Vytvořit uzel",
"description": "Vytvořit nový uzel pro rozšíření síťového připojení", "description": "Vytvořit nový uzel pro rozšíření síťového připojení",
@@ -1723,5 +1737,161 @@
"authPageUpdated": "Autentizační stránka byla úspěšně aktualizována", "authPageUpdated": "Autentizační stránka byla úspěšně aktualizována",
"healthCheckNotAvailable": "Místní", "healthCheckNotAvailable": "Místní",
"rewritePath": "Přepsat cestu", "rewritePath": "Přepsat cestu",
"rewritePathDescription": "Volitelně přepište cestu před odesláním na cíl." "rewritePathDescription": "Volitelně přepište cestu před odesláním na cíl.",
"continueToApplication": "Pokračovat v aplikaci",
"checkingInvite": "Kontrola pozvánky",
"setResourceHeaderAuth": "setResourceHeaderAuth",
"resourceHeaderAuthRemove": "Odstranit Autentizaci Záhlaví",
"resourceHeaderAuthRemoveDescription": "Úspěšně odstraněna autentizace záhlaví.",
"resourceErrorHeaderAuthRemove": "Nepodařilo se odstranit Autentizaci Záhlaví",
"resourceErrorHeaderAuthRemoveDescription": "Nepodařilo se odstranit autentizaci záhlaví ze zdroje.",
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
"headerAuthRemove": "Remove Header Auth",
"headerAuthAdd": "Add Header Auth",
"resourceErrorHeaderAuthSetup": "Nepodařilo se nastavit Autentizaci Záhlaví",
"resourceErrorHeaderAuthSetupDescription": "Nepodařilo se nastavit autentizaci záhlaví ze zdroje.",
"resourceHeaderAuthSetup": "Úspěšně nastavena Autentizace Záhlaví",
"resourceHeaderAuthSetupDescription": "Autentizace záhlaví byla úspěšně nastavena.",
"resourceHeaderAuthSetupTitle": "Nastavit Autentizaci Záhlaví",
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
"resourceHeaderAuthSubmit": "Nastavit Autentizaci Záhlaví",
"actionSetResourceHeaderAuth": "Nastavit Autentizaci Záhlaví",
"enterpriseEdition": "Enterprise Edition",
"unlicensed": "Unlicensed",
"beta": "Beta",
"manageClients": "Manage Clients",
"manageClientsDescription": "Clients are devices that can connect to your sites",
"licenseTableValidUntil": "Valid Until",
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
"sidebarEnterpriseLicenses": "Licenses",
"generateLicenseKey": "Generate License Key",
"generateLicenseKeyForm": {
"validation": {
"emailRequired": "Please enter a valid email address",
"useCaseTypeRequired": "Please select a use case type",
"firstNameRequired": "First name is required",
"lastNameRequired": "Last name is required",
"primaryUseRequired": "Please describe your primary use",
"jobTitleRequiredBusiness": "Job title is required for business use",
"industryRequiredBusiness": "Industry is required for business use",
"stateProvinceRegionRequired": "State/Province/Region is required",
"postalZipCodeRequired": "Postal/ZIP Code is required",
"companyNameRequiredBusiness": "Company name is required for business use",
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
"countryRequiredPersonal": "Country is required for personal use",
"agreeToTermsRequired": "You must agree to the terms",
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
},
"useCaseOptions": {
"personal": {
"title": "Personal Use",
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
},
"business": {
"title": "Business Use",
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
}
},
"steps": {
"emailLicenseType": {
"title": "Email & License Type",
"description": "Enter your email and choose your license type"
},
"personalInformation": {
"title": "Personal Information",
"description": "Tell us about yourself"
},
"contactInformation": {
"title": "Contact Information",
"description": "Your contact details"
},
"termsGenerate": {
"title": "Terms & Generate",
"description": "Review and accept terms to generate your license"
}
},
"alerts": {
"commercialUseDisclosure": {
"title": "Usage Disclosure",
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
},
"trialPeriodInformation": {
"title": "Trial Period Information",
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
}
},
"form": {
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
"firstName": "First Name",
"lastName": "Last Name",
"jobTitle": "Job Title",
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
"industryQuestion": "What is your industry?",
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
"companyName": "Company name",
"countryOfResidence": "Country of residence",
"stateProvinceRegion": "State / Province / Region",
"postalZipCode": "Postal / ZIP Code",
"companyWebsite": "Company website",
"companyPhoneNumber": "Company phone number",
"country": "Country",
"phoneNumberOptional": "Phone number (optional)",
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
},
"buttons": {
"close": "Close",
"previous": "Previous",
"next": "Next",
"generateLicenseKey": "Generate License Key"
},
"toasts": {
"success": {
"title": "License key generated successfully",
"description": "Your license key has been generated and is ready to use."
},
"error": {
"title": "Failed to generate license key",
"description": "An error occurred while generating the license key."
}
}
},
"priority": "Priorita",
"priorityDescription": "Vyšší priorita je vyhodnocena jako první. Priorita = 100 znamená automatické řazení (rozhodnutí systému). Pro vynucení manuální priority použijte jiné číslo.",
"instanceName": "Instance Name",
"pathMatchModalTitle": "Configure Path Matching",
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
"pathMatchType": "Match Type",
"pathMatchPrefix": "Prefix",
"pathMatchExact": "Exact",
"pathMatchRegex": "Regex",
"pathMatchValue": "Path Value",
"clear": "Clear",
"saveChanges": "Save Changes",
"pathMatchRegexPlaceholder": "^/api/.*",
"pathMatchDefaultPlaceholder": "/path",
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
"pathMatchExactHelp": "Example: /api matches only /api",
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
"pathRewriteModalTitle": "Configure Path Rewriting",
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
"pathRewriteType": "Rewrite Type",
"pathRewritePrefixOption": "Prefix - Replace prefix",
"pathRewriteExactOption": "Exact - Replace entire path",
"pathRewriteRegexOption": "Regex - Pattern replacement",
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
"pathRewriteValue": "Rewrite Value",
"pathRewriteRegexPlaceholder": "/new/$1",
"pathRewriteDefaultPlaceholder": "/new-path",
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
"pathRewritePrefix": "Prefix",
"pathRewriteExact": "Exact",
"pathRewriteRegex": "Regex",
"pathRewriteStrip": "Strip",
"pathRewriteStripLabel": "strip"
} }

View File

@@ -96,7 +96,7 @@
"siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.", "siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.",
"siteWgDescriptionSaas": "Verwenden Sie jeden WireGuard-Client, um einen Tunnel zu erstellen. Manuelles NAT-Setup erforderlich. FUNKTIONIERT NUR BEI SELBSTGEHOSTETEN KNOTEN", "siteWgDescriptionSaas": "Verwenden Sie jeden WireGuard-Client, um einen Tunnel zu erstellen. Manuelles NAT-Setup erforderlich. FUNKTIONIERT NUR BEI SELBSTGEHOSTETEN KNOTEN",
"siteLocalDescription": "Nur lokale Ressourcen. Kein Tunneling.", "siteLocalDescription": "Nur lokale Ressourcen. Kein Tunneling.",
"siteLocalDescriptionSaas": "Nur lokale Ressourcen. Keine Tunneldurchführung. FUNKTIONIERT NUR BEI SELBSTGEHOSTETEN KNOTEN", "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
"siteSeeAll": "Alle Standorte anzeigen", "siteSeeAll": "Alle Standorte anzeigen",
"siteTunnelDescription": "Lege fest, wie du dich mit deinem Standort verbinden möchtest", "siteTunnelDescription": "Lege fest, wie du dich mit deinem Standort verbinden möchtest",
"siteNewtCredentials": "Neue Newt Zugangsdaten", "siteNewtCredentials": "Neue Newt Zugangsdaten",
@@ -468,7 +468,10 @@
"createdAt": "Erstellt am", "createdAt": "Erstellt am",
"proxyErrorInvalidHeader": "Ungültiger benutzerdefinierter Host-Header-Wert. Verwenden Sie das Domänennamensformat oder speichern Sie leer, um den benutzerdefinierten Host-Header zu deaktivieren.", "proxyErrorInvalidHeader": "Ungültiger benutzerdefinierter Host-Header-Wert. Verwenden Sie das Domänennamensformat oder speichern Sie leer, um den benutzerdefinierten Host-Header zu deaktivieren.",
"proxyErrorTls": "Ungültiger TLS-Servername. Verwenden Sie das Domänennamensformat oder speichern Sie leer, um den TLS-Servernamen zu entfernen.", "proxyErrorTls": "Ungültiger TLS-Servername. Verwenden Sie das Domänennamensformat oder speichern Sie leer, um den TLS-Servernamen zu entfernen.",
"proxyEnableSSL": "SSL aktivieren (https)", "proxyEnableSSL": "SSL aktivieren",
"proxyEnableSSLDescription": "Aktiviere SSL/TLS-Verschlüsselung für sichere HTTPS-Verbindungen zu deinen Zielen.",
"target": "Target",
"configureTarget": "Ziele konfigurieren",
"targetErrorFetch": "Fehler beim Abrufen der Ziele", "targetErrorFetch": "Fehler beim Abrufen der Ziele",
"targetErrorFetchDescription": "Beim Abrufen der Ziele ist ein Fehler aufgetreten", "targetErrorFetchDescription": "Beim Abrufen der Ziele ist ein Fehler aufgetreten",
"siteErrorFetch": "Fehler beim Abrufen der Ressource", "siteErrorFetch": "Fehler beim Abrufen der Ressource",
@@ -495,7 +498,7 @@
"targetTlsSettings": "Sicherheitskonfiguration", "targetTlsSettings": "Sicherheitskonfiguration",
"targetTlsSettingsDescription": "Konfiguriere SSL/TLS Einstellungen für deine Ressource", "targetTlsSettingsDescription": "Konfiguriere SSL/TLS Einstellungen für deine Ressource",
"targetTlsSettingsAdvanced": "Erweiterte TLS-Einstellungen", "targetTlsSettingsAdvanced": "Erweiterte TLS-Einstellungen",
"targetTlsSni": "TLS-Servername (SNI)", "targetTlsSni": "TLS Servername",
"targetTlsSniDescription": "Der zu verwendende TLS-Servername für SNI. Leer lassen, um den Standard zu verwenden.", "targetTlsSniDescription": "Der zu verwendende TLS-Servername für SNI. Leer lassen, um den Standard zu verwenden.",
"targetTlsSubmit": "Einstellungen speichern", "targetTlsSubmit": "Einstellungen speichern",
"targets": "Ziel-Konfiguration", "targets": "Ziel-Konfiguration",
@@ -504,9 +507,21 @@
"targetStickySessionsDescription": "Verbindungen für die gesamte Sitzung auf demselben Backend-Ziel halten.", "targetStickySessionsDescription": "Verbindungen für die gesamte Sitzung auf demselben Backend-Ziel halten.",
"methodSelect": "Methode auswählen", "methodSelect": "Methode auswählen",
"targetSubmit": "Ziel hinzufügen", "targetSubmit": "Ziel hinzufügen",
"targetNoOne": "Keine Ziele. Fügen Sie ein Ziel über das Formular hinzu.", "targetNoOne": "Diese Ressource hat keine Ziele. Fügen Sie ein Ziel hinzu, um zu konfigurieren, wo Anfragen an Ihr Backend gesendet werden sollen.",
"targetNoOneDescription": "Das Hinzufügen von mehr als einem Ziel aktiviert den Lastausgleich.", "targetNoOneDescription": "Das Hinzufügen von mehr als einem Ziel aktiviert den Lastausgleich.",
"targetsSubmit": "Ziele speichern", "targetsSubmit": "Ziele speichern",
"addTarget": "Ziel hinzufügen",
"targetErrorInvalidIp": "Ungültige IP-Adresse",
"targetErrorInvalidIpDescription": "Bitte geben Sie eine gültige IP-Adresse oder einen Hostnamen ein",
"targetErrorInvalidPort": "Ungültiger Port",
"targetErrorInvalidPortDescription": "Bitte geben Sie eine gültige Portnummer ein",
"targetErrorNoSite": "Keine Site ausgewählt",
"targetErrorNoSiteDescription": "Bitte wähle eine Seite für das Ziel aus",
"targetCreated": "Ziel erstellt",
"targetCreatedDescription": "Ziel wurde erfolgreich erstellt",
"targetErrorCreate": "Fehler beim Erstellen des Ziels",
"targetErrorCreateDescription": "Beim Erstellen des Ziels ist ein Fehler aufgetreten",
"save": "Speichern",
"proxyAdditional": "Zusätzliche Proxy-Einstellungen", "proxyAdditional": "Zusätzliche Proxy-Einstellungen",
"proxyAdditionalDescription": "Konfigurieren Sie, wie Ihre Ressource mit Proxy-Einstellungen umgeht", "proxyAdditionalDescription": "Konfigurieren Sie, wie Ihre Ressource mit Proxy-Einstellungen umgeht",
"proxyCustomHeader": "Benutzerdefinierter Host-Header", "proxyCustomHeader": "Benutzerdefinierter Host-Header",
@@ -715,7 +730,7 @@
"pangolinServerAdmin": "Server-Admin - Pangolin", "pangolinServerAdmin": "Server-Admin - Pangolin",
"licenseTierProfessional": "Professional Lizenz", "licenseTierProfessional": "Professional Lizenz",
"licenseTierEnterprise": "Enterprise Lizenz", "licenseTierEnterprise": "Enterprise Lizenz",
"licenseTierCommercial": "Gewerbliche Lizenz", "licenseTierPersonal": "Personal License",
"licensed": "Lizenziert", "licensed": "Lizenziert",
"yes": "Ja", "yes": "Ja",
"no": "Nein", "no": "Nein",
@@ -750,7 +765,7 @@
"idpDisplayName": "Ein Anzeigename für diesen Identitätsanbieter", "idpDisplayName": "Ein Anzeigename für diesen Identitätsanbieter",
"idpAutoProvisionUsers": "Automatische Benutzerbereitstellung", "idpAutoProvisionUsers": "Automatische Benutzerbereitstellung",
"idpAutoProvisionUsersDescription": "Wenn aktiviert, werden Benutzer beim ersten Login automatisch im System erstellt, mit der Möglichkeit, Benutzer Rollen und Organisationen zuzuordnen.", "idpAutoProvisionUsersDescription": "Wenn aktiviert, werden Benutzer beim ersten Login automatisch im System erstellt, mit der Möglichkeit, Benutzer Rollen und Organisationen zuzuordnen.",
"licenseBadge": "Profi", "licenseBadge": "EE",
"idpType": "Anbietertyp", "idpType": "Anbietertyp",
"idpTypeDescription": "Wählen Sie den Typ des Identitätsanbieters, den Sie konfigurieren möchten", "idpTypeDescription": "Wählen Sie den Typ des Identitätsanbieters, den Sie konfigurieren möchten",
"idpOidcConfigure": "OAuth2/OIDC Konfiguration", "idpOidcConfigure": "OAuth2/OIDC Konfiguration",
@@ -1084,7 +1099,6 @@
"navbar": "Navigationsmenü", "navbar": "Navigationsmenü",
"navbarDescription": "Hauptnavigationsmenü für die Anwendung", "navbarDescription": "Hauptnavigationsmenü für die Anwendung",
"navbarDocsLink": "Dokumentation", "navbarDocsLink": "Dokumentation",
"commercialEdition": "Kommerzielle Edition",
"otpErrorEnable": "2FA konnte nicht aktiviert werden", "otpErrorEnable": "2FA konnte nicht aktiviert werden",
"otpErrorEnableDescription": "Beim Aktivieren der 2FA ist ein Fehler aufgetreten", "otpErrorEnableDescription": "Beim Aktivieren der 2FA ist ein Fehler aufgetreten",
"otpSetupCheckCode": "Bitte geben Sie einen 6-stelligen Code ein", "otpSetupCheckCode": "Bitte geben Sie einen 6-stelligen Code ein",
@@ -1140,7 +1154,7 @@
"sidebarAllUsers": "Alle Benutzer", "sidebarAllUsers": "Alle Benutzer",
"sidebarIdentityProviders": "Identitätsanbieter", "sidebarIdentityProviders": "Identitätsanbieter",
"sidebarLicense": "Lizenz", "sidebarLicense": "Lizenz",
"sidebarClients": "Kunden (Beta)", "sidebarClients": "Clients",
"sidebarDomains": "Domänen", "sidebarDomains": "Domänen",
"enableDockerSocket": "Docker Blaupause aktivieren", "enableDockerSocket": "Docker Blaupause aktivieren",
"enableDockerSocketDescription": "Aktiviere Docker-Socket-Label-Scraping für Blaupausenbeschriftungen. Der Socket-Pfad muss neu angegeben werden.", "enableDockerSocketDescription": "Aktiviere Docker-Socket-Label-Scraping für Blaupausenbeschriftungen. Der Socket-Pfad muss neu angegeben werden.",
@@ -1333,7 +1347,6 @@
"twoFactorRequired": "Zur Registrierung eines Sicherheitsschlüssels ist eine Zwei-Faktor-Authentifizierung erforderlich.", "twoFactorRequired": "Zur Registrierung eines Sicherheitsschlüssels ist eine Zwei-Faktor-Authentifizierung erforderlich.",
"twoFactor": "Zwei-Faktor-Authentifizierung", "twoFactor": "Zwei-Faktor-Authentifizierung",
"adminEnabled2FaOnYourAccount": "Ihr Administrator hat die Zwei-Faktor-Authentifizierung für {email} aktiviert. Bitte schließen Sie den Einrichtungsprozess ab, um fortzufahren.", "adminEnabled2FaOnYourAccount": "Ihr Administrator hat die Zwei-Faktor-Authentifizierung für {email} aktiviert. Bitte schließen Sie den Einrichtungsprozess ab, um fortzufahren.",
"continueToApplication": "Weiter zur Anwendung",
"securityKeyAdd": "Sicherheitsschlüssel hinzufügen", "securityKeyAdd": "Sicherheitsschlüssel hinzufügen",
"securityKeyRegisterTitle": "Neuen Sicherheitsschlüssel registrieren", "securityKeyRegisterTitle": "Neuen Sicherheitsschlüssel registrieren",
"securityKeyRegisterDescription": "Verbinden Sie Ihren Sicherheitsschlüssel und geben Sie einen Namen ein, um ihn zu identifizieren", "securityKeyRegisterDescription": "Verbinden Sie Ihren Sicherheitsschlüssel und geben Sie einen Namen ein, um ihn zu identifizieren",
@@ -1411,6 +1424,7 @@
"externalProxyEnabled": "Externer Proxy aktiviert", "externalProxyEnabled": "Externer Proxy aktiviert",
"addNewTarget": "Neues Ziel hinzufügen", "addNewTarget": "Neues Ziel hinzufügen",
"targetsList": "Ziel-Liste", "targetsList": "Ziel-Liste",
"advancedMode": "Erweiterter Modus",
"targetErrorDuplicateTargetFound": "Doppeltes Ziel gefunden", "targetErrorDuplicateTargetFound": "Doppeltes Ziel gefunden",
"healthCheckHealthy": "Gesund", "healthCheckHealthy": "Gesund",
"healthCheckUnhealthy": "Ungesund", "healthCheckUnhealthy": "Ungesund",
@@ -1543,8 +1557,8 @@
"autoLoginError": "Fehler bei der automatischen Anmeldung", "autoLoginError": "Fehler bei der automatischen Anmeldung",
"autoLoginErrorNoRedirectUrl": "Keine Weiterleitungs-URL vom Identitätsanbieter erhalten.", "autoLoginErrorNoRedirectUrl": "Keine Weiterleitungs-URL vom Identitätsanbieter erhalten.",
"autoLoginErrorGeneratingUrl": "Fehler beim Generieren der Authentifizierungs-URL.", "autoLoginErrorGeneratingUrl": "Fehler beim Generieren der Authentifizierungs-URL.",
"remoteExitNodeManageRemoteExitNodes": "Selbst-Hosted verwalten", "remoteExitNodeManageRemoteExitNodes": "Entfernte Knoten",
"remoteExitNodeDescription": "Knoten verwalten, um die Netzwerkverbindung zu erweitern", "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
"remoteExitNodes": "Knoten", "remoteExitNodes": "Knoten",
"searchRemoteExitNodes": "Knoten suchen...", "searchRemoteExitNodes": "Knoten suchen...",
"remoteExitNodeAdd": "Knoten hinzufügen", "remoteExitNodeAdd": "Knoten hinzufügen",
@@ -1554,7 +1568,7 @@
"remoteExitNodeMessageConfirm": "Um zu bestätigen, geben Sie bitte den Namen des Knotens unten ein.", "remoteExitNodeMessageConfirm": "Um zu bestätigen, geben Sie bitte den Namen des Knotens unten ein.",
"remoteExitNodeConfirmDelete": "Löschknoten bestätigen", "remoteExitNodeConfirmDelete": "Löschknoten bestätigen",
"remoteExitNodeDelete": "Knoten löschen", "remoteExitNodeDelete": "Knoten löschen",
"sidebarRemoteExitNodes": "Knoten", "sidebarRemoteExitNodes": "Entfernte Knoten",
"remoteExitNodeCreate": { "remoteExitNodeCreate": {
"title": "Knoten erstellen", "title": "Knoten erstellen",
"description": "Erstellen Sie einen neuen Knoten, um Ihre Netzwerkverbindung zu erweitern", "description": "Erstellen Sie einen neuen Knoten, um Ihre Netzwerkverbindung zu erweitern",
@@ -1723,5 +1737,161 @@
"authPageUpdated": "Auth-Seite erfolgreich aktualisiert", "authPageUpdated": "Auth-Seite erfolgreich aktualisiert",
"healthCheckNotAvailable": "Lokal", "healthCheckNotAvailable": "Lokal",
"rewritePath": "Pfad neu schreiben", "rewritePath": "Pfad neu schreiben",
"rewritePathDescription": "Optional den Pfad umschreiben, bevor er an das Ziel weitergeleitet wird." "rewritePathDescription": "Optional den Pfad umschreiben, bevor er an das Ziel weitergeleitet wird.",
"continueToApplication": "Weiter zur Anwendung",
"checkingInvite": "Einladung wird überprüft",
"setResourceHeaderAuth": "setResourceHeaderAuth",
"resourceHeaderAuthRemove": "Header-Auth entfernen",
"resourceHeaderAuthRemoveDescription": "Header-Authentifizierung erfolgreich entfernt.",
"resourceErrorHeaderAuthRemove": "Fehler beim Entfernen der Header-Authentifizierung",
"resourceErrorHeaderAuthRemoveDescription": "Die Headerauthentifizierung für die Ressource konnte nicht entfernt werden.",
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
"headerAuthRemove": "Remove Header Auth",
"headerAuthAdd": "Add Header Auth",
"resourceErrorHeaderAuthSetup": "Fehler beim Setzen der Header-Authentifizierung",
"resourceErrorHeaderAuthSetupDescription": "Konnte Header-Authentifizierung für die Ressource nicht festlegen.",
"resourceHeaderAuthSetup": "Header-Authentifizierung erfolgreich festgelegt",
"resourceHeaderAuthSetupDescription": "Header-Authentifizierung wurde erfolgreich festgelegt.",
"resourceHeaderAuthSetupTitle": "Header-Authentifizierung festlegen",
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
"resourceHeaderAuthSubmit": "Header-Authentifizierung festlegen",
"actionSetResourceHeaderAuth": "Header-Authentifizierung festlegen",
"enterpriseEdition": "Enterprise Edition",
"unlicensed": "Unlicensed",
"beta": "Beta",
"manageClients": "Manage Clients",
"manageClientsDescription": "Clients are devices that can connect to your sites",
"licenseTableValidUntil": "Valid Until",
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
"sidebarEnterpriseLicenses": "Licenses",
"generateLicenseKey": "Generate License Key",
"generateLicenseKeyForm": {
"validation": {
"emailRequired": "Please enter a valid email address",
"useCaseTypeRequired": "Please select a use case type",
"firstNameRequired": "First name is required",
"lastNameRequired": "Last name is required",
"primaryUseRequired": "Please describe your primary use",
"jobTitleRequiredBusiness": "Job title is required for business use",
"industryRequiredBusiness": "Industry is required for business use",
"stateProvinceRegionRequired": "State/Province/Region is required",
"postalZipCodeRequired": "Postal/ZIP Code is required",
"companyNameRequiredBusiness": "Company name is required for business use",
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
"countryRequiredPersonal": "Country is required for personal use",
"agreeToTermsRequired": "You must agree to the terms",
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
},
"useCaseOptions": {
"personal": {
"title": "Personal Use",
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
},
"business": {
"title": "Business Use",
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
}
},
"steps": {
"emailLicenseType": {
"title": "Email & License Type",
"description": "Enter your email and choose your license type"
},
"personalInformation": {
"title": "Personal Information",
"description": "Tell us about yourself"
},
"contactInformation": {
"title": "Contact Information",
"description": "Your contact details"
},
"termsGenerate": {
"title": "Terms & Generate",
"description": "Review and accept terms to generate your license"
}
},
"alerts": {
"commercialUseDisclosure": {
"title": "Usage Disclosure",
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
},
"trialPeriodInformation": {
"title": "Trial Period Information",
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
}
},
"form": {
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
"firstName": "First Name",
"lastName": "Last Name",
"jobTitle": "Job Title",
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
"industryQuestion": "What is your industry?",
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
"companyName": "Company name",
"countryOfResidence": "Country of residence",
"stateProvinceRegion": "State / Province / Region",
"postalZipCode": "Postal / ZIP Code",
"companyWebsite": "Company website",
"companyPhoneNumber": "Company phone number",
"country": "Country",
"phoneNumberOptional": "Phone number (optional)",
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
},
"buttons": {
"close": "Close",
"previous": "Previous",
"next": "Next",
"generateLicenseKey": "Generate License Key"
},
"toasts": {
"success": {
"title": "License key generated successfully",
"description": "Your license key has been generated and is ready to use."
},
"error": {
"title": "Failed to generate license key",
"description": "An error occurred while generating the license key."
}
}
},
"priority": "Priorität",
"priorityDescription": "Die Routen mit höherer Priorität werden zuerst ausgewertet. Priorität = 100 bedeutet automatische Bestellung (Systementscheidung). Verwenden Sie eine andere Nummer, um manuelle Priorität zu erzwingen.",
"instanceName": "Instance Name",
"pathMatchModalTitle": "Configure Path Matching",
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
"pathMatchType": "Match Type",
"pathMatchPrefix": "Prefix",
"pathMatchExact": "Exact",
"pathMatchRegex": "Regex",
"pathMatchValue": "Path Value",
"clear": "Clear",
"saveChanges": "Save Changes",
"pathMatchRegexPlaceholder": "^/api/.*",
"pathMatchDefaultPlaceholder": "/path",
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
"pathMatchExactHelp": "Example: /api matches only /api",
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
"pathRewriteModalTitle": "Configure Path Rewriting",
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
"pathRewriteType": "Rewrite Type",
"pathRewritePrefixOption": "Prefix - Replace prefix",
"pathRewriteExactOption": "Exact - Replace entire path",
"pathRewriteRegexOption": "Regex - Pattern replacement",
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
"pathRewriteValue": "Rewrite Value",
"pathRewriteRegexPlaceholder": "/new/$1",
"pathRewriteDefaultPlaceholder": "/new-path",
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
"pathRewritePrefix": "Prefix",
"pathRewriteExact": "Exact",
"pathRewriteRegex": "Regex",
"pathRewriteStrip": "Strip",
"pathRewriteStripLabel": "strip"
} }

View File

@@ -1,4 +1,3 @@
{ {
"setupCreate": "Create your organization, site, and resources", "setupCreate": "Create your organization, site, and resources",
"setupNewOrg": "New Organization", "setupNewOrg": "New Organization",
@@ -97,7 +96,7 @@
"siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.", "siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
"siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.", "siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
"siteLocalDescription": "Local resources only. No tunneling.", "siteLocalDescription": "Local resources only. No tunneling.",
"siteLocalDescriptionSaas": "Local resources only. No tunneling.", "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
"siteSeeAll": "See All Sites", "siteSeeAll": "See All Sites",
"siteTunnelDescription": "Determine how you want to connect to your site", "siteTunnelDescription": "Determine how you want to connect to your site",
"siteNewtCredentials": "Newt Credentials", "siteNewtCredentials": "Newt Credentials",
@@ -469,7 +468,10 @@
"createdAt": "Created At", "createdAt": "Created At",
"proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.", "proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.",
"proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", "proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.",
"proxyEnableSSL": "Enable SSL (https)", "proxyEnableSSL": "Enable SSL",
"proxyEnableSSLDescription": "Enable SSL/TLS encryption for secure HTTPS connections to your targets.",
"target": "Target",
"configureTarget": "Configure Targets",
"targetErrorFetch": "Failed to fetch targets", "targetErrorFetch": "Failed to fetch targets",
"targetErrorFetchDescription": "An error occurred while fetching targets", "targetErrorFetchDescription": "An error occurred while fetching targets",
"siteErrorFetch": "Failed to fetch resource", "siteErrorFetch": "Failed to fetch resource",
@@ -496,7 +498,7 @@
"targetTlsSettings": "Secure Connection Configuration", "targetTlsSettings": "Secure Connection Configuration",
"targetTlsSettingsDescription": "Configure SSL/TLS settings for your resource", "targetTlsSettingsDescription": "Configure SSL/TLS settings for your resource",
"targetTlsSettingsAdvanced": "Advanced TLS Settings", "targetTlsSettingsAdvanced": "Advanced TLS Settings",
"targetTlsSni": "TLS Server Name (SNI)", "targetTlsSni": "TLS Server Name",
"targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.", "targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.",
"targetTlsSubmit": "Save Settings", "targetTlsSubmit": "Save Settings",
"targets": "Targets Configuration", "targets": "Targets Configuration",
@@ -505,9 +507,21 @@
"targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.", "targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.",
"methodSelect": "Select method", "methodSelect": "Select method",
"targetSubmit": "Add Target", "targetSubmit": "Add Target",
"targetNoOne": "No targets. Add a target using the form.", "targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to your backend.",
"targetNoOneDescription": "Adding more than one target above will enable load balancing.", "targetNoOneDescription": "Adding more than one target above will enable load balancing.",
"targetsSubmit": "Save Targets", "targetsSubmit": "Save Targets",
"addTarget": "Add Target",
"targetErrorInvalidIp": "Invalid IP address",
"targetErrorInvalidIpDescription": "Please enter a valid IP address or hostname",
"targetErrorInvalidPort": "Invalid port",
"targetErrorInvalidPortDescription": "Please enter a valid port number",
"targetErrorNoSite": "No site selected",
"targetErrorNoSiteDescription": "Please select a site for the target",
"targetCreated": "Target created",
"targetCreatedDescription": "Target has been created successfully",
"targetErrorCreate": "Failed to create target",
"targetErrorCreateDescription": "An error occurred while creating the target",
"save": "Save",
"proxyAdditional": "Additional Proxy Settings", "proxyAdditional": "Additional Proxy Settings",
"proxyAdditionalDescription": "Configure how your resource handles proxy settings", "proxyAdditionalDescription": "Configure how your resource handles proxy settings",
"proxyCustomHeader": "Custom Host Header", "proxyCustomHeader": "Custom Host Header",
@@ -716,7 +730,7 @@
"pangolinServerAdmin": "Server Admin - Pangolin", "pangolinServerAdmin": "Server Admin - Pangolin",
"licenseTierProfessional": "Professional License", "licenseTierProfessional": "Professional License",
"licenseTierEnterprise": "Enterprise License", "licenseTierEnterprise": "Enterprise License",
"licenseTierCommercial": "Commercial License", "licenseTierPersonal": "Personal License",
"licensed": "Licensed", "licensed": "Licensed",
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",
@@ -751,7 +765,7 @@
"idpDisplayName": "A display name for this identity provider", "idpDisplayName": "A display name for this identity provider",
"idpAutoProvisionUsers": "Auto Provision Users", "idpAutoProvisionUsers": "Auto Provision Users",
"idpAutoProvisionUsersDescription": "When enabled, users will be automatically created in the system upon first login with the ability to map users to roles and organizations.", "idpAutoProvisionUsersDescription": "When enabled, users will be automatically created in the system upon first login with the ability to map users to roles and organizations.",
"licenseBadge": "Professional", "licenseBadge": "EE",
"idpType": "Provider Type", "idpType": "Provider Type",
"idpTypeDescription": "Select the type of identity provider you want to configure", "idpTypeDescription": "Select the type of identity provider you want to configure",
"idpOidcConfigure": "OAuth2/OIDC Configuration", "idpOidcConfigure": "OAuth2/OIDC Configuration",
@@ -1041,26 +1055,6 @@
"actionDeleteResourceRule": "Delete Resource Rule", "actionDeleteResourceRule": "Delete Resource Rule",
"actionListResourceRules": "List Resource Rules", "actionListResourceRules": "List Resource Rules",
"actionUpdateResourceRule": "Update Resource Rule", "actionUpdateResourceRule": "Update Resource Rule",
"ruleTemplates": "Rule Templates",
"ruleTemplatesDescription": "Assign rule templates to automatically apply consistent rules across multiple resources",
"ruleTemplatesSearch": "Search templates...",
"ruleTemplateAdd": "Create Template",
"ruleTemplateErrorDelete": "Failed to delete template",
"ruleTemplateCreated": "Template created",
"ruleTemplateCreatedDescription": "Rule template created successfully",
"ruleTemplateErrorCreate": "Failed to create template",
"ruleTemplateErrorCreateDescription": "An error occurred while creating the template",
"ruleTemplateSetting": "Rule Template Settings",
"ruleTemplateSettingDescription": "Manage template details and rules",
"ruleTemplateErrorLoad": "Failed to load template",
"ruleTemplateErrorLoadDescription": "An error occurred while loading the template",
"ruleTemplateUpdated": "Template updated",
"ruleTemplateUpdatedDescription": "Template updated successfully",
"ruleTemplateErrorUpdate": "Failed to update template",
"ruleTemplateErrorUpdateDescription": "An error occurred while updating the template",
"save": "Save",
"saving": "Saving...",
"templateDetails": "Template Details",
"actionListOrgs": "List Organizations", "actionListOrgs": "List Organizations",
"actionCheckOrgId": "Check ID", "actionCheckOrgId": "Check ID",
"actionCreateOrg": "Create Organization", "actionCreateOrg": "Create Organization",
@@ -1105,7 +1099,6 @@
"navbar": "Navigation Menu", "navbar": "Navigation Menu",
"navbarDescription": "Main navigation menu for the application", "navbarDescription": "Main navigation menu for the application",
"navbarDocsLink": "Documentation", "navbarDocsLink": "Documentation",
"commercialEdition": "Commercial Edition",
"otpErrorEnable": "Unable to enable 2FA", "otpErrorEnable": "Unable to enable 2FA",
"otpErrorEnableDescription": "An error occurred while enabling 2FA", "otpErrorEnableDescription": "An error occurred while enabling 2FA",
"otpSetupCheckCode": "Please enter a 6-digit code", "otpSetupCheckCode": "Please enter a 6-digit code",
@@ -1156,13 +1149,12 @@
"sidebarInvitations": "Invitations", "sidebarInvitations": "Invitations",
"sidebarRoles": "Roles", "sidebarRoles": "Roles",
"sidebarShareableLinks": "Shareable Links", "sidebarShareableLinks": "Shareable Links",
"sidebarRuleTemplates": "Rule Templates",
"sidebarApiKeys": "API Keys", "sidebarApiKeys": "API Keys",
"sidebarSettings": "Settings", "sidebarSettings": "Settings",
"sidebarAllUsers": "All Users", "sidebarAllUsers": "All Users",
"sidebarIdentityProviders": "Identity Providers", "sidebarIdentityProviders": "Identity Providers",
"sidebarLicense": "License", "sidebarLicense": "License",
"sidebarClients": "Clients (Beta)", "sidebarClients": "Clients",
"sidebarDomains": "Domains", "sidebarDomains": "Domains",
"enableDockerSocket": "Enable Docker Blueprint", "enableDockerSocket": "Enable Docker Blueprint",
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.", "enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
@@ -1355,7 +1347,6 @@
"twoFactorRequired": "Two-factor authentication is required to register a security key.", "twoFactorRequired": "Two-factor authentication is required to register a security key.",
"twoFactor": "Two-Factor Authentication", "twoFactor": "Two-Factor Authentication",
"adminEnabled2FaOnYourAccount": "Your administrator has enabled two-factor authentication for {email}. Please complete the setup process to continue.", "adminEnabled2FaOnYourAccount": "Your administrator has enabled two-factor authentication for {email}. Please complete the setup process to continue.",
"continueToApplication": "Continue to Application",
"securityKeyAdd": "Add Security Key", "securityKeyAdd": "Add Security Key",
"securityKeyRegisterTitle": "Register New Security Key", "securityKeyRegisterTitle": "Register New Security Key",
"securityKeyRegisterDescription": "Connect your security key and enter a name to identify it", "securityKeyRegisterDescription": "Connect your security key and enter a name to identify it",
@@ -1433,6 +1424,7 @@
"externalProxyEnabled": "External Proxy Enabled", "externalProxyEnabled": "External Proxy Enabled",
"addNewTarget": "Add New Target", "addNewTarget": "Add New Target",
"targetsList": "Targets List", "targetsList": "Targets List",
"advancedMode": "Advanced Mode",
"targetErrorDuplicateTargetFound": "Duplicate target found", "targetErrorDuplicateTargetFound": "Duplicate target found",
"healthCheckHealthy": "Healthy", "healthCheckHealthy": "Healthy",
"healthCheckUnhealthy": "Unhealthy", "healthCheckUnhealthy": "Unhealthy",
@@ -1565,8 +1557,8 @@
"autoLoginError": "Auto Login Error", "autoLoginError": "Auto Login Error",
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.", "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
"remoteExitNodeManageRemoteExitNodes": "Manage Self-Hosted", "remoteExitNodeManageRemoteExitNodes": "Remote Nodes",
"remoteExitNodeDescription": "Manage nodes to extend your network connectivity", "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
"remoteExitNodes": "Nodes", "remoteExitNodes": "Nodes",
"searchRemoteExitNodes": "Search nodes...", "searchRemoteExitNodes": "Search nodes...",
"remoteExitNodeAdd": "Add Node", "remoteExitNodeAdd": "Add Node",
@@ -1576,7 +1568,7 @@
"remoteExitNodeMessageConfirm": "To confirm, please type the name of the node below.", "remoteExitNodeMessageConfirm": "To confirm, please type the name of the node below.",
"remoteExitNodeConfirmDelete": "Confirm Delete Node", "remoteExitNodeConfirmDelete": "Confirm Delete Node",
"remoteExitNodeDelete": "Delete Node", "remoteExitNodeDelete": "Delete Node",
"sidebarRemoteExitNodes": "Nodes", "sidebarRemoteExitNodes": "Remote Nodes",
"remoteExitNodeCreate": { "remoteExitNodeCreate": {
"title": "Create Node", "title": "Create Node",
"description": "Create a new node to extend your network connectivity", "description": "Create a new node to extend your network connectivity",
@@ -1746,5 +1738,161 @@
"healthCheckNotAvailable": "Local", "healthCheckNotAvailable": "Local",
"rewritePath": "Rewrite Path", "rewritePath": "Rewrite Path",
"rewritePathDescription": "Optionally rewrite the path before forwarding to the target.", "rewritePathDescription": "Optionally rewrite the path before forwarding to the target.",
"continueToApplication": "Continue to application" "continueToApplication": "Continue to application",
"checkingInvite": "Checking Invite",
"setResourceHeaderAuth": "setResourceHeaderAuth",
"resourceHeaderAuthRemove": "Remove Header Auth",
"resourceHeaderAuthRemoveDescription": "Header authentication removed successfully.",
"resourceErrorHeaderAuthRemove": "Failed to remove Header Authentication",
"resourceErrorHeaderAuthRemoveDescription": "Could not remove header authentication for the resource.",
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
"headerAuthRemove": "Remove Header Auth",
"headerAuthAdd": "Add Header Auth",
"resourceErrorHeaderAuthSetup": "Failed to set Header Authentication",
"resourceErrorHeaderAuthSetupDescription": "Could not set header authentication for the resource.",
"resourceHeaderAuthSetup": "Header Authentication set successfully",
"resourceHeaderAuthSetupDescription": "Header authentication has been successfully set.",
"resourceHeaderAuthSetupTitle": "Set Header Authentication",
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
"resourceHeaderAuthSubmit": "Set Header Authentication",
"actionSetResourceHeaderAuth": "Set Header Authentication",
"enterpriseEdition": "Enterprise Edition",
"unlicensed": "Unlicensed",
"beta": "Beta",
"manageClients": "Manage Clients",
"manageClientsDescription": "Clients are devices that can connect to your sites",
"licenseTableValidUntil": "Valid Until",
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
"sidebarEnterpriseLicenses": "Licenses",
"generateLicenseKey": "Generate License Key",
"generateLicenseKeyForm": {
"validation": {
"emailRequired": "Please enter a valid email address",
"useCaseTypeRequired": "Please select a use case type",
"firstNameRequired": "First name is required",
"lastNameRequired": "Last name is required",
"primaryUseRequired": "Please describe your primary use",
"jobTitleRequiredBusiness": "Job title is required for business use",
"industryRequiredBusiness": "Industry is required for business use",
"stateProvinceRegionRequired": "State/Province/Region is required",
"postalZipCodeRequired": "Postal/ZIP Code is required",
"companyNameRequiredBusiness": "Company name is required for business use",
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
"countryRequiredPersonal": "Country is required for personal use",
"agreeToTermsRequired": "You must agree to the terms",
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
},
"useCaseOptions": {
"personal": {
"title": "Personal Use",
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
},
"business": {
"title": "Business Use",
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
}
},
"steps": {
"emailLicenseType": {
"title": "Email & License Type",
"description": "Enter your email and choose your license type"
},
"personalInformation": {
"title": "Personal Information",
"description": "Tell us about yourself"
},
"contactInformation": {
"title": "Contact Information",
"description": "Your contact details"
},
"termsGenerate": {
"title": "Terms & Generate",
"description": "Review and accept terms to generate your license"
}
},
"alerts": {
"commercialUseDisclosure": {
"title": "Usage Disclosure",
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
},
"trialPeriodInformation": {
"title": "Trial Period Information",
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
}
},
"form": {
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
"firstName": "First Name",
"lastName": "Last Name",
"jobTitle": "Job Title",
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
"industryQuestion": "What is your industry?",
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
"companyName": "Company name",
"countryOfResidence": "Country of residence",
"stateProvinceRegion": "State / Province / Region",
"postalZipCode": "Postal / ZIP Code",
"companyWebsite": "Company website",
"companyPhoneNumber": "Company phone number",
"country": "Country",
"phoneNumberOptional": "Phone number (optional)",
"complianceConfirmation": "I confirm that the information I provided is accurate and that I am in compliance with the Fossorial Commercial License. Reporting inaccurate information or misidentifying use of the product is a violation of the license and may result in your key getting revoked."
},
"buttons": {
"close": "Close",
"previous": "Previous",
"next": "Next",
"generateLicenseKey": "Generate License Key"
},
"toasts": {
"success": {
"title": "License key generated successfully",
"description": "Your license key has been generated and is ready to use."
},
"error": {
"title": "Failed to generate license key",
"description": "An error occurred while generating the license key."
}
}
},
"priority": "Priority",
"priorityDescription": "Higher priority routes are evaluated first. Priority = 100 means automatic ordering (system decides). Use another number to enforce manual priority.",
"instanceName": "Instance Name",
"pathMatchModalTitle": "Configure Path Matching",
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
"pathMatchType": "Match Type",
"pathMatchPrefix": "Prefix",
"pathMatchExact": "Exact",
"pathMatchRegex": "Regex",
"pathMatchValue": "Path Value",
"clear": "Clear",
"saveChanges": "Save Changes",
"pathMatchRegexPlaceholder": "^/api/.*",
"pathMatchDefaultPlaceholder": "/path",
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
"pathMatchExactHelp": "Example: /api matches only /api",
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
"pathRewriteModalTitle": "Configure Path Rewriting",
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
"pathRewriteType": "Rewrite Type",
"pathRewritePrefixOption": "Prefix - Replace prefix",
"pathRewriteExactOption": "Exact - Replace entire path",
"pathRewriteRegexOption": "Regex - Pattern replacement",
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
"pathRewriteValue": "Rewrite Value",
"pathRewriteRegexPlaceholder": "/new/$1",
"pathRewriteDefaultPlaceholder": "/new-path",
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
"pathRewritePrefix": "Prefix",
"pathRewriteExact": "Exact",
"pathRewriteRegex": "Regex",
"pathRewriteStrip": "Strip",
"pathRewriteStripLabel": "strip",
"sidebarEnableEnterpriseLicense": "Enable Enterprise License"
} }

View File

@@ -96,7 +96,7 @@
"siteWgDescription": "Utilice cualquier cliente Wirex Guard para establecer un túnel. Se requiere una configuración manual de NAT.", "siteWgDescription": "Utilice cualquier cliente Wirex Guard para establecer un túnel. Se requiere una configuración manual de NAT.",
"siteWgDescriptionSaas": "Utilice cualquier cliente de WireGuard para establecer un túnel. Se requiere configuración manual de NAT. SOLO FUNCIONA EN NODOS AUTOGESTIONADOS", "siteWgDescriptionSaas": "Utilice cualquier cliente de WireGuard para establecer un túnel. Se requiere configuración manual de NAT. SOLO FUNCIONA EN NODOS AUTOGESTIONADOS",
"siteLocalDescription": "Solo recursos locales. Sin túneles.", "siteLocalDescription": "Solo recursos locales. Sin túneles.",
"siteLocalDescriptionSaas": "Solo recursos locales. Sin túneles. SOLO FUNCIONA EN NODOS AUTOGESTIONADOS", "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
"siteSeeAll": "Ver todos los sitios", "siteSeeAll": "Ver todos los sitios",
"siteTunnelDescription": "Determina cómo quieres conectarte a tu sitio", "siteTunnelDescription": "Determina cómo quieres conectarte a tu sitio",
"siteNewtCredentials": "Credenciales nuevas", "siteNewtCredentials": "Credenciales nuevas",
@@ -468,7 +468,10 @@
"createdAt": "Creado el", "createdAt": "Creado el",
"proxyErrorInvalidHeader": "Valor de cabecera de host personalizado no válido. Utilice el formato de nombre de dominio, o guarde en blanco para desestablecer cabecera de host personalizada.", "proxyErrorInvalidHeader": "Valor de cabecera de host personalizado no válido. Utilice el formato de nombre de dominio, o guarde en blanco para desestablecer cabecera de host personalizada.",
"proxyErrorTls": "Nombre de servidor TLS inválido. Utilice el formato de nombre de dominio o guarde en blanco para eliminar el nombre de servidor TLS.", "proxyErrorTls": "Nombre de servidor TLS inválido. Utilice el formato de nombre de dominio o guarde en blanco para eliminar el nombre de servidor TLS.",
"proxyEnableSSL": "Habilitar SSL (https)", "proxyEnableSSL": "Activar SSL",
"proxyEnableSSLDescription": "Activa el cifrado SSL/TLS para conexiones seguras HTTPS a tus objetivos.",
"target": "Target",
"configureTarget": "Configurar objetivos",
"targetErrorFetch": "Error al recuperar los objetivos", "targetErrorFetch": "Error al recuperar los objetivos",
"targetErrorFetchDescription": "Se ha producido un error al recuperar los objetivos", "targetErrorFetchDescription": "Se ha producido un error al recuperar los objetivos",
"siteErrorFetch": "No se pudo obtener el recurso", "siteErrorFetch": "No se pudo obtener el recurso",
@@ -495,7 +498,7 @@
"targetTlsSettings": "Configuración de conexión segura", "targetTlsSettings": "Configuración de conexión segura",
"targetTlsSettingsDescription": "Configurar ajustes SSL/TLS para su recurso", "targetTlsSettingsDescription": "Configurar ajustes SSL/TLS para su recurso",
"targetTlsSettingsAdvanced": "Ajustes avanzados de TLS", "targetTlsSettingsAdvanced": "Ajustes avanzados de TLS",
"targetTlsSni": "Nombre del servidor TLS (SNI)", "targetTlsSni": "Nombre del servidor TLS",
"targetTlsSniDescription": "El nombre del servidor TLS a usar para SNI. Deje en blanco para usar el valor predeterminado.", "targetTlsSniDescription": "El nombre del servidor TLS a usar para SNI. Deje en blanco para usar el valor predeterminado.",
"targetTlsSubmit": "Guardar ajustes", "targetTlsSubmit": "Guardar ajustes",
"targets": "Configuración de objetivos", "targets": "Configuración de objetivos",
@@ -504,9 +507,21 @@
"targetStickySessionsDescription": "Mantener conexiones en el mismo objetivo de backend para toda su sesión.", "targetStickySessionsDescription": "Mantener conexiones en el mismo objetivo de backend para toda su sesión.",
"methodSelect": "Seleccionar método", "methodSelect": "Seleccionar método",
"targetSubmit": "Añadir destino", "targetSubmit": "Añadir destino",
"targetNoOne": "No hay objetivos. Agregue un objetivo usando el formulario.", "targetNoOne": "Este recurso no tiene ningún objetivo. Agrega un objetivo para configurar dónde enviar peticiones al backend.",
"targetNoOneDescription": "Si se añade más de un objetivo anterior se activará el balance de carga.", "targetNoOneDescription": "Si se añade más de un objetivo anterior se activará el balance de carga.",
"targetsSubmit": "Guardar objetivos", "targetsSubmit": "Guardar objetivos",
"addTarget": "Añadir destino",
"targetErrorInvalidIp": "Dirección IP inválida",
"targetErrorInvalidIpDescription": "Por favor, introduzca una dirección IP válida o nombre de host",
"targetErrorInvalidPort": "Puerto inválido",
"targetErrorInvalidPortDescription": "Por favor, introduzca un número de puerto válido",
"targetErrorNoSite": "Ningún sitio seleccionado",
"targetErrorNoSiteDescription": "Por favor, seleccione un sitio para el objetivo",
"targetCreated": "Objetivo creado",
"targetCreatedDescription": "El objetivo se ha creado correctamente",
"targetErrorCreate": "Error al crear el objetivo",
"targetErrorCreateDescription": "Se ha producido un error al crear el objetivo",
"save": "Guardar",
"proxyAdditional": "Ajustes adicionales del proxy", "proxyAdditional": "Ajustes adicionales del proxy",
"proxyAdditionalDescription": "Configura cómo tu recurso maneja la configuración del proxy", "proxyAdditionalDescription": "Configura cómo tu recurso maneja la configuración del proxy",
"proxyCustomHeader": "Cabecera de host personalizada", "proxyCustomHeader": "Cabecera de host personalizada",
@@ -715,7 +730,7 @@
"pangolinServerAdmin": "Admin Servidor - Pangolin", "pangolinServerAdmin": "Admin Servidor - Pangolin",
"licenseTierProfessional": "Licencia profesional", "licenseTierProfessional": "Licencia profesional",
"licenseTierEnterprise": "Licencia Enterprise", "licenseTierEnterprise": "Licencia Enterprise",
"licenseTierCommercial": "Licencia comercial", "licenseTierPersonal": "Personal License",
"licensed": "Licenciado", "licensed": "Licenciado",
"yes": "Sí", "yes": "Sí",
"no": "Nu", "no": "Nu",
@@ -750,7 +765,7 @@
"idpDisplayName": "Un nombre mostrado para este proveedor de identidad", "idpDisplayName": "Un nombre mostrado para este proveedor de identidad",
"idpAutoProvisionUsers": "Auto-Provisión de Usuarios", "idpAutoProvisionUsers": "Auto-Provisión de Usuarios",
"idpAutoProvisionUsersDescription": "Cuando está habilitado, los usuarios serán creados automáticamente en el sistema al iniciar sesión con la capacidad de asignar a los usuarios a roles y organizaciones.", "idpAutoProvisionUsersDescription": "Cuando está habilitado, los usuarios serán creados automáticamente en el sistema al iniciar sesión con la capacidad de asignar a los usuarios a roles y organizaciones.",
"licenseBadge": "Profesional", "licenseBadge": "EE",
"idpType": "Tipo de proveedor", "idpType": "Tipo de proveedor",
"idpTypeDescription": "Seleccione el tipo de proveedor de identidad que desea configurar", "idpTypeDescription": "Seleccione el tipo de proveedor de identidad que desea configurar",
"idpOidcConfigure": "Configuración OAuth2/OIDC", "idpOidcConfigure": "Configuración OAuth2/OIDC",
@@ -1084,7 +1099,6 @@
"navbar": "Menú de navegación", "navbar": "Menú de navegación",
"navbarDescription": "Menú de navegación principal para la aplicación", "navbarDescription": "Menú de navegación principal para la aplicación",
"navbarDocsLink": "Documentación", "navbarDocsLink": "Documentación",
"commercialEdition": "Edición Comercial",
"otpErrorEnable": "No se puede habilitar 2FA", "otpErrorEnable": "No se puede habilitar 2FA",
"otpErrorEnableDescription": "Se ha producido un error al habilitar 2FA", "otpErrorEnableDescription": "Se ha producido un error al habilitar 2FA",
"otpSetupCheckCode": "Por favor, introduzca un código de 6 dígitos", "otpSetupCheckCode": "Por favor, introduzca un código de 6 dígitos",
@@ -1140,7 +1154,7 @@
"sidebarAllUsers": "Todos los usuarios", "sidebarAllUsers": "Todos los usuarios",
"sidebarIdentityProviders": "Proveedores de identidad", "sidebarIdentityProviders": "Proveedores de identidad",
"sidebarLicense": "Licencia", "sidebarLicense": "Licencia",
"sidebarClients": "Clientes (Beta)", "sidebarClients": "Clients",
"sidebarDomains": "Dominios", "sidebarDomains": "Dominios",
"enableDockerSocket": "Habilitar Plano Docker", "enableDockerSocket": "Habilitar Plano Docker",
"enableDockerSocketDescription": "Activar el raspado de etiquetas de Socket Docker para etiquetas de planos. La ruta del Socket debe proporcionarse a Newt.", "enableDockerSocketDescription": "Activar el raspado de etiquetas de Socket Docker para etiquetas de planos. La ruta del Socket debe proporcionarse a Newt.",
@@ -1333,7 +1347,6 @@
"twoFactorRequired": "Se requiere autenticación de dos factores para registrar una llave de seguridad.", "twoFactorRequired": "Se requiere autenticación de dos factores para registrar una llave de seguridad.",
"twoFactor": "Autenticación de dos factores", "twoFactor": "Autenticación de dos factores",
"adminEnabled2FaOnYourAccount": "Su administrador ha habilitado la autenticación de dos factores para {email}. Por favor, complete el proceso de configuración para continuar.", "adminEnabled2FaOnYourAccount": "Su administrador ha habilitado la autenticación de dos factores para {email}. Por favor, complete el proceso de configuración para continuar.",
"continueToApplication": "Continuar a la aplicación",
"securityKeyAdd": "Agregar llave de seguridad", "securityKeyAdd": "Agregar llave de seguridad",
"securityKeyRegisterTitle": "Registrar nueva llave de seguridad", "securityKeyRegisterTitle": "Registrar nueva llave de seguridad",
"securityKeyRegisterDescription": "Conecta tu llave de seguridad y escribe un nombre para identificarla", "securityKeyRegisterDescription": "Conecta tu llave de seguridad y escribe un nombre para identificarla",
@@ -1411,6 +1424,7 @@
"externalProxyEnabled": "Proxy externo habilitado", "externalProxyEnabled": "Proxy externo habilitado",
"addNewTarget": "Agregar nuevo destino", "addNewTarget": "Agregar nuevo destino",
"targetsList": "Lista de destinos", "targetsList": "Lista de destinos",
"advancedMode": "Modo avanzado",
"targetErrorDuplicateTargetFound": "Se encontró un destino duplicado", "targetErrorDuplicateTargetFound": "Se encontró un destino duplicado",
"healthCheckHealthy": "Saludable", "healthCheckHealthy": "Saludable",
"healthCheckUnhealthy": "No saludable", "healthCheckUnhealthy": "No saludable",
@@ -1543,8 +1557,8 @@
"autoLoginError": "Error de inicio de sesión automático", "autoLoginError": "Error de inicio de sesión automático",
"autoLoginErrorNoRedirectUrl": "No se recibió URL de redirección del proveedor de identidad.", "autoLoginErrorNoRedirectUrl": "No se recibió URL de redirección del proveedor de identidad.",
"autoLoginErrorGeneratingUrl": "Error al generar URL de autenticación.", "autoLoginErrorGeneratingUrl": "Error al generar URL de autenticación.",
"remoteExitNodeManageRemoteExitNodes": "Administrar Nodos Autogestionados", "remoteExitNodeManageRemoteExitNodes": "Nodos remotos",
"remoteExitNodeDescription": "Administrar nodos para extender la conectividad de red", "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
"remoteExitNodes": "Nodos", "remoteExitNodes": "Nodos",
"searchRemoteExitNodes": "Buscar nodos...", "searchRemoteExitNodes": "Buscar nodos...",
"remoteExitNodeAdd": "Añadir Nodo", "remoteExitNodeAdd": "Añadir Nodo",
@@ -1554,7 +1568,7 @@
"remoteExitNodeMessageConfirm": "Para confirmar, por favor escriba el nombre del nodo a continuación.", "remoteExitNodeMessageConfirm": "Para confirmar, por favor escriba el nombre del nodo a continuación.",
"remoteExitNodeConfirmDelete": "Confirmar eliminar nodo", "remoteExitNodeConfirmDelete": "Confirmar eliminar nodo",
"remoteExitNodeDelete": "Eliminar Nodo", "remoteExitNodeDelete": "Eliminar Nodo",
"sidebarRemoteExitNodes": "Nodos", "sidebarRemoteExitNodes": "Nodos remotos",
"remoteExitNodeCreate": { "remoteExitNodeCreate": {
"title": "Crear Nodo", "title": "Crear Nodo",
"description": "Crear un nuevo nodo para extender la conectividad de red", "description": "Crear un nuevo nodo para extender la conectividad de red",
@@ -1723,5 +1737,161 @@
"authPageUpdated": "Página auth actualizada correctamente", "authPageUpdated": "Página auth actualizada correctamente",
"healthCheckNotAvailable": "Local", "healthCheckNotAvailable": "Local",
"rewritePath": "Reescribir Ruta", "rewritePath": "Reescribir Ruta",
"rewritePathDescription": "Opcionalmente reescribe la ruta antes de reenviar al destino." "rewritePathDescription": "Opcionalmente reescribe la ruta antes de reenviar al destino.",
"continueToApplication": "Continuar a la aplicación",
"checkingInvite": "Comprobando invitación",
"setResourceHeaderAuth": "set-Resource HeaderAuth",
"resourceHeaderAuthRemove": "Eliminar Auth del Encabezado",
"resourceHeaderAuthRemoveDescription": "Autenticación de cabecera eliminada correctamente.",
"resourceErrorHeaderAuthRemove": "Error al eliminar autenticación de cabecera",
"resourceErrorHeaderAuthRemoveDescription": "No se pudo eliminar la autenticación de cabecera del recurso.",
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
"headerAuthRemove": "Remove Header Auth",
"headerAuthAdd": "Add Header Auth",
"resourceErrorHeaderAuthSetup": "Error al establecer autenticación de cabecera",
"resourceErrorHeaderAuthSetupDescription": "No se pudo establecer autenticación de cabecera para el recurso.",
"resourceHeaderAuthSetup": "Autenticación de cabecera establecida correctamente",
"resourceHeaderAuthSetupDescription": "La autenticación de cabecera se ha establecido correctamente.",
"resourceHeaderAuthSetupTitle": "Establecer autenticación de cabecera",
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
"resourceHeaderAuthSubmit": "Establecer autenticación de cabecera",
"actionSetResourceHeaderAuth": "Establecer autenticación de cabecera",
"enterpriseEdition": "Enterprise Edition",
"unlicensed": "Unlicensed",
"beta": "Beta",
"manageClients": "Manage Clients",
"manageClientsDescription": "Clients are devices that can connect to your sites",
"licenseTableValidUntil": "Valid Until",
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
"sidebarEnterpriseLicenses": "Licenses",
"generateLicenseKey": "Generate License Key",
"generateLicenseKeyForm": {
"validation": {
"emailRequired": "Please enter a valid email address",
"useCaseTypeRequired": "Please select a use case type",
"firstNameRequired": "First name is required",
"lastNameRequired": "Last name is required",
"primaryUseRequired": "Please describe your primary use",
"jobTitleRequiredBusiness": "Job title is required for business use",
"industryRequiredBusiness": "Industry is required for business use",
"stateProvinceRegionRequired": "State/Province/Region is required",
"postalZipCodeRequired": "Postal/ZIP Code is required",
"companyNameRequiredBusiness": "Company name is required for business use",
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
"countryRequiredPersonal": "Country is required for personal use",
"agreeToTermsRequired": "You must agree to the terms",
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
},
"useCaseOptions": {
"personal": {
"title": "Personal Use",
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
},
"business": {
"title": "Business Use",
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
}
},
"steps": {
"emailLicenseType": {
"title": "Email & License Type",
"description": "Enter your email and choose your license type"
},
"personalInformation": {
"title": "Personal Information",
"description": "Tell us about yourself"
},
"contactInformation": {
"title": "Contact Information",
"description": "Your contact details"
},
"termsGenerate": {
"title": "Terms & Generate",
"description": "Review and accept terms to generate your license"
}
},
"alerts": {
"commercialUseDisclosure": {
"title": "Usage Disclosure",
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
},
"trialPeriodInformation": {
"title": "Trial Period Information",
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
}
},
"form": {
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
"firstName": "First Name",
"lastName": "Last Name",
"jobTitle": "Job Title",
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
"industryQuestion": "What is your industry?",
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
"companyName": "Company name",
"countryOfResidence": "Country of residence",
"stateProvinceRegion": "State / Province / Region",
"postalZipCode": "Postal / ZIP Code",
"companyWebsite": "Company website",
"companyPhoneNumber": "Company phone number",
"country": "Country",
"phoneNumberOptional": "Phone number (optional)",
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
},
"buttons": {
"close": "Close",
"previous": "Previous",
"next": "Next",
"generateLicenseKey": "Generate License Key"
},
"toasts": {
"success": {
"title": "License key generated successfully",
"description": "Your license key has been generated and is ready to use."
},
"error": {
"title": "Failed to generate license key",
"description": "An error occurred while generating the license key."
}
}
},
"priority": "Prioridad",
"priorityDescription": "Las rutas de prioridad más alta son evaluadas primero. Prioridad = 100 significa orden automático (decisiones del sistema). Utilice otro número para hacer cumplir la prioridad manual.",
"instanceName": "Instance Name",
"pathMatchModalTitle": "Configure Path Matching",
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
"pathMatchType": "Match Type",
"pathMatchPrefix": "Prefix",
"pathMatchExact": "Exact",
"pathMatchRegex": "Regex",
"pathMatchValue": "Path Value",
"clear": "Clear",
"saveChanges": "Save Changes",
"pathMatchRegexPlaceholder": "^/api/.*",
"pathMatchDefaultPlaceholder": "/path",
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
"pathMatchExactHelp": "Example: /api matches only /api",
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
"pathRewriteModalTitle": "Configure Path Rewriting",
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
"pathRewriteType": "Rewrite Type",
"pathRewritePrefixOption": "Prefix - Replace prefix",
"pathRewriteExactOption": "Exact - Replace entire path",
"pathRewriteRegexOption": "Regex - Pattern replacement",
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
"pathRewriteValue": "Rewrite Value",
"pathRewriteRegexPlaceholder": "/new/$1",
"pathRewriteDefaultPlaceholder": "/new-path",
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
"pathRewritePrefix": "Prefix",
"pathRewriteExact": "Exact",
"pathRewriteRegex": "Regex",
"pathRewriteStrip": "Strip",
"pathRewriteStripLabel": "strip"
} }

View File

@@ -96,7 +96,7 @@
"siteWgDescription": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise.", "siteWgDescription": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise.",
"siteWgDescriptionSaas": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES", "siteWgDescriptionSaas": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES",
"siteLocalDescription": "Ressources locales seulement. Pas de tunneling.", "siteLocalDescription": "Ressources locales seulement. Pas de tunneling.",
"siteLocalDescriptionSaas": "Ressources locales uniquement. Pas de tunneling. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES", "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
"siteSeeAll": "Voir tous les sites", "siteSeeAll": "Voir tous les sites",
"siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre site", "siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre site",
"siteNewtCredentials": "Identifiants Newt", "siteNewtCredentials": "Identifiants Newt",
@@ -468,7 +468,10 @@
"createdAt": "Créé le", "createdAt": "Créé le",
"proxyErrorInvalidHeader": "Valeur d'en-tête Host personnalisée invalide. Utilisez le format de nom de domaine, ou laissez vide pour désactiver l'en-tête Host personnalisé.", "proxyErrorInvalidHeader": "Valeur d'en-tête Host personnalisée invalide. Utilisez le format de nom de domaine, ou laissez vide pour désactiver l'en-tête Host personnalisé.",
"proxyErrorTls": "Nom de serveur TLS invalide. Utilisez le format de nom de domaine, ou laissez vide pour supprimer le nom de serveur TLS.", "proxyErrorTls": "Nom de serveur TLS invalide. Utilisez le format de nom de domaine, ou laissez vide pour supprimer le nom de serveur TLS.",
"proxyEnableSSL": "Activer SSL (https)", "proxyEnableSSL": "Activer SSL",
"proxyEnableSSLDescription": "Activez le cryptage SSL/TLS pour des connexions HTTPS sécurisées vers vos cibles.",
"target": "Target",
"configureTarget": "Configurer les cibles",
"targetErrorFetch": "Échec de la récupération des cibles", "targetErrorFetch": "Échec de la récupération des cibles",
"targetErrorFetchDescription": "Une erreur s'est produite lors de la récupération des cibles", "targetErrorFetchDescription": "Une erreur s'est produite lors de la récupération des cibles",
"siteErrorFetch": "Échec de la récupération de la ressource", "siteErrorFetch": "Échec de la récupération de la ressource",
@@ -495,7 +498,7 @@
"targetTlsSettings": "Configuration sécurisée de connexion", "targetTlsSettings": "Configuration sécurisée de connexion",
"targetTlsSettingsDescription": "Configurer les paramètres SSL/TLS pour votre ressource", "targetTlsSettingsDescription": "Configurer les paramètres SSL/TLS pour votre ressource",
"targetTlsSettingsAdvanced": "Paramètres TLS avancés", "targetTlsSettingsAdvanced": "Paramètres TLS avancés",
"targetTlsSni": "Nom de serveur TLS (SNI)", "targetTlsSni": "Nom du serveur TLS",
"targetTlsSniDescription": "Le nom de serveur TLS à utiliser pour SNI. Laissez vide pour utiliser la valeur par défaut.", "targetTlsSniDescription": "Le nom de serveur TLS à utiliser pour SNI. Laissez vide pour utiliser la valeur par défaut.",
"targetTlsSubmit": "Enregistrer les paramètres", "targetTlsSubmit": "Enregistrer les paramètres",
"targets": "Configuration des cibles", "targets": "Configuration des cibles",
@@ -504,9 +507,21 @@
"targetStickySessionsDescription": "Maintenir les connexions sur la même cible backend pendant toute leur session.", "targetStickySessionsDescription": "Maintenir les connexions sur la même cible backend pendant toute leur session.",
"methodSelect": "Sélectionner la méthode", "methodSelect": "Sélectionner la méthode",
"targetSubmit": "Ajouter une cible", "targetSubmit": "Ajouter une cible",
"targetNoOne": "Aucune cible. Ajoutez une cible en utilisant le formulaire.", "targetNoOne": "Cette ressource n'a aucune cible. Ajoutez une cible pour configurer où envoyer des requêtes à votre backend.",
"targetNoOneDescription": "L'ajout de plus d'une cible ci-dessus activera l'équilibrage de charge.", "targetNoOneDescription": "L'ajout de plus d'une cible ci-dessus activera l'équilibrage de charge.",
"targetsSubmit": "Enregistrer les cibles", "targetsSubmit": "Enregistrer les cibles",
"addTarget": "Ajouter une cible",
"targetErrorInvalidIp": "Adresse IP invalide",
"targetErrorInvalidIpDescription": "Veuillez entrer une adresse IP ou un nom d'hôte valide",
"targetErrorInvalidPort": "Port invalide",
"targetErrorInvalidPortDescription": "Veuillez entrer un numéro de port valide",
"targetErrorNoSite": "Aucun site sélectionné",
"targetErrorNoSiteDescription": "Veuillez sélectionner un site pour la cible",
"targetCreated": "Cible créée",
"targetCreatedDescription": "La cible a été créée avec succès",
"targetErrorCreate": "Impossible de créer la cible",
"targetErrorCreateDescription": "Une erreur s'est produite lors de la création de la cible",
"save": "Enregistrer",
"proxyAdditional": "Paramètres de proxy supplémentaires", "proxyAdditional": "Paramètres de proxy supplémentaires",
"proxyAdditionalDescription": "Configurer la façon dont votre ressource gère les paramètres de proxy", "proxyAdditionalDescription": "Configurer la façon dont votre ressource gère les paramètres de proxy",
"proxyCustomHeader": "En-tête Host personnalisé", "proxyCustomHeader": "En-tête Host personnalisé",
@@ -715,7 +730,7 @@
"pangolinServerAdmin": "Admin Serveur - Pangolin", "pangolinServerAdmin": "Admin Serveur - Pangolin",
"licenseTierProfessional": "Licence Professionnelle", "licenseTierProfessional": "Licence Professionnelle",
"licenseTierEnterprise": "Licence Entreprise", "licenseTierEnterprise": "Licence Entreprise",
"licenseTierCommercial": "Licence commerciale", "licenseTierPersonal": "Personal License",
"licensed": "Sous licence", "licensed": "Sous licence",
"yes": "Oui", "yes": "Oui",
"no": "Non", "no": "Non",
@@ -750,7 +765,7 @@
"idpDisplayName": "Un nom d'affichage pour ce fournisseur d'identité", "idpDisplayName": "Un nom d'affichage pour ce fournisseur d'identité",
"idpAutoProvisionUsers": "Approvisionnement automatique des utilisateurs", "idpAutoProvisionUsers": "Approvisionnement automatique des utilisateurs",
"idpAutoProvisionUsersDescription": "Lorsque cette option est activée, les utilisateurs seront automatiquement créés dans le système lors de leur première connexion avec la possibilité de mapper les utilisateurs aux rôles et aux organisations.", "idpAutoProvisionUsersDescription": "Lorsque cette option est activée, les utilisateurs seront automatiquement créés dans le système lors de leur première connexion avec la possibilité de mapper les utilisateurs aux rôles et aux organisations.",
"licenseBadge": "Professionnel", "licenseBadge": "EE",
"idpType": "Type de fournisseur", "idpType": "Type de fournisseur",
"idpTypeDescription": "Sélectionnez le type de fournisseur d'identité que vous souhaitez configurer", "idpTypeDescription": "Sélectionnez le type de fournisseur d'identité que vous souhaitez configurer",
"idpOidcConfigure": "Configuration OAuth2/OIDC", "idpOidcConfigure": "Configuration OAuth2/OIDC",
@@ -1084,7 +1099,6 @@
"navbar": "Menu de navigation", "navbar": "Menu de navigation",
"navbarDescription": "Menu de navigation principal de l'application", "navbarDescription": "Menu de navigation principal de l'application",
"navbarDocsLink": "Documentation", "navbarDocsLink": "Documentation",
"commercialEdition": "Édition Commerciale",
"otpErrorEnable": "Impossible d'activer l'A2F", "otpErrorEnable": "Impossible d'activer l'A2F",
"otpErrorEnableDescription": "Une erreur s'est produite lors de l'activation de l'A2F", "otpErrorEnableDescription": "Une erreur s'est produite lors de l'activation de l'A2F",
"otpSetupCheckCode": "Veuillez entrer un code à 6 chiffres", "otpSetupCheckCode": "Veuillez entrer un code à 6 chiffres",
@@ -1140,7 +1154,7 @@
"sidebarAllUsers": "Tous les utilisateurs", "sidebarAllUsers": "Tous les utilisateurs",
"sidebarIdentityProviders": "Fournisseurs d'identité", "sidebarIdentityProviders": "Fournisseurs d'identité",
"sidebarLicense": "Licence", "sidebarLicense": "Licence",
"sidebarClients": "Clients (Bêta)", "sidebarClients": "Clients",
"sidebarDomains": "Domaines", "sidebarDomains": "Domaines",
"enableDockerSocket": "Activer le Plan Docker", "enableDockerSocket": "Activer le Plan Docker",
"enableDockerSocketDescription": "Activer le ramassage d'étiquettes de socket Docker pour les étiquettes de plan. Le chemin de socket doit être fourni à Newt.", "enableDockerSocketDescription": "Activer le ramassage d'étiquettes de socket Docker pour les étiquettes de plan. Le chemin de socket doit être fourni à Newt.",
@@ -1333,7 +1347,6 @@
"twoFactorRequired": "L'authentification à deux facteurs est requise pour enregistrer une clé de sécurité.", "twoFactorRequired": "L'authentification à deux facteurs est requise pour enregistrer une clé de sécurité.",
"twoFactor": "Authentification à deux facteurs", "twoFactor": "Authentification à deux facteurs",
"adminEnabled2FaOnYourAccount": "Votre administrateur a activé l'authentification à deux facteurs pour {email}. Veuillez terminer le processus d'installation pour continuer.", "adminEnabled2FaOnYourAccount": "Votre administrateur a activé l'authentification à deux facteurs pour {email}. Veuillez terminer le processus d'installation pour continuer.",
"continueToApplication": "Continuer vers l'application",
"securityKeyAdd": "Ajouter une clé de sécurité", "securityKeyAdd": "Ajouter une clé de sécurité",
"securityKeyRegisterTitle": "Enregistrer une nouvelle clé de sécurité", "securityKeyRegisterTitle": "Enregistrer une nouvelle clé de sécurité",
"securityKeyRegisterDescription": "Connectez votre clé de sécurité et saisissez un nom pour l'identifier", "securityKeyRegisterDescription": "Connectez votre clé de sécurité et saisissez un nom pour l'identifier",
@@ -1411,6 +1424,7 @@
"externalProxyEnabled": "Proxy externe activé", "externalProxyEnabled": "Proxy externe activé",
"addNewTarget": "Ajouter une nouvelle cible", "addNewTarget": "Ajouter une nouvelle cible",
"targetsList": "Liste des cibles", "targetsList": "Liste des cibles",
"advancedMode": "Mode Avancé",
"targetErrorDuplicateTargetFound": "Cible en double trouvée", "targetErrorDuplicateTargetFound": "Cible en double trouvée",
"healthCheckHealthy": "Sain", "healthCheckHealthy": "Sain",
"healthCheckUnhealthy": "En mauvaise santé", "healthCheckUnhealthy": "En mauvaise santé",
@@ -1543,8 +1557,8 @@
"autoLoginError": "Erreur de connexion automatique", "autoLoginError": "Erreur de connexion automatique",
"autoLoginErrorNoRedirectUrl": "Aucune URL de redirection reçue du fournisseur d'identité.", "autoLoginErrorNoRedirectUrl": "Aucune URL de redirection reçue du fournisseur d'identité.",
"autoLoginErrorGeneratingUrl": "Échec de la génération de l'URL d'authentification.", "autoLoginErrorGeneratingUrl": "Échec de la génération de l'URL d'authentification.",
"remoteExitNodeManageRemoteExitNodes": "Gérer auto-hébergé", "remoteExitNodeManageRemoteExitNodes": "Nœuds distants",
"remoteExitNodeDescription": "Gérer les nœuds pour étendre votre connectivité réseau", "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
"remoteExitNodes": "Nœuds", "remoteExitNodes": "Nœuds",
"searchRemoteExitNodes": "Rechercher des nœuds...", "searchRemoteExitNodes": "Rechercher des nœuds...",
"remoteExitNodeAdd": "Ajouter un noeud", "remoteExitNodeAdd": "Ajouter un noeud",
@@ -1554,7 +1568,7 @@
"remoteExitNodeMessageConfirm": "Pour confirmer, veuillez saisir le nom du noeud ci-dessous.", "remoteExitNodeMessageConfirm": "Pour confirmer, veuillez saisir le nom du noeud ci-dessous.",
"remoteExitNodeConfirmDelete": "Confirmer la suppression du noeud", "remoteExitNodeConfirmDelete": "Confirmer la suppression du noeud",
"remoteExitNodeDelete": "Supprimer le noeud", "remoteExitNodeDelete": "Supprimer le noeud",
"sidebarRemoteExitNodes": "Nœuds", "sidebarRemoteExitNodes": "Nœuds distants",
"remoteExitNodeCreate": { "remoteExitNodeCreate": {
"title": "Créer un noeud", "title": "Créer un noeud",
"description": "Créer un nouveau nœud pour étendre votre connectivité réseau", "description": "Créer un nouveau nœud pour étendre votre connectivité réseau",
@@ -1723,5 +1737,161 @@
"authPageUpdated": "Page d\u000027authentification mise à jour avec succès", "authPageUpdated": "Page d\u000027authentification mise à jour avec succès",
"healthCheckNotAvailable": "Locale", "healthCheckNotAvailable": "Locale",
"rewritePath": "Réécrire le chemin", "rewritePath": "Réécrire le chemin",
"rewritePathDescription": "Réécrivez éventuellement le chemin avant de le transmettre à la cible." "rewritePathDescription": "Réécrivez éventuellement le chemin avant de le transmettre à la cible.",
"continueToApplication": "Continuer vers l'application",
"checkingInvite": "Vérification de l'invitation",
"setResourceHeaderAuth": "Définir l\\'authentification d\\'en-tête de la ressource",
"resourceHeaderAuthRemove": "Supprimer l'authentification de l'en-tête",
"resourceHeaderAuthRemoveDescription": "Authentification de l'en-tête supprimée avec succès.",
"resourceErrorHeaderAuthRemove": "Échec de la suppression de l'authentification de l'en-tête",
"resourceErrorHeaderAuthRemoveDescription": "Impossible de supprimer l'authentification de l'en-tête de la ressource.",
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
"headerAuthRemove": "Remove Header Auth",
"headerAuthAdd": "Add Header Auth",
"resourceErrorHeaderAuthSetup": "Impossible de définir l'authentification de l'en-tête",
"resourceErrorHeaderAuthSetupDescription": "Impossible de définir l'authentification de l'en-tête pour la ressource.",
"resourceHeaderAuthSetup": "Authentification de l'en-tête définie avec succès",
"resourceHeaderAuthSetupDescription": "L'authentification de l'en-tête a été définie avec succès.",
"resourceHeaderAuthSetupTitle": "Authentification de l'en-tête",
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
"resourceHeaderAuthSubmit": "Authentification de l'en-tête",
"actionSetResourceHeaderAuth": "Authentification de l'en-tête",
"enterpriseEdition": "Enterprise Edition",
"unlicensed": "Unlicensed",
"beta": "Beta",
"manageClients": "Manage Clients",
"manageClientsDescription": "Clients are devices that can connect to your sites",
"licenseTableValidUntil": "Valid Until",
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
"sidebarEnterpriseLicenses": "Licenses",
"generateLicenseKey": "Generate License Key",
"generateLicenseKeyForm": {
"validation": {
"emailRequired": "Please enter a valid email address",
"useCaseTypeRequired": "Please select a use case type",
"firstNameRequired": "First name is required",
"lastNameRequired": "Last name is required",
"primaryUseRequired": "Please describe your primary use",
"jobTitleRequiredBusiness": "Job title is required for business use",
"industryRequiredBusiness": "Industry is required for business use",
"stateProvinceRegionRequired": "State/Province/Region is required",
"postalZipCodeRequired": "Postal/ZIP Code is required",
"companyNameRequiredBusiness": "Company name is required for business use",
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
"countryRequiredPersonal": "Country is required for personal use",
"agreeToTermsRequired": "You must agree to the terms",
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
},
"useCaseOptions": {
"personal": {
"title": "Personal Use",
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
},
"business": {
"title": "Business Use",
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
}
},
"steps": {
"emailLicenseType": {
"title": "Email & License Type",
"description": "Enter your email and choose your license type"
},
"personalInformation": {
"title": "Personal Information",
"description": "Tell us about yourself"
},
"contactInformation": {
"title": "Contact Information",
"description": "Your contact details"
},
"termsGenerate": {
"title": "Terms & Generate",
"description": "Review and accept terms to generate your license"
}
},
"alerts": {
"commercialUseDisclosure": {
"title": "Usage Disclosure",
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
},
"trialPeriodInformation": {
"title": "Trial Period Information",
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
}
},
"form": {
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
"firstName": "First Name",
"lastName": "Last Name",
"jobTitle": "Job Title",
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
"industryQuestion": "What is your industry?",
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
"companyName": "Company name",
"countryOfResidence": "Country of residence",
"stateProvinceRegion": "State / Province / Region",
"postalZipCode": "Postal / ZIP Code",
"companyWebsite": "Company website",
"companyPhoneNumber": "Company phone number",
"country": "Country",
"phoneNumberOptional": "Phone number (optional)",
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
},
"buttons": {
"close": "Close",
"previous": "Previous",
"next": "Next",
"generateLicenseKey": "Generate License Key"
},
"toasts": {
"success": {
"title": "License key generated successfully",
"description": "Your license key has been generated and is ready to use."
},
"error": {
"title": "Failed to generate license key",
"description": "An error occurred while generating the license key."
}
}
},
"priority": "Priorité",
"priorityDescription": "Les routes de haute priorité sont évaluées en premier. La priorité = 100 signifie l'ordre automatique (décision du système). Utilisez un autre nombre pour imposer la priorité manuelle.",
"instanceName": "Instance Name",
"pathMatchModalTitle": "Configure Path Matching",
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
"pathMatchType": "Match Type",
"pathMatchPrefix": "Prefix",
"pathMatchExact": "Exact",
"pathMatchRegex": "Regex",
"pathMatchValue": "Path Value",
"clear": "Clear",
"saveChanges": "Save Changes",
"pathMatchRegexPlaceholder": "^/api/.*",
"pathMatchDefaultPlaceholder": "/path",
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
"pathMatchExactHelp": "Example: /api matches only /api",
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
"pathRewriteModalTitle": "Configure Path Rewriting",
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
"pathRewriteType": "Rewrite Type",
"pathRewritePrefixOption": "Prefix - Replace prefix",
"pathRewriteExactOption": "Exact - Replace entire path",
"pathRewriteRegexOption": "Regex - Pattern replacement",
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
"pathRewriteValue": "Rewrite Value",
"pathRewriteRegexPlaceholder": "/new/$1",
"pathRewriteDefaultPlaceholder": "/new-path",
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
"pathRewritePrefix": "Prefix",
"pathRewriteExact": "Exact",
"pathRewriteRegex": "Regex",
"pathRewriteStrip": "Strip",
"pathRewriteStripLabel": "strip"
} }

View File

@@ -96,7 +96,7 @@
"siteWgDescription": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.", "siteWgDescription": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.",
"siteWgDescriptionSaas": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta. FUNZIONA SOLO SU NODI AUTO-OSPITATI", "siteWgDescriptionSaas": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta. FUNZIONA SOLO SU NODI AUTO-OSPITATI",
"siteLocalDescription": "Solo risorse locali. Nessun tunneling.", "siteLocalDescription": "Solo risorse locali. Nessun tunneling.",
"siteLocalDescriptionSaas": "Solo risorse locali. Nessun tunneling. FUNZIONA SOLO SU NODI AUTO-OSPITATI", "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
"siteSeeAll": "Vedi Tutti I Siti", "siteSeeAll": "Vedi Tutti I Siti",
"siteTunnelDescription": "Determina come vuoi connetterti al tuo sito", "siteTunnelDescription": "Determina come vuoi connetterti al tuo sito",
"siteNewtCredentials": "Credenziali Newt", "siteNewtCredentials": "Credenziali Newt",
@@ -468,7 +468,10 @@
"createdAt": "Creato Il", "createdAt": "Creato Il",
"proxyErrorInvalidHeader": "Valore dell'intestazione Host personalizzata non valido. Usa il formato nome dominio o salva vuoto per rimuovere l'intestazione Host personalizzata.", "proxyErrorInvalidHeader": "Valore dell'intestazione Host personalizzata non valido. Usa il formato nome dominio o salva vuoto per rimuovere l'intestazione Host personalizzata.",
"proxyErrorTls": "Nome Server TLS non valido. Usa il formato nome dominio o salva vuoto per rimuovere il Nome Server TLS.", "proxyErrorTls": "Nome Server TLS non valido. Usa il formato nome dominio o salva vuoto per rimuovere il Nome Server TLS.",
"proxyEnableSSL": "Abilita SSL (https)", "proxyEnableSSL": "Abilita SSL",
"proxyEnableSSLDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure ai tuoi obiettivi.",
"target": "Target",
"configureTarget": "Configura Obiettivi",
"targetErrorFetch": "Impossibile recuperare i target", "targetErrorFetch": "Impossibile recuperare i target",
"targetErrorFetchDescription": "Si è verificato un errore durante il recupero dei target", "targetErrorFetchDescription": "Si è verificato un errore durante il recupero dei target",
"siteErrorFetch": "Impossibile recuperare la risorsa", "siteErrorFetch": "Impossibile recuperare la risorsa",
@@ -495,7 +498,7 @@
"targetTlsSettings": "Configurazione Connessione Sicura", "targetTlsSettings": "Configurazione Connessione Sicura",
"targetTlsSettingsDescription": "Configura le impostazioni SSL/TLS per la tua risorsa", "targetTlsSettingsDescription": "Configura le impostazioni SSL/TLS per la tua risorsa",
"targetTlsSettingsAdvanced": "Impostazioni TLS Avanzate", "targetTlsSettingsAdvanced": "Impostazioni TLS Avanzate",
"targetTlsSni": "Nome Server TLS (SNI)", "targetTlsSni": "Nome Server Tls",
"targetTlsSniDescription": "Il Nome Server TLS da usare per SNI. Lascia vuoto per usare quello predefinito.", "targetTlsSniDescription": "Il Nome Server TLS da usare per SNI. Lascia vuoto per usare quello predefinito.",
"targetTlsSubmit": "Salva Impostazioni", "targetTlsSubmit": "Salva Impostazioni",
"targets": "Configurazione Target", "targets": "Configurazione Target",
@@ -504,9 +507,21 @@
"targetStickySessionsDescription": "Mantieni le connessioni sullo stesso target backend per l'intera sessione.", "targetStickySessionsDescription": "Mantieni le connessioni sullo stesso target backend per l'intera sessione.",
"methodSelect": "Seleziona metodo", "methodSelect": "Seleziona metodo",
"targetSubmit": "Aggiungi Target", "targetSubmit": "Aggiungi Target",
"targetNoOne": "Nessun target. Aggiungi un target usando il modulo.", "targetNoOne": "Questa risorsa non ha bersagli. Aggiungi un obiettivo per configurare dove inviare le richieste al tuo backend.",
"targetNoOneDescription": "L'aggiunta di più di un target abiliterà il bilanciamento del carico.", "targetNoOneDescription": "L'aggiunta di più di un target abiliterà il bilanciamento del carico.",
"targetsSubmit": "Salva Target", "targetsSubmit": "Salva Target",
"addTarget": "Aggiungi Target",
"targetErrorInvalidIp": "Indirizzo IP non valido",
"targetErrorInvalidIpDescription": "Inserisci un indirizzo IP o un hostname valido",
"targetErrorInvalidPort": "Porta non valida",
"targetErrorInvalidPortDescription": "Inserisci un numero di porta valido",
"targetErrorNoSite": "Nessun sito selezionato",
"targetErrorNoSiteDescription": "Si prega di selezionare un sito per l'obiettivo",
"targetCreated": "Destinazione creata",
"targetCreatedDescription": "L'obiettivo è stato creato con successo",
"targetErrorCreate": "Impossibile creare l'obiettivo",
"targetErrorCreateDescription": "Si è verificato un errore durante la creazione del target",
"save": "Salva",
"proxyAdditional": "Impostazioni Proxy Aggiuntive", "proxyAdditional": "Impostazioni Proxy Aggiuntive",
"proxyAdditionalDescription": "Configura come la tua risorsa gestisce le impostazioni proxy", "proxyAdditionalDescription": "Configura come la tua risorsa gestisce le impostazioni proxy",
"proxyCustomHeader": "Intestazione Host Personalizzata", "proxyCustomHeader": "Intestazione Host Personalizzata",
@@ -715,7 +730,7 @@
"pangolinServerAdmin": "Server Admin - Pangolina", "pangolinServerAdmin": "Server Admin - Pangolina",
"licenseTierProfessional": "Licenza Professional", "licenseTierProfessional": "Licenza Professional",
"licenseTierEnterprise": "Licenza Enterprise", "licenseTierEnterprise": "Licenza Enterprise",
"licenseTierCommercial": "Licenza Commerciale", "licenseTierPersonal": "Personal License",
"licensed": "Con Licenza", "licensed": "Con Licenza",
"yes": "Sì", "yes": "Sì",
"no": "No", "no": "No",
@@ -750,7 +765,7 @@
"idpDisplayName": "Un nome visualizzato per questo provider di identità", "idpDisplayName": "Un nome visualizzato per questo provider di identità",
"idpAutoProvisionUsers": "Provisioning Automatico Utenti", "idpAutoProvisionUsers": "Provisioning Automatico Utenti",
"idpAutoProvisionUsersDescription": "Quando abilitato, gli utenti verranno creati automaticamente nel sistema al primo accesso con la possibilità di mappare gli utenti a ruoli e organizzazioni.", "idpAutoProvisionUsersDescription": "Quando abilitato, gli utenti verranno creati automaticamente nel sistema al primo accesso con la possibilità di mappare gli utenti a ruoli e organizzazioni.",
"licenseBadge": "Professionista", "licenseBadge": "EE",
"idpType": "Tipo di Provider", "idpType": "Tipo di Provider",
"idpTypeDescription": "Seleziona il tipo di provider di identità che desideri configurare", "idpTypeDescription": "Seleziona il tipo di provider di identità che desideri configurare",
"idpOidcConfigure": "Configurazione OAuth2/OIDC", "idpOidcConfigure": "Configurazione OAuth2/OIDC",
@@ -1084,7 +1099,6 @@
"navbar": "Menu di Navigazione", "navbar": "Menu di Navigazione",
"navbarDescription": "Menu di navigazione principale dell'applicazione", "navbarDescription": "Menu di navigazione principale dell'applicazione",
"navbarDocsLink": "Documentazione", "navbarDocsLink": "Documentazione",
"commercialEdition": "Edizione Commerciale",
"otpErrorEnable": "Impossibile abilitare 2FA", "otpErrorEnable": "Impossibile abilitare 2FA",
"otpErrorEnableDescription": "Si è verificato un errore durante l'abilitazione di 2FA", "otpErrorEnableDescription": "Si è verificato un errore durante l'abilitazione di 2FA",
"otpSetupCheckCode": "Inserisci un codice a 6 cifre", "otpSetupCheckCode": "Inserisci un codice a 6 cifre",
@@ -1140,7 +1154,7 @@
"sidebarAllUsers": "Tutti Gli Utenti", "sidebarAllUsers": "Tutti Gli Utenti",
"sidebarIdentityProviders": "Fornitori Di Identità", "sidebarIdentityProviders": "Fornitori Di Identità",
"sidebarLicense": "Licenza", "sidebarLicense": "Licenza",
"sidebarClients": "Clienti (Beta)", "sidebarClients": "Clients",
"sidebarDomains": "Domini", "sidebarDomains": "Domini",
"enableDockerSocket": "Abilita Progetto Docker", "enableDockerSocket": "Abilita Progetto Docker",
"enableDockerSocketDescription": "Abilita la raschiatura dell'etichetta Docker Socket per le etichette dei progetti. Il percorso del socket deve essere fornito a Newt.", "enableDockerSocketDescription": "Abilita la raschiatura dell'etichetta Docker Socket per le etichette dei progetti. Il percorso del socket deve essere fornito a Newt.",
@@ -1333,7 +1347,6 @@
"twoFactorRequired": "È richiesta l'autenticazione a due fattori per registrare una chiave di sicurezza.", "twoFactorRequired": "È richiesta l'autenticazione a due fattori per registrare una chiave di sicurezza.",
"twoFactor": "Autenticazione a Due Fattori", "twoFactor": "Autenticazione a Due Fattori",
"adminEnabled2FaOnYourAccount": "Il tuo amministratore ha abilitato l'autenticazione a due fattori per {email}. Completa il processo di configurazione per continuare.", "adminEnabled2FaOnYourAccount": "Il tuo amministratore ha abilitato l'autenticazione a due fattori per {email}. Completa il processo di configurazione per continuare.",
"continueToApplication": "Continua con l'applicazione",
"securityKeyAdd": "Aggiungi Chiave di Sicurezza", "securityKeyAdd": "Aggiungi Chiave di Sicurezza",
"securityKeyRegisterTitle": "Registra Nuova Chiave di Sicurezza", "securityKeyRegisterTitle": "Registra Nuova Chiave di Sicurezza",
"securityKeyRegisterDescription": "Collega la tua chiave di sicurezza e inserisci un nome per identificarla", "securityKeyRegisterDescription": "Collega la tua chiave di sicurezza e inserisci un nome per identificarla",
@@ -1411,6 +1424,7 @@
"externalProxyEnabled": "Proxy Esterno Abilitato", "externalProxyEnabled": "Proxy Esterno Abilitato",
"addNewTarget": "Aggiungi Nuovo Target", "addNewTarget": "Aggiungi Nuovo Target",
"targetsList": "Elenco dei Target", "targetsList": "Elenco dei Target",
"advancedMode": "Modalità Avanzata",
"targetErrorDuplicateTargetFound": "Target duplicato trovato", "targetErrorDuplicateTargetFound": "Target duplicato trovato",
"healthCheckHealthy": "Sano", "healthCheckHealthy": "Sano",
"healthCheckUnhealthy": "Non Sano", "healthCheckUnhealthy": "Non Sano",
@@ -1543,8 +1557,8 @@
"autoLoginError": "Errore di Accesso Automatico", "autoLoginError": "Errore di Accesso Automatico",
"autoLoginErrorNoRedirectUrl": "Nessun URL di reindirizzamento ricevuto dal provider di identità.", "autoLoginErrorNoRedirectUrl": "Nessun URL di reindirizzamento ricevuto dal provider di identità.",
"autoLoginErrorGeneratingUrl": "Impossibile generare l'URL di autenticazione.", "autoLoginErrorGeneratingUrl": "Impossibile generare l'URL di autenticazione.",
"remoteExitNodeManageRemoteExitNodes": "Gestisci Self-Hosted", "remoteExitNodeManageRemoteExitNodes": "Nodi Remoti",
"remoteExitNodeDescription": "Gestisci i nodi per estendere la connettività di rete", "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
"remoteExitNodes": "Nodi", "remoteExitNodes": "Nodi",
"searchRemoteExitNodes": "Cerca nodi...", "searchRemoteExitNodes": "Cerca nodi...",
"remoteExitNodeAdd": "Aggiungi Nodo", "remoteExitNodeAdd": "Aggiungi Nodo",
@@ -1554,7 +1568,7 @@
"remoteExitNodeMessageConfirm": "Per confermare, digita il nome del nodo qui sotto.", "remoteExitNodeMessageConfirm": "Per confermare, digita il nome del nodo qui sotto.",
"remoteExitNodeConfirmDelete": "Conferma Eliminazione Nodo", "remoteExitNodeConfirmDelete": "Conferma Eliminazione Nodo",
"remoteExitNodeDelete": "Elimina Nodo", "remoteExitNodeDelete": "Elimina Nodo",
"sidebarRemoteExitNodes": "Nodi", "sidebarRemoteExitNodes": "Nodi Remoti",
"remoteExitNodeCreate": { "remoteExitNodeCreate": {
"title": "Crea Nodo", "title": "Crea Nodo",
"description": "Crea un nuovo nodo per estendere la connettività di rete", "description": "Crea un nuovo nodo per estendere la connettività di rete",
@@ -1723,5 +1737,161 @@
"authPageUpdated": "Pagina di autenticazione aggiornata con successo", "authPageUpdated": "Pagina di autenticazione aggiornata con successo",
"healthCheckNotAvailable": "Locale", "healthCheckNotAvailable": "Locale",
"rewritePath": "Riscrivi percorso", "rewritePath": "Riscrivi percorso",
"rewritePathDescription": "Riscrivi eventualmente il percorso prima di inoltrarlo al target." "rewritePathDescription": "Riscrivi eventualmente il percorso prima di inoltrarlo al target.",
"continueToApplication": "Continua con l'applicazione",
"checkingInvite": "Controllo Invito",
"setResourceHeaderAuth": "setResourceHeaderAuth",
"resourceHeaderAuthRemove": "Rimuovi Autenticazione Intestazione",
"resourceHeaderAuthRemoveDescription": "Autenticazione intestazione rimossa con successo.",
"resourceErrorHeaderAuthRemove": "Impossibile rimuovere l'autenticazione dell'intestazione",
"resourceErrorHeaderAuthRemoveDescription": "Impossibile rimuovere l'autenticazione dell'intestazione per la risorsa.",
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
"headerAuthRemove": "Remove Header Auth",
"headerAuthAdd": "Add Header Auth",
"resourceErrorHeaderAuthSetup": "Impossibile impostare l'autenticazione dell'intestazione",
"resourceErrorHeaderAuthSetupDescription": "Impossibile impostare l'autenticazione dell'intestazione per la risorsa.",
"resourceHeaderAuthSetup": "Autenticazione intestazione impostata con successo",
"resourceHeaderAuthSetupDescription": "L'autenticazione dell'intestazione è stata impostata correttamente.",
"resourceHeaderAuthSetupTitle": "Imposta Autenticazione Intestazione",
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
"resourceHeaderAuthSubmit": "Imposta Autenticazione Intestazione",
"actionSetResourceHeaderAuth": "Imposta Autenticazione Intestazione",
"enterpriseEdition": "Enterprise Edition",
"unlicensed": "Unlicensed",
"beta": "Beta",
"manageClients": "Manage Clients",
"manageClientsDescription": "Clients are devices that can connect to your sites",
"licenseTableValidUntil": "Valid Until",
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
"sidebarEnterpriseLicenses": "Licenses",
"generateLicenseKey": "Generate License Key",
"generateLicenseKeyForm": {
"validation": {
"emailRequired": "Please enter a valid email address",
"useCaseTypeRequired": "Please select a use case type",
"firstNameRequired": "First name is required",
"lastNameRequired": "Last name is required",
"primaryUseRequired": "Please describe your primary use",
"jobTitleRequiredBusiness": "Job title is required for business use",
"industryRequiredBusiness": "Industry is required for business use",
"stateProvinceRegionRequired": "State/Province/Region is required",
"postalZipCodeRequired": "Postal/ZIP Code is required",
"companyNameRequiredBusiness": "Company name is required for business use",
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
"countryRequiredPersonal": "Country is required for personal use",
"agreeToTermsRequired": "You must agree to the terms",
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
},
"useCaseOptions": {
"personal": {
"title": "Personal Use",
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
},
"business": {
"title": "Business Use",
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
}
},
"steps": {
"emailLicenseType": {
"title": "Email & License Type",
"description": "Enter your email and choose your license type"
},
"personalInformation": {
"title": "Personal Information",
"description": "Tell us about yourself"
},
"contactInformation": {
"title": "Contact Information",
"description": "Your contact details"
},
"termsGenerate": {
"title": "Terms & Generate",
"description": "Review and accept terms to generate your license"
}
},
"alerts": {
"commercialUseDisclosure": {
"title": "Usage Disclosure",
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
},
"trialPeriodInformation": {
"title": "Trial Period Information",
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
}
},
"form": {
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
"firstName": "First Name",
"lastName": "Last Name",
"jobTitle": "Job Title",
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
"industryQuestion": "What is your industry?",
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
"companyName": "Company name",
"countryOfResidence": "Country of residence",
"stateProvinceRegion": "State / Province / Region",
"postalZipCode": "Postal / ZIP Code",
"companyWebsite": "Company website",
"companyPhoneNumber": "Company phone number",
"country": "Country",
"phoneNumberOptional": "Phone number (optional)",
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
},
"buttons": {
"close": "Close",
"previous": "Previous",
"next": "Next",
"generateLicenseKey": "Generate License Key"
},
"toasts": {
"success": {
"title": "License key generated successfully",
"description": "Your license key has been generated and is ready to use."
},
"error": {
"title": "Failed to generate license key",
"description": "An error occurred while generating the license key."
}
}
},
"priority": "Priorità",
"priorityDescription": "I percorsi prioritari più alti sono valutati prima. Priorità = 100 significa ordinamento automatico (decidi di sistema). Usa un altro numero per applicare la priorità manuale.",
"instanceName": "Instance Name",
"pathMatchModalTitle": "Configure Path Matching",
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
"pathMatchType": "Match Type",
"pathMatchPrefix": "Prefix",
"pathMatchExact": "Exact",
"pathMatchRegex": "Regex",
"pathMatchValue": "Path Value",
"clear": "Clear",
"saveChanges": "Save Changes",
"pathMatchRegexPlaceholder": "^/api/.*",
"pathMatchDefaultPlaceholder": "/path",
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
"pathMatchExactHelp": "Example: /api matches only /api",
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
"pathRewriteModalTitle": "Configure Path Rewriting",
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
"pathRewriteType": "Rewrite Type",
"pathRewritePrefixOption": "Prefix - Replace prefix",
"pathRewriteExactOption": "Exact - Replace entire path",
"pathRewriteRegexOption": "Regex - Pattern replacement",
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
"pathRewriteValue": "Rewrite Value",
"pathRewriteRegexPlaceholder": "/new/$1",
"pathRewriteDefaultPlaceholder": "/new-path",
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
"pathRewritePrefix": "Prefix",
"pathRewriteExact": "Exact",
"pathRewriteRegex": "Regex",
"pathRewriteStrip": "Strip",
"pathRewriteStripLabel": "strip"
} }

View File

@@ -96,7 +96,7 @@
"siteWgDescription": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다.", "siteWgDescription": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다.",
"siteWgDescriptionSaas": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다. 자체 호스팅 노드에서만 작동합니다.", "siteWgDescriptionSaas": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다. 자체 호스팅 노드에서만 작동합니다.",
"siteLocalDescription": "로컬 리소스만 사용 가능합니다. 터널링이 없습니다.", "siteLocalDescription": "로컬 리소스만 사용 가능합니다. 터널링이 없습니다.",
"siteLocalDescriptionSaas": "로컬 리소스만. 터널링 없음. 자체 호스팅 노드에서만 작동합니다.", "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
"siteSeeAll": "모든 사이트 보기", "siteSeeAll": "모든 사이트 보기",
"siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요", "siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요",
"siteNewtCredentials": "Newt 자격 증명", "siteNewtCredentials": "Newt 자격 증명",
@@ -468,7 +468,10 @@
"createdAt": "생성일", "createdAt": "생성일",
"proxyErrorInvalidHeader": "잘못된 사용자 정의 호스트 헤더 값입니다. 도메인 이름 형식을 사용하거나 사용자 정의 호스트 헤더를 해제하려면 비워 두십시오.", "proxyErrorInvalidHeader": "잘못된 사용자 정의 호스트 헤더 값입니다. 도메인 이름 형식을 사용하거나 사용자 정의 호스트 헤더를 해제하려면 비워 두십시오.",
"proxyErrorTls": "유효하지 않은 TLS 서버 이름입니다. 도메인 이름 형식을 사용하거나 비워 두어 TLS 서버 이름을 제거하십시오.", "proxyErrorTls": "유효하지 않은 TLS 서버 이름입니다. 도메인 이름 형식을 사용하거나 비워 두어 TLS 서버 이름을 제거하십시오.",
"proxyEnableSSL": "SSL 활성화 (https)", "proxyEnableSSL": "SSL 활성화",
"proxyEnableSSLDescription": "대상에 대한 안전한 HTTPS 연결을 위해 SSL/TLS 암호화를 활성화하세요.",
"target": "대상",
"configureTarget": "대상 구성",
"targetErrorFetch": "대상 가져오는 데 실패했습니다.", "targetErrorFetch": "대상 가져오는 데 실패했습니다.",
"targetErrorFetchDescription": "대상 가져오는 중 오류가 발생했습니다", "targetErrorFetchDescription": "대상 가져오는 중 오류가 발생했습니다",
"siteErrorFetch": "리소스를 가져오는 데 실패했습니다", "siteErrorFetch": "리소스를 가져오는 데 실패했습니다",
@@ -495,7 +498,7 @@
"targetTlsSettings": "보안 연결 구성", "targetTlsSettings": "보안 연결 구성",
"targetTlsSettingsDescription": "리소스에 대한 SSL/TLS 설정 구성", "targetTlsSettingsDescription": "리소스에 대한 SSL/TLS 설정 구성",
"targetTlsSettingsAdvanced": "고급 TLS 설정", "targetTlsSettingsAdvanced": "고급 TLS 설정",
"targetTlsSni": "TLS 서버 이름 (SNI)", "targetTlsSni": "TLS 서버 이름",
"targetTlsSniDescription": "SNI에 사용할 TLS 서버 이름. 기본값을 사용하려면 비워 두십시오.", "targetTlsSniDescription": "SNI에 사용할 TLS 서버 이름. 기본값을 사용하려면 비워 두십시오.",
"targetTlsSubmit": "설정 저장", "targetTlsSubmit": "설정 저장",
"targets": "대상 구성", "targets": "대상 구성",
@@ -504,9 +507,21 @@
"targetStickySessionsDescription": "세션 전체 동안 동일한 백엔드 대상을 유지합니다.", "targetStickySessionsDescription": "세션 전체 동안 동일한 백엔드 대상을 유지합니다.",
"methodSelect": "선택 방법", "methodSelect": "선택 방법",
"targetSubmit": "대상 추가", "targetSubmit": "대상 추가",
"targetNoOne": "대상이 없습니다. 양식을 사용하여 대상을 추가하세요.", "targetNoOne": "이 리소스에는 대상이 없습니다. 백엔드로 요청을 보내려면 대상을 추가하세요.",
"targetNoOneDescription": "위에 하나 이상의 대상을 추가하면 로드 밸런싱이 활성화됩니다.", "targetNoOneDescription": "위에 하나 이상의 대상을 추가하면 로드 밸런싱이 활성화됩니다.",
"targetsSubmit": "대상 저장", "targetsSubmit": "대상 저장",
"addTarget": "대상 추가",
"targetErrorInvalidIp": "유효하지 않은 IP 주소",
"targetErrorInvalidIpDescription": "유효한 IP 주소 또는 호스트 이름을 입력하세요.",
"targetErrorInvalidPort": "유효하지 않은 포트",
"targetErrorInvalidPortDescription": "유효한 포트 번호를 입력하세요.",
"targetErrorNoSite": "선택된 사이트 없음",
"targetErrorNoSiteDescription": "대상을 위해 사이트를 선택하세요.",
"targetCreated": "대상 생성",
"targetCreatedDescription": "대상이 성공적으로 생성되었습니다.",
"targetErrorCreate": "대상 생성 실패",
"targetErrorCreateDescription": "대상 생성 중 오류가 발생했습니다.",
"save": "저장",
"proxyAdditional": "추가 프록시 설정", "proxyAdditional": "추가 프록시 설정",
"proxyAdditionalDescription": "리소스가 프록시 설정을 처리하는 방법 구성", "proxyAdditionalDescription": "리소스가 프록시 설정을 처리하는 방법 구성",
"proxyCustomHeader": "사용자 정의 호스트 헤더", "proxyCustomHeader": "사용자 정의 호스트 헤더",
@@ -715,7 +730,7 @@
"pangolinServerAdmin": "서버 관리자 - 판골린", "pangolinServerAdmin": "서버 관리자 - 판골린",
"licenseTierProfessional": "전문 라이센스", "licenseTierProfessional": "전문 라이센스",
"licenseTierEnterprise": "기업 라이선스", "licenseTierEnterprise": "기업 라이선스",
"licenseTierCommercial": "상업용 라이선스", "licenseTierPersonal": "Personal License",
"licensed": "라이센스", "licensed": "라이센스",
"yes": "예", "yes": "예",
"no": "아니요", "no": "아니요",
@@ -750,7 +765,7 @@
"idpDisplayName": "이 신원 공급자를 위한 표시 이름", "idpDisplayName": "이 신원 공급자를 위한 표시 이름",
"idpAutoProvisionUsers": "사용자 자동 프로비저닝", "idpAutoProvisionUsers": "사용자 자동 프로비저닝",
"idpAutoProvisionUsersDescription": "활성화되면 사용자가 첫 로그인 시 시스템에 자동으로 생성되며, 사용자와 역할 및 조직을 매핑할 수 있습니다.", "idpAutoProvisionUsersDescription": "활성화되면 사용자가 첫 로그인 시 시스템에 자동으로 생성되며, 사용자와 역할 및 조직을 매핑할 수 있습니다.",
"licenseBadge": "전문가", "licenseBadge": "EE",
"idpType": "제공자 유형", "idpType": "제공자 유형",
"idpTypeDescription": "구성할 ID 공급자의 유형을 선택하십시오.", "idpTypeDescription": "구성할 ID 공급자의 유형을 선택하십시오.",
"idpOidcConfigure": "OAuth2/OIDC 구성", "idpOidcConfigure": "OAuth2/OIDC 구성",
@@ -1084,7 +1099,6 @@
"navbar": "탐색 메뉴", "navbar": "탐색 메뉴",
"navbarDescription": "애플리케이션의 주요 탐색 메뉴", "navbarDescription": "애플리케이션의 주요 탐색 메뉴",
"navbarDocsLink": "문서", "navbarDocsLink": "문서",
"commercialEdition": "상업용 에디션",
"otpErrorEnable": "2FA를 활성화할 수 없습니다.", "otpErrorEnable": "2FA를 활성화할 수 없습니다.",
"otpErrorEnableDescription": "2FA를 활성화하는 동안 오류가 발생했습니다", "otpErrorEnableDescription": "2FA를 활성화하는 동안 오류가 발생했습니다",
"otpSetupCheckCode": "6자리 코드를 입력하세요", "otpSetupCheckCode": "6자리 코드를 입력하세요",
@@ -1140,7 +1154,7 @@
"sidebarAllUsers": "모든 사용자", "sidebarAllUsers": "모든 사용자",
"sidebarIdentityProviders": "신원 공급자", "sidebarIdentityProviders": "신원 공급자",
"sidebarLicense": "라이선스", "sidebarLicense": "라이선스",
"sidebarClients": "클라이언트 (Beta)", "sidebarClients": "Clients",
"sidebarDomains": "도메인", "sidebarDomains": "도메인",
"enableDockerSocket": "Docker 청사진 활성화", "enableDockerSocket": "Docker 청사진 활성화",
"enableDockerSocketDescription": "블루프린트 레이블을 위한 Docker 소켓 레이블 수집을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.", "enableDockerSocketDescription": "블루프린트 레이블을 위한 Docker 소켓 레이블 수집을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.",
@@ -1333,7 +1347,6 @@
"twoFactorRequired": "보안 키를 등록하려면 이중 인증이 필요합니다.", "twoFactorRequired": "보안 키를 등록하려면 이중 인증이 필요합니다.",
"twoFactor": "이중 인증", "twoFactor": "이중 인증",
"adminEnabled2FaOnYourAccount": "관리자가 {email}에 대한 이중 인증을 활성화했습니다. 계속하려면 설정을 완료하세요.", "adminEnabled2FaOnYourAccount": "관리자가 {email}에 대한 이중 인증을 활성화했습니다. 계속하려면 설정을 완료하세요.",
"continueToApplication": "응용 프로그램으로 계속",
"securityKeyAdd": "보안 키 추가", "securityKeyAdd": "보안 키 추가",
"securityKeyRegisterTitle": "새 보안 키 등록", "securityKeyRegisterTitle": "새 보안 키 등록",
"securityKeyRegisterDescription": "보안 키를 연결하고 식별할 이름을 입력하세요.", "securityKeyRegisterDescription": "보안 키를 연결하고 식별할 이름을 입력하세요.",
@@ -1411,6 +1424,7 @@
"externalProxyEnabled": "외부 프록시 활성화됨", "externalProxyEnabled": "외부 프록시 활성화됨",
"addNewTarget": "새 대상 추가", "addNewTarget": "새 대상 추가",
"targetsList": "대상 목록", "targetsList": "대상 목록",
"advancedMode": "고급 모드",
"targetErrorDuplicateTargetFound": "중복 대상 발견", "targetErrorDuplicateTargetFound": "중복 대상 발견",
"healthCheckHealthy": "정상", "healthCheckHealthy": "정상",
"healthCheckUnhealthy": "비정상", "healthCheckUnhealthy": "비정상",
@@ -1543,8 +1557,8 @@
"autoLoginError": "자동 로그인 오류", "autoLoginError": "자동 로그인 오류",
"autoLoginErrorNoRedirectUrl": "ID 공급자로부터 리디렉션 URL을 받지 못했습니다.", "autoLoginErrorNoRedirectUrl": "ID 공급자로부터 리디렉션 URL을 받지 못했습니다.",
"autoLoginErrorGeneratingUrl": "인증 URL 생성 실패.", "autoLoginErrorGeneratingUrl": "인증 URL 생성 실패.",
"remoteExitNodeManageRemoteExitNodes": "관리 자체 호스팅", "remoteExitNodeManageRemoteExitNodes": "원격 노드",
"remoteExitNodeDescription": "네트워크 연결성을 확장하기 위해 노드를 관리하세요", "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
"remoteExitNodes": "노드", "remoteExitNodes": "노드",
"searchRemoteExitNodes": "노드 검색...", "searchRemoteExitNodes": "노드 검색...",
"remoteExitNodeAdd": "노드 추가", "remoteExitNodeAdd": "노드 추가",
@@ -1554,7 +1568,7 @@
"remoteExitNodeMessageConfirm": "확인을 위해 아래에 노드 이름을 입력해 주세요.", "remoteExitNodeMessageConfirm": "확인을 위해 아래에 노드 이름을 입력해 주세요.",
"remoteExitNodeConfirmDelete": "노드 삭제 확인", "remoteExitNodeConfirmDelete": "노드 삭제 확인",
"remoteExitNodeDelete": "노드 삭제", "remoteExitNodeDelete": "노드 삭제",
"sidebarRemoteExitNodes": "노드", "sidebarRemoteExitNodes": "원격 노드",
"remoteExitNodeCreate": { "remoteExitNodeCreate": {
"title": "노드 생성", "title": "노드 생성",
"description": "네트워크 연결성을 확장하기 위해 새 노드를 생성하세요", "description": "네트워크 연결성을 확장하기 위해 새 노드를 생성하세요",
@@ -1723,5 +1737,161 @@
"authPageUpdated": "인증 페이지가 성공적으로 업데이트되었습니다", "authPageUpdated": "인증 페이지가 성공적으로 업데이트되었습니다",
"healthCheckNotAvailable": "로컬", "healthCheckNotAvailable": "로컬",
"rewritePath": "경로 재작성", "rewritePath": "경로 재작성",
"rewritePathDescription": "대상으로 전달하기 전에 경로를 선택적으로 재작성합니다." "rewritePathDescription": "대상으로 전달하기 전에 경로를 선택적으로 재작성합니다.",
"continueToApplication": "응용 프로그램으로 계속",
"checkingInvite": "초대 확인 중",
"setResourceHeaderAuth": "setResourceHeaderAuth",
"resourceHeaderAuthRemove": "헤더 인증 제거",
"resourceHeaderAuthRemoveDescription": "헤더 인증이 성공적으로 제거되었습니다.",
"resourceErrorHeaderAuthRemove": "헤더 인증 제거 실패",
"resourceErrorHeaderAuthRemoveDescription": "리소스의 헤더 인증을 제거할 수 없습니다.",
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
"headerAuthRemove": "Remove Header Auth",
"headerAuthAdd": "Add Header Auth",
"resourceErrorHeaderAuthSetup": "헤더 인증 설정 실패",
"resourceErrorHeaderAuthSetupDescription": "리소스의 헤더 인증을 설정할 수 없습니다.",
"resourceHeaderAuthSetup": "헤더 인증이 성공적으로 설정되었습니다.",
"resourceHeaderAuthSetupDescription": "헤더 인증이 성공적으로 설정되었습니다.",
"resourceHeaderAuthSetupTitle": "헤더 인증 설정",
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
"resourceHeaderAuthSubmit": "헤더 인증 설정",
"actionSetResourceHeaderAuth": "헤더 인증 설정",
"enterpriseEdition": "Enterprise Edition",
"unlicensed": "Unlicensed",
"beta": "Beta",
"manageClients": "Manage Clients",
"manageClientsDescription": "Clients are devices that can connect to your sites",
"licenseTableValidUntil": "Valid Until",
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
"sidebarEnterpriseLicenses": "Licenses",
"generateLicenseKey": "Generate License Key",
"generateLicenseKeyForm": {
"validation": {
"emailRequired": "Please enter a valid email address",
"useCaseTypeRequired": "Please select a use case type",
"firstNameRequired": "First name is required",
"lastNameRequired": "Last name is required",
"primaryUseRequired": "Please describe your primary use",
"jobTitleRequiredBusiness": "Job title is required for business use",
"industryRequiredBusiness": "Industry is required for business use",
"stateProvinceRegionRequired": "State/Province/Region is required",
"postalZipCodeRequired": "Postal/ZIP Code is required",
"companyNameRequiredBusiness": "Company name is required for business use",
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
"countryRequiredPersonal": "Country is required for personal use",
"agreeToTermsRequired": "You must agree to the terms",
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
},
"useCaseOptions": {
"personal": {
"title": "Personal Use",
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
},
"business": {
"title": "Business Use",
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
}
},
"steps": {
"emailLicenseType": {
"title": "Email & License Type",
"description": "Enter your email and choose your license type"
},
"personalInformation": {
"title": "Personal Information",
"description": "Tell us about yourself"
},
"contactInformation": {
"title": "Contact Information",
"description": "Your contact details"
},
"termsGenerate": {
"title": "Terms & Generate",
"description": "Review and accept terms to generate your license"
}
},
"alerts": {
"commercialUseDisclosure": {
"title": "Usage Disclosure",
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
},
"trialPeriodInformation": {
"title": "Trial Period Information",
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
}
},
"form": {
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
"firstName": "First Name",
"lastName": "Last Name",
"jobTitle": "Job Title",
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
"industryQuestion": "What is your industry?",
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
"companyName": "Company name",
"countryOfResidence": "Country of residence",
"stateProvinceRegion": "State / Province / Region",
"postalZipCode": "Postal / ZIP Code",
"companyWebsite": "Company website",
"companyPhoneNumber": "Company phone number",
"country": "Country",
"phoneNumberOptional": "Phone number (optional)",
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
},
"buttons": {
"close": "Close",
"previous": "Previous",
"next": "Next",
"generateLicenseKey": "Generate License Key"
},
"toasts": {
"success": {
"title": "License key generated successfully",
"description": "Your license key has been generated and is ready to use."
},
"error": {
"title": "Failed to generate license key",
"description": "An error occurred while generating the license key."
}
}
},
"priority": "우선순위",
"priorityDescription": "우선 순위가 높은 경로가 먼저 평가됩니다. 우선 순위 = 100은 자동 정렬(시스템 결정)이 의미합니다. 수동 우선 순위를 적용하려면 다른 숫자를 사용하세요.",
"instanceName": "Instance Name",
"pathMatchModalTitle": "Configure Path Matching",
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
"pathMatchType": "Match Type",
"pathMatchPrefix": "Prefix",
"pathMatchExact": "Exact",
"pathMatchRegex": "Regex",
"pathMatchValue": "Path Value",
"clear": "Clear",
"saveChanges": "Save Changes",
"pathMatchRegexPlaceholder": "^/api/.*",
"pathMatchDefaultPlaceholder": "/path",
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
"pathMatchExactHelp": "Example: /api matches only /api",
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
"pathRewriteModalTitle": "Configure Path Rewriting",
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
"pathRewriteType": "Rewrite Type",
"pathRewritePrefixOption": "Prefix - Replace prefix",
"pathRewriteExactOption": "Exact - Replace entire path",
"pathRewriteRegexOption": "Regex - Pattern replacement",
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
"pathRewriteValue": "Rewrite Value",
"pathRewriteRegexPlaceholder": "/new/$1",
"pathRewriteDefaultPlaceholder": "/new-path",
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
"pathRewritePrefix": "Prefix",
"pathRewriteExact": "Exact",
"pathRewriteRegex": "Regex",
"pathRewriteStrip": "Strip",
"pathRewriteStripLabel": "strip"
} }

View File

@@ -96,7 +96,7 @@
"siteWgDescription": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett kreves.", "siteWgDescription": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett kreves.",
"siteWgDescriptionSaas": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett er nødvendig. FUNGERER KUN PÅ SELVHOSTEDE NODER", "siteWgDescriptionSaas": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett er nødvendig. FUNGERER KUN PÅ SELVHOSTEDE NODER",
"siteLocalDescription": "Kun lokale ressurser. Ingen tunnelering.", "siteLocalDescription": "Kun lokale ressurser. Ingen tunnelering.",
"siteLocalDescriptionSaas": "Kun lokale ressurser. Ingen tunneling. FUNGERER KUN PÅ SELVHOSTEDE NODER", "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
"siteSeeAll": "Se alle områder", "siteSeeAll": "Se alle områder",
"siteTunnelDescription": "Bestem hvordan du vil koble deg til ditt område", "siteTunnelDescription": "Bestem hvordan du vil koble deg til ditt område",
"siteNewtCredentials": "Newt påloggingsinformasjon", "siteNewtCredentials": "Newt påloggingsinformasjon",
@@ -468,7 +468,10 @@
"createdAt": "Opprettet", "createdAt": "Opprettet",
"proxyErrorInvalidHeader": "Ugyldig verdi for egendefinert vertsoverskrift. Bruk domenenavnformat, eller lagre tomt for å fjerne den egendefinerte vertsoverskriften.", "proxyErrorInvalidHeader": "Ugyldig verdi for egendefinert vertsoverskrift. Bruk domenenavnformat, eller lagre tomt for å fjerne den egendefinerte vertsoverskriften.",
"proxyErrorTls": "Ugyldig TLS-servernavn. Bruk domenenavnformat, eller la stå tomt for å fjerne TLS-servernavnet.", "proxyErrorTls": "Ugyldig TLS-servernavn. Bruk domenenavnformat, eller la stå tomt for å fjerne TLS-servernavnet.",
"proxyEnableSSL": "Aktiver SSL (https)", "proxyEnableSSL": "Aktiver SSL",
"proxyEnableSSLDescription": "Aktiver SSL/TLS-kryptering for sikre HTTPS-tilkoblinger til dine mål.",
"target": "Target",
"configureTarget": "Konfigurer mål",
"targetErrorFetch": "Kunne ikke hente mål", "targetErrorFetch": "Kunne ikke hente mål",
"targetErrorFetchDescription": "Det oppsto en feil under henting av mål", "targetErrorFetchDescription": "Det oppsto en feil under henting av mål",
"siteErrorFetch": "Klarte ikke å hente ressurs", "siteErrorFetch": "Klarte ikke å hente ressurs",
@@ -495,7 +498,7 @@
"targetTlsSettings": "Sikker tilkoblings-konfigurasjon", "targetTlsSettings": "Sikker tilkoblings-konfigurasjon",
"targetTlsSettingsDescription": "Konfigurer SSL/TLS-innstillinger for ressursen din", "targetTlsSettingsDescription": "Konfigurer SSL/TLS-innstillinger for ressursen din",
"targetTlsSettingsAdvanced": "Avanserte TLS-innstillinger", "targetTlsSettingsAdvanced": "Avanserte TLS-innstillinger",
"targetTlsSni": "TLS Servernavn (SNI)", "targetTlsSni": "TLS servernavn",
"targetTlsSniDescription": "TLS-servernavnet som skal brukes for SNI. La stå tomt for å bruke standardverdien.", "targetTlsSniDescription": "TLS-servernavnet som skal brukes for SNI. La stå tomt for å bruke standardverdien.",
"targetTlsSubmit": "Lagre innstillinger", "targetTlsSubmit": "Lagre innstillinger",
"targets": "Målkonfigurasjon", "targets": "Målkonfigurasjon",
@@ -504,9 +507,21 @@
"targetStickySessionsDescription": "Behold tilkoblinger på samme bakend-mål gjennom hele sesjonen.", "targetStickySessionsDescription": "Behold tilkoblinger på samme bakend-mål gjennom hele sesjonen.",
"methodSelect": "Velg metode", "methodSelect": "Velg metode",
"targetSubmit": "Legg til mål", "targetSubmit": "Legg til mål",
"targetNoOne": "Ingen mål. Legg til et mål ved hjelp av skjemaet.", "targetNoOne": "Denne ressursen har ikke noen mål. Legg til et mål for å konfigurere hvor du vil sende forespørsler til din backend.",
"targetNoOneDescription": "Å legge til mer enn ett mål ovenfor vil aktivere lastbalansering.", "targetNoOneDescription": "Å legge til mer enn ett mål ovenfor vil aktivere lastbalansering.",
"targetsSubmit": "Lagre mål", "targetsSubmit": "Lagre mål",
"addTarget": "Legg til mål",
"targetErrorInvalidIp": "Ugyldig IP-adresse",
"targetErrorInvalidIpDescription": "Skriv inn en gyldig IP-adresse eller vertsnavn",
"targetErrorInvalidPort": "Ugyldig port",
"targetErrorInvalidPortDescription": "Vennligst skriv inn et gyldig portnummer",
"targetErrorNoSite": "Ingen nettsted valgt",
"targetErrorNoSiteDescription": "Velg et nettsted for målet",
"targetCreated": "Mål opprettet",
"targetCreatedDescription": "Målet har blitt opprettet",
"targetErrorCreate": "Kunne ikke opprette målet",
"targetErrorCreateDescription": "Det oppstod en feil under oppretting av målet",
"save": "Lagre",
"proxyAdditional": "Ytterligere Proxy-innstillinger", "proxyAdditional": "Ytterligere Proxy-innstillinger",
"proxyAdditionalDescription": "Konfigurer hvordan ressursen din håndterer proxy-innstillinger", "proxyAdditionalDescription": "Konfigurer hvordan ressursen din håndterer proxy-innstillinger",
"proxyCustomHeader": "Tilpasset verts-header", "proxyCustomHeader": "Tilpasset verts-header",
@@ -715,7 +730,7 @@
"pangolinServerAdmin": "Server Admin - Pangolin", "pangolinServerAdmin": "Server Admin - Pangolin",
"licenseTierProfessional": "Profesjonell lisens", "licenseTierProfessional": "Profesjonell lisens",
"licenseTierEnterprise": "Bedriftslisens", "licenseTierEnterprise": "Bedriftslisens",
"licenseTierCommercial": "Kommersiell lisens", "licenseTierPersonal": "Personal License",
"licensed": "Lisensiert", "licensed": "Lisensiert",
"yes": "Ja", "yes": "Ja",
"no": "Nei", "no": "Nei",
@@ -750,7 +765,7 @@
"idpDisplayName": "Et visningsnavn for denne identitetsleverandøren", "idpDisplayName": "Et visningsnavn for denne identitetsleverandøren",
"idpAutoProvisionUsers": "Automatisk brukerklargjøring", "idpAutoProvisionUsers": "Automatisk brukerklargjøring",
"idpAutoProvisionUsersDescription": "Når aktivert, opprettes brukere automatisk i systemet ved første innlogging, med mulighet til å tilordne brukere til roller og organisasjoner.", "idpAutoProvisionUsersDescription": "Når aktivert, opprettes brukere automatisk i systemet ved første innlogging, med mulighet til å tilordne brukere til roller og organisasjoner.",
"licenseBadge": "Profesjonell", "licenseBadge": "EE",
"idpType": "Leverandørtype", "idpType": "Leverandørtype",
"idpTypeDescription": "Velg typen identitetsleverandør du ønsker å konfigurere", "idpTypeDescription": "Velg typen identitetsleverandør du ønsker å konfigurere",
"idpOidcConfigure": "OAuth2/OIDC-konfigurasjon", "idpOidcConfigure": "OAuth2/OIDC-konfigurasjon",
@@ -1084,7 +1099,6 @@
"navbar": "Navigasjonsmeny", "navbar": "Navigasjonsmeny",
"navbarDescription": "Hovednavigasjonsmeny for applikasjonen", "navbarDescription": "Hovednavigasjonsmeny for applikasjonen",
"navbarDocsLink": "Dokumentasjon", "navbarDocsLink": "Dokumentasjon",
"commercialEdition": "Kommersiell utgave",
"otpErrorEnable": "Kunne ikke aktivere 2FA", "otpErrorEnable": "Kunne ikke aktivere 2FA",
"otpErrorEnableDescription": "En feil oppstod under aktivering av 2FA", "otpErrorEnableDescription": "En feil oppstod under aktivering av 2FA",
"otpSetupCheckCode": "Vennligst skriv inn en 6-sifret kode", "otpSetupCheckCode": "Vennligst skriv inn en 6-sifret kode",
@@ -1140,7 +1154,7 @@
"sidebarAllUsers": "Alle brukere", "sidebarAllUsers": "Alle brukere",
"sidebarIdentityProviders": "Identitetsleverandører", "sidebarIdentityProviders": "Identitetsleverandører",
"sidebarLicense": "Lisens", "sidebarLicense": "Lisens",
"sidebarClients": "Klienter (Beta)", "sidebarClients": "Clients",
"sidebarDomains": "Domener", "sidebarDomains": "Domener",
"enableDockerSocket": "Aktiver Docker blåkopi", "enableDockerSocket": "Aktiver Docker blåkopi",
"enableDockerSocketDescription": "Aktiver skraping av Docker Socket for blueprint Etiketter. Socket bane må brukes for nye.", "enableDockerSocketDescription": "Aktiver skraping av Docker Socket for blueprint Etiketter. Socket bane må brukes for nye.",
@@ -1333,7 +1347,6 @@
"twoFactorRequired": "Tofaktorautentisering er påkrevd for å registrere en sikkerhetsnøkkel.", "twoFactorRequired": "Tofaktorautentisering er påkrevd for å registrere en sikkerhetsnøkkel.",
"twoFactor": "Tofaktorautentisering", "twoFactor": "Tofaktorautentisering",
"adminEnabled2FaOnYourAccount": "Din administrator har aktivert tofaktorautentisering for {email}. Vennligst fullfør oppsettsprosessen for å fortsette.", "adminEnabled2FaOnYourAccount": "Din administrator har aktivert tofaktorautentisering for {email}. Vennligst fullfør oppsettsprosessen for å fortsette.",
"continueToApplication": "Fortsett til applikasjonen",
"securityKeyAdd": "Legg til sikkerhetsnøkkel", "securityKeyAdd": "Legg til sikkerhetsnøkkel",
"securityKeyRegisterTitle": "Registrer ny sikkerhetsnøkkel", "securityKeyRegisterTitle": "Registrer ny sikkerhetsnøkkel",
"securityKeyRegisterDescription": "Koble til sikkerhetsnøkkelen og skriv inn et navn for å identifisere den", "securityKeyRegisterDescription": "Koble til sikkerhetsnøkkelen og skriv inn et navn for å identifisere den",
@@ -1411,6 +1424,7 @@
"externalProxyEnabled": "Ekstern proxy aktivert", "externalProxyEnabled": "Ekstern proxy aktivert",
"addNewTarget": "Legg til nytt mål", "addNewTarget": "Legg til nytt mål",
"targetsList": "Liste over mål", "targetsList": "Liste over mål",
"advancedMode": "Avansert modus",
"targetErrorDuplicateTargetFound": "Duplikat av mål funnet", "targetErrorDuplicateTargetFound": "Duplikat av mål funnet",
"healthCheckHealthy": "Sunn", "healthCheckHealthy": "Sunn",
"healthCheckUnhealthy": "Usunn", "healthCheckUnhealthy": "Usunn",
@@ -1543,8 +1557,8 @@
"autoLoginError": "Feil ved automatisk innlogging", "autoLoginError": "Feil ved automatisk innlogging",
"autoLoginErrorNoRedirectUrl": "Ingen omdirigerings-URL mottatt fra identitetsleverandøren.", "autoLoginErrorNoRedirectUrl": "Ingen omdirigerings-URL mottatt fra identitetsleverandøren.",
"autoLoginErrorGeneratingUrl": "Kunne ikke generere autentiserings-URL.", "autoLoginErrorGeneratingUrl": "Kunne ikke generere autentiserings-URL.",
"remoteExitNodeManageRemoteExitNodes": "Administrer Selv-Hostet", "remoteExitNodeManageRemoteExitNodes": "Eksterne Noder",
"remoteExitNodeDescription": "Administrer noder for å forlenge nettverkstilkoblingen din", "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
"remoteExitNodes": "Noder", "remoteExitNodes": "Noder",
"searchRemoteExitNodes": "Søk noder...", "searchRemoteExitNodes": "Søk noder...",
"remoteExitNodeAdd": "Legg til Node", "remoteExitNodeAdd": "Legg til Node",
@@ -1554,7 +1568,7 @@
"remoteExitNodeMessageConfirm": "For å bekrefte, skriv inn navnet på noden nedenfor.", "remoteExitNodeMessageConfirm": "For å bekrefte, skriv inn navnet på noden nedenfor.",
"remoteExitNodeConfirmDelete": "Bekreft sletting av Node", "remoteExitNodeConfirmDelete": "Bekreft sletting av Node",
"remoteExitNodeDelete": "Slett Node", "remoteExitNodeDelete": "Slett Node",
"sidebarRemoteExitNodes": "Noder", "sidebarRemoteExitNodes": "Eksterne Noder",
"remoteExitNodeCreate": { "remoteExitNodeCreate": {
"title": "Opprett node", "title": "Opprett node",
"description": "Opprett en ny node for å utvide nettverkstilkoblingen din", "description": "Opprett en ny node for å utvide nettverkstilkoblingen din",
@@ -1723,5 +1737,161 @@
"authPageUpdated": "Godkjenningsside oppdatert", "authPageUpdated": "Godkjenningsside oppdatert",
"healthCheckNotAvailable": "Lokal", "healthCheckNotAvailable": "Lokal",
"rewritePath": "Omskriv sti", "rewritePath": "Omskriv sti",
"rewritePathDescription": "Valgfritt omskrive stien før videresending til målet." "rewritePathDescription": "Valgfritt omskrive stien før videresending til målet.",
"continueToApplication": "Fortsett til applikasjonen",
"checkingInvite": "Sjekker invitasjon",
"setResourceHeaderAuth": "setResourceHeaderAuth",
"resourceHeaderAuthRemove": "Fjern topptekst Auth",
"resourceHeaderAuthRemoveDescription": "Topplinje autentisering fjernet.",
"resourceErrorHeaderAuthRemove": "Kunne ikke fjerne topptekst autentisering",
"resourceErrorHeaderAuthRemoveDescription": "Kunne ikke fjerne topptekst autentisering for ressursen.",
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
"headerAuthRemove": "Remove Header Auth",
"headerAuthAdd": "Add Header Auth",
"resourceErrorHeaderAuthSetup": "Kunne ikke sette topptekst autentisering",
"resourceErrorHeaderAuthSetupDescription": "Kunne ikke sette topplinje autentisering for ressursen.",
"resourceHeaderAuthSetup": "Header godkjenningssett var vellykket",
"resourceHeaderAuthSetupDescription": "Topplinje autentisering har blitt lagret.",
"resourceHeaderAuthSetupTitle": "Angi topptekst godkjenning",
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
"resourceHeaderAuthSubmit": "Angi topptekst godkjenning",
"actionSetResourceHeaderAuth": "Angi topptekst godkjenning",
"enterpriseEdition": "Enterprise Edition",
"unlicensed": "Unlicensed",
"beta": "Beta",
"manageClients": "Manage Clients",
"manageClientsDescription": "Clients are devices that can connect to your sites",
"licenseTableValidUntil": "Valid Until",
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
"sidebarEnterpriseLicenses": "Licenses",
"generateLicenseKey": "Generate License Key",
"generateLicenseKeyForm": {
"validation": {
"emailRequired": "Please enter a valid email address",
"useCaseTypeRequired": "Please select a use case type",
"firstNameRequired": "First name is required",
"lastNameRequired": "Last name is required",
"primaryUseRequired": "Please describe your primary use",
"jobTitleRequiredBusiness": "Job title is required for business use",
"industryRequiredBusiness": "Industry is required for business use",
"stateProvinceRegionRequired": "State/Province/Region is required",
"postalZipCodeRequired": "Postal/ZIP Code is required",
"companyNameRequiredBusiness": "Company name is required for business use",
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
"countryRequiredPersonal": "Country is required for personal use",
"agreeToTermsRequired": "You must agree to the terms",
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
},
"useCaseOptions": {
"personal": {
"title": "Personal Use",
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
},
"business": {
"title": "Business Use",
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
}
},
"steps": {
"emailLicenseType": {
"title": "Email & License Type",
"description": "Enter your email and choose your license type"
},
"personalInformation": {
"title": "Personal Information",
"description": "Tell us about yourself"
},
"contactInformation": {
"title": "Contact Information",
"description": "Your contact details"
},
"termsGenerate": {
"title": "Terms & Generate",
"description": "Review and accept terms to generate your license"
}
},
"alerts": {
"commercialUseDisclosure": {
"title": "Usage Disclosure",
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
},
"trialPeriodInformation": {
"title": "Trial Period Information",
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
}
},
"form": {
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
"firstName": "First Name",
"lastName": "Last Name",
"jobTitle": "Job Title",
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
"industryQuestion": "What is your industry?",
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
"companyName": "Company name",
"countryOfResidence": "Country of residence",
"stateProvinceRegion": "State / Province / Region",
"postalZipCode": "Postal / ZIP Code",
"companyWebsite": "Company website",
"companyPhoneNumber": "Company phone number",
"country": "Country",
"phoneNumberOptional": "Phone number (optional)",
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
},
"buttons": {
"close": "Close",
"previous": "Previous",
"next": "Next",
"generateLicenseKey": "Generate License Key"
},
"toasts": {
"success": {
"title": "License key generated successfully",
"description": "Your license key has been generated and is ready to use."
},
"error": {
"title": "Failed to generate license key",
"description": "An error occurred while generating the license key."
}
}
},
"priority": "Prioritet",
"priorityDescription": "Høyere prioriterte ruter evalueres først. Prioritet = 100 betyr automatisk bestilling (systembeslutninger). Bruk et annet nummer til å håndheve manuell prioritet.",
"instanceName": "Instance Name",
"pathMatchModalTitle": "Configure Path Matching",
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
"pathMatchType": "Match Type",
"pathMatchPrefix": "Prefix",
"pathMatchExact": "Exact",
"pathMatchRegex": "Regex",
"pathMatchValue": "Path Value",
"clear": "Clear",
"saveChanges": "Save Changes",
"pathMatchRegexPlaceholder": "^/api/.*",
"pathMatchDefaultPlaceholder": "/path",
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
"pathMatchExactHelp": "Example: /api matches only /api",
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
"pathRewriteModalTitle": "Configure Path Rewriting",
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
"pathRewriteType": "Rewrite Type",
"pathRewritePrefixOption": "Prefix - Replace prefix",
"pathRewriteExactOption": "Exact - Replace entire path",
"pathRewriteRegexOption": "Regex - Pattern replacement",
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
"pathRewriteValue": "Rewrite Value",
"pathRewriteRegexPlaceholder": "/new/$1",
"pathRewriteDefaultPlaceholder": "/new-path",
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
"pathRewritePrefix": "Prefix",
"pathRewriteExact": "Exact",
"pathRewriteRegex": "Regex",
"pathRewriteStrip": "Strip",
"pathRewriteStripLabel": "strip"
} }

View File

@@ -96,7 +96,7 @@
"siteWgDescription": "Gebruik een WireGuard client om een tunnel te bouwen. Handmatige NAT installatie vereist.", "siteWgDescription": "Gebruik een WireGuard client om een tunnel te bouwen. Handmatige NAT installatie vereist.",
"siteWgDescriptionSaas": "Gebruik elke WireGuard-client om een tunnel op te zetten. Handmatige NAT-instelling vereist. WERKT ALLEEN OP SELF HOSTED NODES", "siteWgDescriptionSaas": "Gebruik elke WireGuard-client om een tunnel op te zetten. Handmatige NAT-instelling vereist. WERKT ALLEEN OP SELF HOSTED NODES",
"siteLocalDescription": "Alleen lokale bronnen. Geen tunneling.", "siteLocalDescription": "Alleen lokale bronnen. Geen tunneling.",
"siteLocalDescriptionSaas": "Alleen lokale bronnen. Geen tunneling. WERKT ALLEEN OP SELF HOSTED NODES", "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
"siteSeeAll": "Alle sites bekijken", "siteSeeAll": "Alle sites bekijken",
"siteTunnelDescription": "Bepaal hoe u verbinding wilt maken met uw site", "siteTunnelDescription": "Bepaal hoe u verbinding wilt maken met uw site",
"siteNewtCredentials": "Nieuwste aanmeldgegevens", "siteNewtCredentials": "Nieuwste aanmeldgegevens",
@@ -468,7 +468,10 @@
"createdAt": "Aangemaakt op", "createdAt": "Aangemaakt op",
"proxyErrorInvalidHeader": "Ongeldige aangepaste Header waarde. Gebruik het domeinnaam formaat, of sla leeg op om de aangepaste Host header ongedaan te maken.", "proxyErrorInvalidHeader": "Ongeldige aangepaste Header waarde. Gebruik het domeinnaam formaat, of sla leeg op om de aangepaste Host header ongedaan te maken.",
"proxyErrorTls": "Ongeldige TLS servernaam. Gebruik de domeinnaam of sla leeg op om de TLS servernaam te verwijderen.", "proxyErrorTls": "Ongeldige TLS servernaam. Gebruik de domeinnaam of sla leeg op om de TLS servernaam te verwijderen.",
"proxyEnableSSL": "SSL (https) inschakelen", "proxyEnableSSL": "SSL inschakelen",
"proxyEnableSSLDescription": "SSL/TLS-versleuteling inschakelen voor beveiligde HTTPS-verbindingen naar uw doelen.",
"target": "Target",
"configureTarget": "Doelstellingen configureren",
"targetErrorFetch": "Ophalen van doelen mislukt", "targetErrorFetch": "Ophalen van doelen mislukt",
"targetErrorFetchDescription": "Er is een fout opgetreden bij het ophalen van de objecten", "targetErrorFetchDescription": "Er is een fout opgetreden bij het ophalen van de objecten",
"siteErrorFetch": "Mislukt om resource op te halen", "siteErrorFetch": "Mislukt om resource op te halen",
@@ -495,7 +498,7 @@
"targetTlsSettings": "HTTPS & TLS instellingen", "targetTlsSettings": "HTTPS & TLS instellingen",
"targetTlsSettingsDescription": "SSL/TLS-instellingen voor uw bron configureren", "targetTlsSettingsDescription": "SSL/TLS-instellingen voor uw bron configureren",
"targetTlsSettingsAdvanced": "Geavanceerde TLS instellingen", "targetTlsSettingsAdvanced": "Geavanceerde TLS instellingen",
"targetTlsSni": "TLS Server Naam (SNI)", "targetTlsSni": "TLS servernaam",
"targetTlsSniDescription": "De TLS servernaam om te gebruiken voor SNI. Laat leeg om de standaard te gebruiken.", "targetTlsSniDescription": "De TLS servernaam om te gebruiken voor SNI. Laat leeg om de standaard te gebruiken.",
"targetTlsSubmit": "Instellingen opslaan", "targetTlsSubmit": "Instellingen opslaan",
"targets": "Doelstellingen configuratie", "targets": "Doelstellingen configuratie",
@@ -504,9 +507,21 @@
"targetStickySessionsDescription": "Behoud verbindingen op hetzelfde backend doel voor hun hele sessie.", "targetStickySessionsDescription": "Behoud verbindingen op hetzelfde backend doel voor hun hele sessie.",
"methodSelect": "Selecteer methode", "methodSelect": "Selecteer methode",
"targetSubmit": "Doelwit toevoegen", "targetSubmit": "Doelwit toevoegen",
"targetNoOne": "Geen doel toegevoegd. Voeg deze toe via dit formulier.", "targetNoOne": "Deze bron heeft geen doelen. Voeg een doel toe om te configureren waar verzoeken naar uw backend.",
"targetNoOneDescription": "Het toevoegen van meer dan één doel hierboven zal de load balancering mogelijk maken.", "targetNoOneDescription": "Het toevoegen van meer dan één doel hierboven zal de load balancering mogelijk maken.",
"targetsSubmit": "Doelstellingen opslaan", "targetsSubmit": "Doelstellingen opslaan",
"addTarget": "Doelwit toevoegen",
"targetErrorInvalidIp": "Ongeldig IP-adres",
"targetErrorInvalidIpDescription": "Voer een geldig IP-adres of hostnaam in",
"targetErrorInvalidPort": "Ongeldige poort",
"targetErrorInvalidPortDescription": "Voer een geldig poortnummer in",
"targetErrorNoSite": "Geen site geselecteerd",
"targetErrorNoSiteDescription": "Selecteer een site voor het doel",
"targetCreated": "Doel aangemaakt",
"targetCreatedDescription": "Doel is succesvol aangemaakt",
"targetErrorCreate": "Kan doel niet aanmaken",
"targetErrorCreateDescription": "Fout opgetreden tijdens het aanmaken van het doel",
"save": "Opslaan",
"proxyAdditional": "Extra Proxy-instellingen", "proxyAdditional": "Extra Proxy-instellingen",
"proxyAdditionalDescription": "Configureer hoe de proxy-instellingen van uw bron worden afgehandeld", "proxyAdditionalDescription": "Configureer hoe de proxy-instellingen van uw bron worden afgehandeld",
"proxyCustomHeader": "Aangepaste Host-header", "proxyCustomHeader": "Aangepaste Host-header",
@@ -715,7 +730,7 @@
"pangolinServerAdmin": "Serverbeheer - Pangolin", "pangolinServerAdmin": "Serverbeheer - Pangolin",
"licenseTierProfessional": "Professionele licentie", "licenseTierProfessional": "Professionele licentie",
"licenseTierEnterprise": "Enterprise Licentie", "licenseTierEnterprise": "Enterprise Licentie",
"licenseTierCommercial": "Commerciële licentie", "licenseTierPersonal": "Personal License",
"licensed": "Gelicentieerd", "licensed": "Gelicentieerd",
"yes": "ja", "yes": "ja",
"no": "Neen", "no": "Neen",
@@ -750,7 +765,7 @@
"idpDisplayName": "Een weergavenaam voor deze identiteitsprovider", "idpDisplayName": "Een weergavenaam voor deze identiteitsprovider",
"idpAutoProvisionUsers": "Auto Provisie Gebruikers", "idpAutoProvisionUsers": "Auto Provisie Gebruikers",
"idpAutoProvisionUsersDescription": "Wanneer ingeschakeld, worden gebruikers automatisch in het systeem aangemaakt wanneer ze de eerste keer inloggen met de mogelijkheid om gebruikers toe te wijzen aan rollen en organisaties.", "idpAutoProvisionUsersDescription": "Wanneer ingeschakeld, worden gebruikers automatisch in het systeem aangemaakt wanneer ze de eerste keer inloggen met de mogelijkheid om gebruikers toe te wijzen aan rollen en organisaties.",
"licenseBadge": "Professioneel", "licenseBadge": "EE",
"idpType": "Type provider", "idpType": "Type provider",
"idpTypeDescription": "Selecteer het type identiteitsprovider dat u wilt configureren", "idpTypeDescription": "Selecteer het type identiteitsprovider dat u wilt configureren",
"idpOidcConfigure": "OAuth2/OIDC configuratie", "idpOidcConfigure": "OAuth2/OIDC configuratie",
@@ -1084,7 +1099,6 @@
"navbar": "Navigatiemenu", "navbar": "Navigatiemenu",
"navbarDescription": "Hoofd navigatie menu voor de applicatie", "navbarDescription": "Hoofd navigatie menu voor de applicatie",
"navbarDocsLink": "Documentatie", "navbarDocsLink": "Documentatie",
"commercialEdition": "Commerciële editie",
"otpErrorEnable": "Kan 2FA niet inschakelen", "otpErrorEnable": "Kan 2FA niet inschakelen",
"otpErrorEnableDescription": "Er is een fout opgetreden tijdens het inschakelen van 2FA", "otpErrorEnableDescription": "Er is een fout opgetreden tijdens het inschakelen van 2FA",
"otpSetupCheckCode": "Voer een 6-cijferige code in", "otpSetupCheckCode": "Voer een 6-cijferige code in",
@@ -1140,7 +1154,7 @@
"sidebarAllUsers": "Alle gebruikers", "sidebarAllUsers": "Alle gebruikers",
"sidebarIdentityProviders": "Identiteit aanbieders", "sidebarIdentityProviders": "Identiteit aanbieders",
"sidebarLicense": "Licentie", "sidebarLicense": "Licentie",
"sidebarClients": "Clients (Bèta)", "sidebarClients": "Clients",
"sidebarDomains": "Domeinen", "sidebarDomains": "Domeinen",
"enableDockerSocket": "Schakel Docker Blauwdruk in", "enableDockerSocket": "Schakel Docker Blauwdruk in",
"enableDockerSocketDescription": "Schakel Docker Socket label in voor blauwdruk labels. Pad naar Nieuw.", "enableDockerSocketDescription": "Schakel Docker Socket label in voor blauwdruk labels. Pad naar Nieuw.",
@@ -1333,7 +1347,6 @@
"twoFactorRequired": "Tweestapsverificatie is vereist om een beveiligingssleutel te registreren.", "twoFactorRequired": "Tweestapsverificatie is vereist om een beveiligingssleutel te registreren.",
"twoFactor": "Tweestapsverificatie", "twoFactor": "Tweestapsverificatie",
"adminEnabled2FaOnYourAccount": "Je beheerder heeft tweestapsverificatie voor {email} ingeschakeld. Voltooi het instellingsproces om verder te gaan.", "adminEnabled2FaOnYourAccount": "Je beheerder heeft tweestapsverificatie voor {email} ingeschakeld. Voltooi het instellingsproces om verder te gaan.",
"continueToApplication": "Doorgaan naar applicatie",
"securityKeyAdd": "Beveiligingssleutel toevoegen", "securityKeyAdd": "Beveiligingssleutel toevoegen",
"securityKeyRegisterTitle": "Nieuwe beveiligingssleutel registreren", "securityKeyRegisterTitle": "Nieuwe beveiligingssleutel registreren",
"securityKeyRegisterDescription": "Verbind je beveiligingssleutel en voer een naam in om deze te identificeren", "securityKeyRegisterDescription": "Verbind je beveiligingssleutel en voer een naam in om deze te identificeren",
@@ -1411,6 +1424,7 @@
"externalProxyEnabled": "Externe Proxy Ingeschakeld", "externalProxyEnabled": "Externe Proxy Ingeschakeld",
"addNewTarget": "Voeg nieuw doelwit toe", "addNewTarget": "Voeg nieuw doelwit toe",
"targetsList": "Lijst met doelen", "targetsList": "Lijst met doelen",
"advancedMode": "Geavanceerde modus",
"targetErrorDuplicateTargetFound": "Dubbel doelwit gevonden", "targetErrorDuplicateTargetFound": "Dubbel doelwit gevonden",
"healthCheckHealthy": "Gezond", "healthCheckHealthy": "Gezond",
"healthCheckUnhealthy": "Ongezond", "healthCheckUnhealthy": "Ongezond",
@@ -1543,8 +1557,8 @@
"autoLoginError": "Auto Login Fout", "autoLoginError": "Auto Login Fout",
"autoLoginErrorNoRedirectUrl": "Geen redirect URL ontvangen van de identity provider.", "autoLoginErrorNoRedirectUrl": "Geen redirect URL ontvangen van de identity provider.",
"autoLoginErrorGeneratingUrl": "Genereren van authenticatie-URL mislukt.", "autoLoginErrorGeneratingUrl": "Genereren van authenticatie-URL mislukt.",
"remoteExitNodeManageRemoteExitNodes": "Beheer Zelf-Gehoste", "remoteExitNodeManageRemoteExitNodes": "Externe knooppunten",
"remoteExitNodeDescription": "Beheer knooppunten om uw netwerkverbinding uit te breiden", "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
"remoteExitNodes": "Nodes", "remoteExitNodes": "Nodes",
"searchRemoteExitNodes": "Knooppunten zoeken...", "searchRemoteExitNodes": "Knooppunten zoeken...",
"remoteExitNodeAdd": "Voeg node toe", "remoteExitNodeAdd": "Voeg node toe",
@@ -1554,7 +1568,7 @@
"remoteExitNodeMessageConfirm": "Om te bevestigen, typ de naam van het knooppunt hieronder.", "remoteExitNodeMessageConfirm": "Om te bevestigen, typ de naam van het knooppunt hieronder.",
"remoteExitNodeConfirmDelete": "Bevestig verwijderen node", "remoteExitNodeConfirmDelete": "Bevestig verwijderen node",
"remoteExitNodeDelete": "Knoop verwijderen", "remoteExitNodeDelete": "Knoop verwijderen",
"sidebarRemoteExitNodes": "Nodes", "sidebarRemoteExitNodes": "Externe knooppunten",
"remoteExitNodeCreate": { "remoteExitNodeCreate": {
"title": "Maak node", "title": "Maak node",
"description": "Maak een nieuwe node aan om uw netwerkverbinding uit te breiden", "description": "Maak een nieuwe node aan om uw netwerkverbinding uit te breiden",
@@ -1723,5 +1737,161 @@
"authPageUpdated": "Auth-pagina succesvol bijgewerkt", "authPageUpdated": "Auth-pagina succesvol bijgewerkt",
"healthCheckNotAvailable": "Lokaal", "healthCheckNotAvailable": "Lokaal",
"rewritePath": "Herschrijf Pad", "rewritePath": "Herschrijf Pad",
"rewritePathDescription": "Optioneel het pad herschrijven voordat je het naar het doel doorstuurt." "rewritePathDescription": "Optioneel het pad herschrijven voordat je het naar het doel doorstuurt.",
"continueToApplication": "Doorgaan naar applicatie",
"checkingInvite": "Uitnodiging controleren",
"setResourceHeaderAuth": "stelResourceHeaderAuth",
"resourceHeaderAuthRemove": "Auth koptekst verwijderen",
"resourceHeaderAuthRemoveDescription": "Koptekst authenticatie succesvol verwijderd.",
"resourceErrorHeaderAuthRemove": "Kan Header-authenticatie niet verwijderen",
"resourceErrorHeaderAuthRemoveDescription": "Kon header authenticatie niet verwijderen voor de bron.",
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
"headerAuthRemove": "Remove Header Auth",
"headerAuthAdd": "Add Header Auth",
"resourceErrorHeaderAuthSetup": "Kan Header Authenticatie niet instellen",
"resourceErrorHeaderAuthSetupDescription": "Kan geen header authenticatie instellen voor de bron.",
"resourceHeaderAuthSetup": "Header Authenticatie set succesvol",
"resourceHeaderAuthSetupDescription": "Header authenticatie is met succes ingesteld.",
"resourceHeaderAuthSetupTitle": "Header Authenticatie instellen",
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
"resourceHeaderAuthSubmit": "Header Authenticatie instellen",
"actionSetResourceHeaderAuth": "Header Authenticatie instellen",
"enterpriseEdition": "Enterprise Edition",
"unlicensed": "Unlicensed",
"beta": "Beta",
"manageClients": "Manage Clients",
"manageClientsDescription": "Clients are devices that can connect to your sites",
"licenseTableValidUntil": "Valid Until",
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
"sidebarEnterpriseLicenses": "Licenses",
"generateLicenseKey": "Generate License Key",
"generateLicenseKeyForm": {
"validation": {
"emailRequired": "Please enter a valid email address",
"useCaseTypeRequired": "Please select a use case type",
"firstNameRequired": "First name is required",
"lastNameRequired": "Last name is required",
"primaryUseRequired": "Please describe your primary use",
"jobTitleRequiredBusiness": "Job title is required for business use",
"industryRequiredBusiness": "Industry is required for business use",
"stateProvinceRegionRequired": "State/Province/Region is required",
"postalZipCodeRequired": "Postal/ZIP Code is required",
"companyNameRequiredBusiness": "Company name is required for business use",
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
"countryRequiredPersonal": "Country is required for personal use",
"agreeToTermsRequired": "You must agree to the terms",
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
},
"useCaseOptions": {
"personal": {
"title": "Personal Use",
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
},
"business": {
"title": "Business Use",
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
}
},
"steps": {
"emailLicenseType": {
"title": "Email & License Type",
"description": "Enter your email and choose your license type"
},
"personalInformation": {
"title": "Personal Information",
"description": "Tell us about yourself"
},
"contactInformation": {
"title": "Contact Information",
"description": "Your contact details"
},
"termsGenerate": {
"title": "Terms & Generate",
"description": "Review and accept terms to generate your license"
}
},
"alerts": {
"commercialUseDisclosure": {
"title": "Usage Disclosure",
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
},
"trialPeriodInformation": {
"title": "Trial Period Information",
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
}
},
"form": {
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
"firstName": "First Name",
"lastName": "Last Name",
"jobTitle": "Job Title",
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
"industryQuestion": "What is your industry?",
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
"companyName": "Company name",
"countryOfResidence": "Country of residence",
"stateProvinceRegion": "State / Province / Region",
"postalZipCode": "Postal / ZIP Code",
"companyWebsite": "Company website",
"companyPhoneNumber": "Company phone number",
"country": "Country",
"phoneNumberOptional": "Phone number (optional)",
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
},
"buttons": {
"close": "Close",
"previous": "Previous",
"next": "Next",
"generateLicenseKey": "Generate License Key"
},
"toasts": {
"success": {
"title": "License key generated successfully",
"description": "Your license key has been generated and is ready to use."
},
"error": {
"title": "Failed to generate license key",
"description": "An error occurred while generating the license key."
}
}
},
"priority": "Prioriteit",
"priorityDescription": "routes met hogere prioriteit worden eerst geëvalueerd. Prioriteit = 100 betekent automatisch bestellen (systeem beslist de). Gebruik een ander nummer om handmatige prioriteit af te dwingen.",
"instanceName": "Instance Name",
"pathMatchModalTitle": "Configure Path Matching",
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
"pathMatchType": "Match Type",
"pathMatchPrefix": "Prefix",
"pathMatchExact": "Exact",
"pathMatchRegex": "Regex",
"pathMatchValue": "Path Value",
"clear": "Clear",
"saveChanges": "Save Changes",
"pathMatchRegexPlaceholder": "^/api/.*",
"pathMatchDefaultPlaceholder": "/path",
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
"pathMatchExactHelp": "Example: /api matches only /api",
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
"pathRewriteModalTitle": "Configure Path Rewriting",
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
"pathRewriteType": "Rewrite Type",
"pathRewritePrefixOption": "Prefix - Replace prefix",
"pathRewriteExactOption": "Exact - Replace entire path",
"pathRewriteRegexOption": "Regex - Pattern replacement",
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
"pathRewriteValue": "Rewrite Value",
"pathRewriteRegexPlaceholder": "/new/$1",
"pathRewriteDefaultPlaceholder": "/new-path",
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
"pathRewritePrefix": "Prefix",
"pathRewriteExact": "Exact",
"pathRewriteRegex": "Regex",
"pathRewriteStrip": "Strip",
"pathRewriteStripLabel": "strip"
} }

View File

@@ -96,7 +96,7 @@
"siteWgDescription": "Użyj dowolnego klienta WireGuard do utworzenia tunelu. Wymagana jest ręczna konfiguracja NAT.", "siteWgDescription": "Użyj dowolnego klienta WireGuard do utworzenia tunelu. Wymagana jest ręczna konfiguracja NAT.",
"siteWgDescriptionSaas": "Użyj dowolnego klienta WireGuard do utworzenia tunelu. Wymagana ręczna konfiguracja NAT. DZIAŁA TYLKO NA SAMODZIELNIE HOSTOWANYCH WĘZŁACH", "siteWgDescriptionSaas": "Użyj dowolnego klienta WireGuard do utworzenia tunelu. Wymagana ręczna konfiguracja NAT. DZIAŁA TYLKO NA SAMODZIELNIE HOSTOWANYCH WĘZŁACH",
"siteLocalDescription": "Tylko lokalne zasoby. Brak tunelu.", "siteLocalDescription": "Tylko lokalne zasoby. Brak tunelu.",
"siteLocalDescriptionSaas": "Tylko zasoby lokalne. Brak tunelowania. DZIAŁA TYLKO NA SAMODZIELNIE HOSTOWANYCH WĘZŁACH", "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
"siteSeeAll": "Zobacz wszystkie witryny", "siteSeeAll": "Zobacz wszystkie witryny",
"siteTunnelDescription": "Określ jak chcesz połączyć się ze swoją stroną", "siteTunnelDescription": "Określ jak chcesz połączyć się ze swoją stroną",
"siteNewtCredentials": "Aktualne dane logowania", "siteNewtCredentials": "Aktualne dane logowania",
@@ -468,7 +468,10 @@
"createdAt": "Utworzono", "createdAt": "Utworzono",
"proxyErrorInvalidHeader": "Nieprawidłowa wartość niestandardowego nagłówka hosta. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć niestandardowy nagłówek hosta.", "proxyErrorInvalidHeader": "Nieprawidłowa wartość niestandardowego nagłówka hosta. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć niestandardowy nagłówek hosta.",
"proxyErrorTls": "Nieprawidłowa nazwa serwera TLS. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć nazwę serwera TLS.", "proxyErrorTls": "Nieprawidłowa nazwa serwera TLS. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć nazwę serwera TLS.",
"proxyEnableSSL": "Włącz SSL (https)", "proxyEnableSSL": "Włącz SSL",
"proxyEnableSSLDescription": "Włącz szyfrowanie SSL/TLS dla bezpiecznych połączeń HTTPS z Twoimi celami.",
"target": "Target",
"configureTarget": "Konfiguruj Targety",
"targetErrorFetch": "Nie udało się pobrać celów", "targetErrorFetch": "Nie udało się pobrać celów",
"targetErrorFetchDescription": "Wystąpił błąd podczas pobierania celów", "targetErrorFetchDescription": "Wystąpił błąd podczas pobierania celów",
"siteErrorFetch": "Nie udało się pobrać zasobu", "siteErrorFetch": "Nie udało się pobrać zasobu",
@@ -495,7 +498,7 @@
"targetTlsSettings": "Konfiguracja bezpiecznego połączenia", "targetTlsSettings": "Konfiguracja bezpiecznego połączenia",
"targetTlsSettingsDescription": "Skonfiguruj ustawienia SSL/TLS dla twojego zasobu", "targetTlsSettingsDescription": "Skonfiguruj ustawienia SSL/TLS dla twojego zasobu",
"targetTlsSettingsAdvanced": "Zaawansowane ustawienia TLS", "targetTlsSettingsAdvanced": "Zaawansowane ustawienia TLS",
"targetTlsSni": "Nazwa serwera TLS (SNI)", "targetTlsSni": "Nazwa serwera TLS",
"targetTlsSniDescription": "Nazwa serwera TLS do użycia dla SNI. Pozostaw puste, aby użyć domyślnej.", "targetTlsSniDescription": "Nazwa serwera TLS do użycia dla SNI. Pozostaw puste, aby użyć domyślnej.",
"targetTlsSubmit": "Zapisz ustawienia", "targetTlsSubmit": "Zapisz ustawienia",
"targets": "Konfiguracja celów", "targets": "Konfiguracja celów",
@@ -504,9 +507,21 @@
"targetStickySessionsDescription": "Utrzymuj połączenia na tym samym celu backendowym przez całą sesję.", "targetStickySessionsDescription": "Utrzymuj połączenia na tym samym celu backendowym przez całą sesję.",
"methodSelect": "Wybierz metodę", "methodSelect": "Wybierz metodę",
"targetSubmit": "Dodaj cel", "targetSubmit": "Dodaj cel",
"targetNoOne": "Brak celów. Dodaj cel używając formularza.", "targetNoOne": "Ten zasób nie ma żadnych celów. Dodaj cel, aby skonfigurować miejsce wysyłania żądań do twojego backendu.",
"targetNoOneDescription": "Dodanie więcej niż jednego celu powyżej włączy równoważenie obciążenia.", "targetNoOneDescription": "Dodanie więcej niż jednego celu powyżej włączy równoważenie obciążenia.",
"targetsSubmit": "Zapisz cele", "targetsSubmit": "Zapisz cele",
"addTarget": "Dodaj cel",
"targetErrorInvalidIp": "Nieprawidłowy adres IP",
"targetErrorInvalidIpDescription": "Wprowadź prawidłowy adres IP lub nazwę hosta",
"targetErrorInvalidPort": "Nieprawidłowy port",
"targetErrorInvalidPortDescription": "Wprowadź prawidłowy numer portu",
"targetErrorNoSite": "Nie wybrano witryny",
"targetErrorNoSiteDescription": "Wybierz witrynę docelową",
"targetCreated": "Cel utworzony",
"targetCreatedDescription": "Cel został utworzony pomyślnie",
"targetErrorCreate": "Nie udało się utworzyć celu",
"targetErrorCreateDescription": "Wystąpił błąd podczas tworzenia celu",
"save": "Zapisz",
"proxyAdditional": "Dodatkowe ustawienia proxy", "proxyAdditional": "Dodatkowe ustawienia proxy",
"proxyAdditionalDescription": "Skonfiguruj jak twój zasób obsługuje ustawienia proxy", "proxyAdditionalDescription": "Skonfiguruj jak twój zasób obsługuje ustawienia proxy",
"proxyCustomHeader": "Niestandardowy nagłówek hosta", "proxyCustomHeader": "Niestandardowy nagłówek hosta",
@@ -715,7 +730,7 @@
"pangolinServerAdmin": "Administrator serwera - Pangolin", "pangolinServerAdmin": "Administrator serwera - Pangolin",
"licenseTierProfessional": "Licencja Professional", "licenseTierProfessional": "Licencja Professional",
"licenseTierEnterprise": "Licencja Enterprise", "licenseTierEnterprise": "Licencja Enterprise",
"licenseTierCommercial": "Licencja handlowa", "licenseTierPersonal": "Personal License",
"licensed": "Licencjonowany", "licensed": "Licencjonowany",
"yes": "Tak", "yes": "Tak",
"no": "Nie", "no": "Nie",
@@ -750,7 +765,7 @@
"idpDisplayName": "Nazwa wyświetlana dla tego dostawcy tożsamości", "idpDisplayName": "Nazwa wyświetlana dla tego dostawcy tożsamości",
"idpAutoProvisionUsers": "Automatyczne tworzenie użytkowników", "idpAutoProvisionUsers": "Automatyczne tworzenie użytkowników",
"idpAutoProvisionUsersDescription": "Gdy włączone, użytkownicy będą automatycznie tworzeni w systemie przy pierwszym logowaniu z możliwością mapowania użytkowników do ról i organizacji.", "idpAutoProvisionUsersDescription": "Gdy włączone, użytkownicy będą automatycznie tworzeni w systemie przy pierwszym logowaniu z możliwością mapowania użytkowników do ról i organizacji.",
"licenseBadge": "Profesjonalny", "licenseBadge": "EE",
"idpType": "Typ dostawcy", "idpType": "Typ dostawcy",
"idpTypeDescription": "Wybierz typ dostawcy tożsamości, który chcesz skonfigurować", "idpTypeDescription": "Wybierz typ dostawcy tożsamości, który chcesz skonfigurować",
"idpOidcConfigure": "Konfiguracja OAuth2/OIDC", "idpOidcConfigure": "Konfiguracja OAuth2/OIDC",
@@ -1084,7 +1099,6 @@
"navbar": "Menu nawigacyjne", "navbar": "Menu nawigacyjne",
"navbarDescription": "Główne menu nawigacyjne aplikacji", "navbarDescription": "Główne menu nawigacyjne aplikacji",
"navbarDocsLink": "Dokumentacja", "navbarDocsLink": "Dokumentacja",
"commercialEdition": "Edycja komercyjna",
"otpErrorEnable": "Nie można włączyć 2FA", "otpErrorEnable": "Nie można włączyć 2FA",
"otpErrorEnableDescription": "Wystąpił błąd podczas włączania 2FA", "otpErrorEnableDescription": "Wystąpił błąd podczas włączania 2FA",
"otpSetupCheckCode": "Wprowadź 6-cyfrowy kod", "otpSetupCheckCode": "Wprowadź 6-cyfrowy kod",
@@ -1140,7 +1154,7 @@
"sidebarAllUsers": "Wszyscy użytkownicy", "sidebarAllUsers": "Wszyscy użytkownicy",
"sidebarIdentityProviders": "Dostawcy tożsamości", "sidebarIdentityProviders": "Dostawcy tożsamości",
"sidebarLicense": "Licencja", "sidebarLicense": "Licencja",
"sidebarClients": "Klienci (Beta)", "sidebarClients": "Clients",
"sidebarDomains": "Domeny", "sidebarDomains": "Domeny",
"enableDockerSocket": "Włącz schemat dokera", "enableDockerSocket": "Włącz schemat dokera",
"enableDockerSocketDescription": "Włącz etykietowanie kieszeni dokującej dla etykiet schematów. Ścieżka do gniazda musi być dostarczona do Newt.", "enableDockerSocketDescription": "Włącz etykietowanie kieszeni dokującej dla etykiet schematów. Ścieżka do gniazda musi być dostarczona do Newt.",
@@ -1333,7 +1347,6 @@
"twoFactorRequired": "Uwierzytelnianie dwuskładnikowe jest wymagane do zarejestrowania klucza bezpieczeństwa.", "twoFactorRequired": "Uwierzytelnianie dwuskładnikowe jest wymagane do zarejestrowania klucza bezpieczeństwa.",
"twoFactor": "Uwierzytelnianie dwuskładnikowe", "twoFactor": "Uwierzytelnianie dwuskładnikowe",
"adminEnabled2FaOnYourAccount": "Twój administrator włączył uwierzytelnianie dwuskładnikowe dla {email}. Proszę ukończyć proces konfiguracji, aby kontynuować.", "adminEnabled2FaOnYourAccount": "Twój administrator włączył uwierzytelnianie dwuskładnikowe dla {email}. Proszę ukończyć proces konfiguracji, aby kontynuować.",
"continueToApplication": "Kontynuuj do aplikacji",
"securityKeyAdd": "Dodaj klucz bezpieczeństwa", "securityKeyAdd": "Dodaj klucz bezpieczeństwa",
"securityKeyRegisterTitle": "Zarejestruj nowy klucz bezpieczeństwa", "securityKeyRegisterTitle": "Zarejestruj nowy klucz bezpieczeństwa",
"securityKeyRegisterDescription": "Podłącz swój klucz bezpieczeństwa i wprowadź nazwę, aby go zidentyfikować", "securityKeyRegisterDescription": "Podłącz swój klucz bezpieczeństwa i wprowadź nazwę, aby go zidentyfikować",
@@ -1411,6 +1424,7 @@
"externalProxyEnabled": "Zewnętrzny Proxy Włączony", "externalProxyEnabled": "Zewnętrzny Proxy Włączony",
"addNewTarget": "Dodaj nowy cel", "addNewTarget": "Dodaj nowy cel",
"targetsList": "Lista celów", "targetsList": "Lista celów",
"advancedMode": "Tryb zaawansowany",
"targetErrorDuplicateTargetFound": "Znaleziono duplikat celu", "targetErrorDuplicateTargetFound": "Znaleziono duplikat celu",
"healthCheckHealthy": "Zdrowy", "healthCheckHealthy": "Zdrowy",
"healthCheckUnhealthy": "Niezdrowy", "healthCheckUnhealthy": "Niezdrowy",
@@ -1543,8 +1557,8 @@
"autoLoginError": "Błąd automatycznego logowania", "autoLoginError": "Błąd automatycznego logowania",
"autoLoginErrorNoRedirectUrl": "Nie otrzymano URL przekierowania od dostawcy tożsamości.", "autoLoginErrorNoRedirectUrl": "Nie otrzymano URL przekierowania od dostawcy tożsamości.",
"autoLoginErrorGeneratingUrl": "Nie udało się wygenerować URL uwierzytelniania.", "autoLoginErrorGeneratingUrl": "Nie udało się wygenerować URL uwierzytelniania.",
"remoteExitNodeManageRemoteExitNodes": "Zarządzaj Samodzielnie-Hostingowane", "remoteExitNodeManageRemoteExitNodes": "Zdalne węzły",
"remoteExitNodeDescription": "Zarządzaj węzłami w celu rozszerzenia połączenia z siecią", "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
"remoteExitNodes": "Węzły", "remoteExitNodes": "Węzły",
"searchRemoteExitNodes": "Szukaj węzłów...", "searchRemoteExitNodes": "Szukaj węzłów...",
"remoteExitNodeAdd": "Dodaj węzeł", "remoteExitNodeAdd": "Dodaj węzeł",
@@ -1554,7 +1568,7 @@
"remoteExitNodeMessageConfirm": "Aby potwierdzić, wpisz nazwę węzła poniżej.", "remoteExitNodeMessageConfirm": "Aby potwierdzić, wpisz nazwę węzła poniżej.",
"remoteExitNodeConfirmDelete": "Potwierdź usunięcie węzła", "remoteExitNodeConfirmDelete": "Potwierdź usunięcie węzła",
"remoteExitNodeDelete": "Usuń węzeł", "remoteExitNodeDelete": "Usuń węzeł",
"sidebarRemoteExitNodes": "Węzły", "sidebarRemoteExitNodes": "Zdalne węzły",
"remoteExitNodeCreate": { "remoteExitNodeCreate": {
"title": "Utwórz węzeł", "title": "Utwórz węzeł",
"description": "Utwórz nowy węzeł, aby rozszerzyć połączenie z siecią", "description": "Utwórz nowy węzeł, aby rozszerzyć połączenie z siecią",
@@ -1723,5 +1737,161 @@
"authPageUpdated": "Strona uwierzytelniania została pomyślnie zaktualizowana", "authPageUpdated": "Strona uwierzytelniania została pomyślnie zaktualizowana",
"healthCheckNotAvailable": "Lokalny", "healthCheckNotAvailable": "Lokalny",
"rewritePath": "Przepis Ścieżki", "rewritePath": "Przepis Ścieżki",
"rewritePathDescription": "Opcjonalnie przepisz ścieżkę przed przesłaniem do celu." "rewritePathDescription": "Opcjonalnie przepisz ścieżkę przed przesłaniem do celu.",
"continueToApplication": "Kontynuuj do aplikacji",
"checkingInvite": "Sprawdzanie zaproszenia",
"setResourceHeaderAuth": "setResourceHeaderAuth",
"resourceHeaderAuthRemove": "Usuń autoryzację nagłówka",
"resourceHeaderAuthRemoveDescription": "Uwierzytelnianie nagłówka zostało pomyślnie usunięte.",
"resourceErrorHeaderAuthRemove": "Nie udało się usunąć uwierzytelniania nagłówka",
"resourceErrorHeaderAuthRemoveDescription": "Nie można usunąć uwierzytelniania nagłówka zasobu.",
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
"headerAuthRemove": "Remove Header Auth",
"headerAuthAdd": "Add Header Auth",
"resourceErrorHeaderAuthSetup": "Nie udało się ustawić uwierzytelniania nagłówka",
"resourceErrorHeaderAuthSetupDescription": "Nie można ustawić uwierzytelniania nagłówka dla zasobu.",
"resourceHeaderAuthSetup": "Uwierzytelnianie nagłówka ustawione pomyślnie",
"resourceHeaderAuthSetupDescription": "Uwierzytelnianie nagłówka zostało ustawione.",
"resourceHeaderAuthSetupTitle": "Ustaw uwierzytelnianie nagłówka",
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
"resourceHeaderAuthSubmit": "Ustaw uwierzytelnianie nagłówka",
"actionSetResourceHeaderAuth": "Ustaw uwierzytelnianie nagłówka",
"enterpriseEdition": "Enterprise Edition",
"unlicensed": "Unlicensed",
"beta": "Beta",
"manageClients": "Manage Clients",
"manageClientsDescription": "Clients are devices that can connect to your sites",
"licenseTableValidUntil": "Valid Until",
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
"sidebarEnterpriseLicenses": "Licenses",
"generateLicenseKey": "Generate License Key",
"generateLicenseKeyForm": {
"validation": {
"emailRequired": "Please enter a valid email address",
"useCaseTypeRequired": "Please select a use case type",
"firstNameRequired": "First name is required",
"lastNameRequired": "Last name is required",
"primaryUseRequired": "Please describe your primary use",
"jobTitleRequiredBusiness": "Job title is required for business use",
"industryRequiredBusiness": "Industry is required for business use",
"stateProvinceRegionRequired": "State/Province/Region is required",
"postalZipCodeRequired": "Postal/ZIP Code is required",
"companyNameRequiredBusiness": "Company name is required for business use",
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
"countryRequiredPersonal": "Country is required for personal use",
"agreeToTermsRequired": "You must agree to the terms",
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
},
"useCaseOptions": {
"personal": {
"title": "Personal Use",
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
},
"business": {
"title": "Business Use",
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
}
},
"steps": {
"emailLicenseType": {
"title": "Email & License Type",
"description": "Enter your email and choose your license type"
},
"personalInformation": {
"title": "Personal Information",
"description": "Tell us about yourself"
},
"contactInformation": {
"title": "Contact Information",
"description": "Your contact details"
},
"termsGenerate": {
"title": "Terms & Generate",
"description": "Review and accept terms to generate your license"
}
},
"alerts": {
"commercialUseDisclosure": {
"title": "Usage Disclosure",
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
},
"trialPeriodInformation": {
"title": "Trial Period Information",
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
}
},
"form": {
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
"firstName": "First Name",
"lastName": "Last Name",
"jobTitle": "Job Title",
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
"industryQuestion": "What is your industry?",
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
"companyName": "Company name",
"countryOfResidence": "Country of residence",
"stateProvinceRegion": "State / Province / Region",
"postalZipCode": "Postal / ZIP Code",
"companyWebsite": "Company website",
"companyPhoneNumber": "Company phone number",
"country": "Country",
"phoneNumberOptional": "Phone number (optional)",
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
},
"buttons": {
"close": "Close",
"previous": "Previous",
"next": "Next",
"generateLicenseKey": "Generate License Key"
},
"toasts": {
"success": {
"title": "License key generated successfully",
"description": "Your license key has been generated and is ready to use."
},
"error": {
"title": "Failed to generate license key",
"description": "An error occurred while generating the license key."
}
}
},
"priority": "Priorytet",
"priorityDescription": "Najpierw oceniane są trasy priorytetowe. Priorytet = 100 oznacza automatyczne zamawianie (decyzje systemowe). Użyj innego numeru, aby wyegzekwować ręczny priorytet.",
"instanceName": "Instance Name",
"pathMatchModalTitle": "Configure Path Matching",
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
"pathMatchType": "Match Type",
"pathMatchPrefix": "Prefix",
"pathMatchExact": "Exact",
"pathMatchRegex": "Regex",
"pathMatchValue": "Path Value",
"clear": "Clear",
"saveChanges": "Save Changes",
"pathMatchRegexPlaceholder": "^/api/.*",
"pathMatchDefaultPlaceholder": "/path",
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
"pathMatchExactHelp": "Example: /api matches only /api",
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
"pathRewriteModalTitle": "Configure Path Rewriting",
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
"pathRewriteType": "Rewrite Type",
"pathRewritePrefixOption": "Prefix - Replace prefix",
"pathRewriteExactOption": "Exact - Replace entire path",
"pathRewriteRegexOption": "Regex - Pattern replacement",
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
"pathRewriteValue": "Rewrite Value",
"pathRewriteRegexPlaceholder": "/new/$1",
"pathRewriteDefaultPlaceholder": "/new-path",
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
"pathRewritePrefix": "Prefix",
"pathRewriteExact": "Exact",
"pathRewriteRegex": "Regex",
"pathRewriteStrip": "Strip",
"pathRewriteStripLabel": "strip"
} }

View File

@@ -96,7 +96,7 @@
"siteWgDescription": "Use qualquer cliente do WireGuard para estabelecer um túnel. Configuração manual NAT é necessária.", "siteWgDescription": "Use qualquer cliente do WireGuard para estabelecer um túnel. Configuração manual NAT é necessária.",
"siteWgDescriptionSaas": "Use qualquer cliente WireGuard para estabelecer um túnel. Configuração manual NAT necessária. SOMENTE FUNCIONA EM NODES AUTO-HOSPEDADOS", "siteWgDescriptionSaas": "Use qualquer cliente WireGuard para estabelecer um túnel. Configuração manual NAT necessária. SOMENTE FUNCIONA EM NODES AUTO-HOSPEDADOS",
"siteLocalDescription": "Recursos locais apenas. Sem túneis.", "siteLocalDescription": "Recursos locais apenas. Sem túneis.",
"siteLocalDescriptionSaas": "Apenas recursos locais. Sem tunelamento. SOMENTE FUNCIONA EM NODES AUTO-HOSPEDADOS", "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
"siteSeeAll": "Ver todos os sites", "siteSeeAll": "Ver todos os sites",
"siteTunnelDescription": "Determine como você deseja se conectar ao seu site", "siteTunnelDescription": "Determine como você deseja se conectar ao seu site",
"siteNewtCredentials": "Credenciais Novas", "siteNewtCredentials": "Credenciais Novas",
@@ -468,7 +468,10 @@
"createdAt": "Criado Em", "createdAt": "Criado Em",
"proxyErrorInvalidHeader": "Valor do cabeçalho Host personalizado inválido. Use o formato de nome de domínio ou salve vazio para remover o cabeçalho Host personalizado.", "proxyErrorInvalidHeader": "Valor do cabeçalho Host personalizado inválido. Use o formato de nome de domínio ou salve vazio para remover o cabeçalho Host personalizado.",
"proxyErrorTls": "Nome do Servidor TLS inválido. Use o formato de nome de domínio ou salve vazio para remover o Nome do Servidor TLS.", "proxyErrorTls": "Nome do Servidor TLS inválido. Use o formato de nome de domínio ou salve vazio para remover o Nome do Servidor TLS.",
"proxyEnableSSL": "Habilitar SSL (https)", "proxyEnableSSL": "Habilitar SSL",
"proxyEnableSSLDescription": "Habilitar criptografia SSL/TLS para conexões HTTPS seguras a seus alvos.",
"target": "Target",
"configureTarget": "Configurar Alvos",
"targetErrorFetch": "Falha ao buscar alvos", "targetErrorFetch": "Falha ao buscar alvos",
"targetErrorFetchDescription": "Ocorreu um erro ao buscar alvos", "targetErrorFetchDescription": "Ocorreu um erro ao buscar alvos",
"siteErrorFetch": "Falha ao buscar recurso", "siteErrorFetch": "Falha ao buscar recurso",
@@ -495,7 +498,7 @@
"targetTlsSettings": "Configuração de conexão segura", "targetTlsSettings": "Configuração de conexão segura",
"targetTlsSettingsDescription": "Configurar configurações SSL/TLS para seu recurso", "targetTlsSettingsDescription": "Configurar configurações SSL/TLS para seu recurso",
"targetTlsSettingsAdvanced": "Configurações TLS Avançadas", "targetTlsSettingsAdvanced": "Configurações TLS Avançadas",
"targetTlsSni": "Nome do Servidor TLS (SNI)", "targetTlsSni": "Nome do Servidor TLS",
"targetTlsSniDescription": "O Nome do Servidor TLS para usar para SNI. Deixe vazio para usar o padrão.", "targetTlsSniDescription": "O Nome do Servidor TLS para usar para SNI. Deixe vazio para usar o padrão.",
"targetTlsSubmit": "Guardar Configurações", "targetTlsSubmit": "Guardar Configurações",
"targets": "Configuração de Alvos", "targets": "Configuração de Alvos",
@@ -504,9 +507,21 @@
"targetStickySessionsDescription": "Manter conexões no mesmo alvo backend durante toda a sessão.", "targetStickySessionsDescription": "Manter conexões no mesmo alvo backend durante toda a sessão.",
"methodSelect": "Selecionar método", "methodSelect": "Selecionar método",
"targetSubmit": "Adicionar Alvo", "targetSubmit": "Adicionar Alvo",
"targetNoOne": "Sem alvos. Adicione um alvo usando o formulário.", "targetNoOne": "Este recurso não tem nenhum alvo. Adicione um alvo para configurar para onde enviar solicitações para sua área de administração.",
"targetNoOneDescription": "Adicionar mais de um alvo acima habilitará o balanceamento de carga.", "targetNoOneDescription": "Adicionar mais de um alvo acima habilitará o balanceamento de carga.",
"targetsSubmit": "Guardar Alvos", "targetsSubmit": "Guardar Alvos",
"addTarget": "Adicionar Alvo",
"targetErrorInvalidIp": "Endereço IP inválido",
"targetErrorInvalidIpDescription": "Por favor, insira um endereço IP ou nome de host válido",
"targetErrorInvalidPort": "Porta inválida",
"targetErrorInvalidPortDescription": "Por favor, digite um número de porta válido",
"targetErrorNoSite": "Nenhum site selecionado",
"targetErrorNoSiteDescription": "Selecione um site para o destino",
"targetCreated": "Destino criado",
"targetCreatedDescription": "O alvo foi criado com sucesso",
"targetErrorCreate": "Falha ao criar destino",
"targetErrorCreateDescription": "Ocorreu um erro ao criar o destino",
"save": "Guardar",
"proxyAdditional": "Configurações Adicionais de Proxy", "proxyAdditional": "Configurações Adicionais de Proxy",
"proxyAdditionalDescription": "Configure como seu recurso lida com configurações de proxy", "proxyAdditionalDescription": "Configure como seu recurso lida com configurações de proxy",
"proxyCustomHeader": "Cabeçalho Host Personalizado", "proxyCustomHeader": "Cabeçalho Host Personalizado",
@@ -715,7 +730,7 @@
"pangolinServerAdmin": "Administrador do Servidor - Pangolin", "pangolinServerAdmin": "Administrador do Servidor - Pangolin",
"licenseTierProfessional": "Licença Profissional", "licenseTierProfessional": "Licença Profissional",
"licenseTierEnterprise": "Licença Empresarial", "licenseTierEnterprise": "Licença Empresarial",
"licenseTierCommercial": "Licença comercial", "licenseTierPersonal": "Personal License",
"licensed": "Licenciado", "licensed": "Licenciado",
"yes": "Sim", "yes": "Sim",
"no": "Não", "no": "Não",
@@ -750,7 +765,7 @@
"idpDisplayName": "Um nome de exibição para este provedor de identidade", "idpDisplayName": "Um nome de exibição para este provedor de identidade",
"idpAutoProvisionUsers": "Provisionamento Automático de Utilizadores", "idpAutoProvisionUsers": "Provisionamento Automático de Utilizadores",
"idpAutoProvisionUsersDescription": "Quando ativado, os utilizadores serão criados automaticamente no sistema no primeiro login com a capacidade de mapear utilizadores para funções e organizações.", "idpAutoProvisionUsersDescription": "Quando ativado, os utilizadores serão criados automaticamente no sistema no primeiro login com a capacidade de mapear utilizadores para funções e organizações.",
"licenseBadge": "Profissional", "licenseBadge": "EE",
"idpType": "Tipo de Provedor", "idpType": "Tipo de Provedor",
"idpTypeDescription": "Selecione o tipo de provedor de identidade que deseja configurar", "idpTypeDescription": "Selecione o tipo de provedor de identidade que deseja configurar",
"idpOidcConfigure": "Configuração OAuth2/OIDC", "idpOidcConfigure": "Configuração OAuth2/OIDC",
@@ -1084,7 +1099,6 @@
"navbar": "Menu de Navegação", "navbar": "Menu de Navegação",
"navbarDescription": "Menu de navegação principal da aplicação", "navbarDescription": "Menu de navegação principal da aplicação",
"navbarDocsLink": "Documentação", "navbarDocsLink": "Documentação",
"commercialEdition": "Edição Comercial",
"otpErrorEnable": "Não foi possível ativar 2FA", "otpErrorEnable": "Não foi possível ativar 2FA",
"otpErrorEnableDescription": "Ocorreu um erro ao ativar 2FA", "otpErrorEnableDescription": "Ocorreu um erro ao ativar 2FA",
"otpSetupCheckCode": "Por favor, insira um código de 6 dígitos", "otpSetupCheckCode": "Por favor, insira um código de 6 dígitos",
@@ -1140,7 +1154,7 @@
"sidebarAllUsers": "Todos os utilizadores", "sidebarAllUsers": "Todos os utilizadores",
"sidebarIdentityProviders": "Provedores de identidade", "sidebarIdentityProviders": "Provedores de identidade",
"sidebarLicense": "Tipo:", "sidebarLicense": "Tipo:",
"sidebarClients": "Clientes (Beta)", "sidebarClients": "Clients",
"sidebarDomains": "Domínios", "sidebarDomains": "Domínios",
"enableDockerSocket": "Habilitar o Diagrama Docker", "enableDockerSocket": "Habilitar o Diagrama Docker",
"enableDockerSocketDescription": "Ativar a scraping de rótulo Docker para rótulos de diagramas. Caminho de Socket deve ser fornecido para Newt.", "enableDockerSocketDescription": "Ativar a scraping de rótulo Docker para rótulos de diagramas. Caminho de Socket deve ser fornecido para Newt.",
@@ -1333,7 +1347,6 @@
"twoFactorRequired": "A autenticação de dois fatores é necessária para registrar uma chave de segurança.", "twoFactorRequired": "A autenticação de dois fatores é necessária para registrar uma chave de segurança.",
"twoFactor": "Autenticação de Dois Fatores", "twoFactor": "Autenticação de Dois Fatores",
"adminEnabled2FaOnYourAccount": "Seu administrador ativou a autenticação de dois fatores para {email}. Complete o processo de configuração para continuar.", "adminEnabled2FaOnYourAccount": "Seu administrador ativou a autenticação de dois fatores para {email}. Complete o processo de configuração para continuar.",
"continueToApplication": "Continuar para o aplicativo",
"securityKeyAdd": "Adicionar Chave de Segurança", "securityKeyAdd": "Adicionar Chave de Segurança",
"securityKeyRegisterTitle": "Registrar Nova Chave de Segurança", "securityKeyRegisterTitle": "Registrar Nova Chave de Segurança",
"securityKeyRegisterDescription": "Conecte sua chave de segurança e insira um nome para identificá-la", "securityKeyRegisterDescription": "Conecte sua chave de segurança e insira um nome para identificá-la",
@@ -1411,6 +1424,7 @@
"externalProxyEnabled": "Proxy Externo Habilitado", "externalProxyEnabled": "Proxy Externo Habilitado",
"addNewTarget": "Adicionar Novo Alvo", "addNewTarget": "Adicionar Novo Alvo",
"targetsList": "Lista de Alvos", "targetsList": "Lista de Alvos",
"advancedMode": "Modo Avançado",
"targetErrorDuplicateTargetFound": "Alvo duplicado encontrado", "targetErrorDuplicateTargetFound": "Alvo duplicado encontrado",
"healthCheckHealthy": "Saudável", "healthCheckHealthy": "Saudável",
"healthCheckUnhealthy": "Não Saudável", "healthCheckUnhealthy": "Não Saudável",
@@ -1543,8 +1557,8 @@
"autoLoginError": "Erro de Login Automático", "autoLoginError": "Erro de Login Automático",
"autoLoginErrorNoRedirectUrl": "Nenhum URL de redirecionamento recebido do provedor de identidade.", "autoLoginErrorNoRedirectUrl": "Nenhum URL de redirecionamento recebido do provedor de identidade.",
"autoLoginErrorGeneratingUrl": "Falha ao gerar URL de autenticação.", "autoLoginErrorGeneratingUrl": "Falha ao gerar URL de autenticação.",
"remoteExitNodeManageRemoteExitNodes": "Gerenciar Auto-Hospedados", "remoteExitNodeManageRemoteExitNodes": "Nós remotos",
"remoteExitNodeDescription": "Gerencie os nós para estender sua conectividade de rede", "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
"remoteExitNodes": "Nós", "remoteExitNodes": "Nós",
"searchRemoteExitNodes": "Buscar nós...", "searchRemoteExitNodes": "Buscar nós...",
"remoteExitNodeAdd": "Adicionar node", "remoteExitNodeAdd": "Adicionar node",
@@ -1554,7 +1568,7 @@
"remoteExitNodeMessageConfirm": "Para confirmar, por favor, digite o nome do nó abaixo.", "remoteExitNodeMessageConfirm": "Para confirmar, por favor, digite o nome do nó abaixo.",
"remoteExitNodeConfirmDelete": "Confirmar exclusão do nó", "remoteExitNodeConfirmDelete": "Confirmar exclusão do nó",
"remoteExitNodeDelete": "Excluir nó", "remoteExitNodeDelete": "Excluir nó",
"sidebarRemoteExitNodes": "Nós", "sidebarRemoteExitNodes": "Nós remotos",
"remoteExitNodeCreate": { "remoteExitNodeCreate": {
"title": "Criar nó", "title": "Criar nó",
"description": "Crie um novo nó para estender sua conectividade de rede", "description": "Crie um novo nó para estender sua conectividade de rede",
@@ -1723,5 +1737,161 @@
"authPageUpdated": "Página de autenticação atualizada com sucesso", "authPageUpdated": "Página de autenticação atualizada com sucesso",
"healthCheckNotAvailable": "Localização", "healthCheckNotAvailable": "Localização",
"rewritePath": "Reescrever Caminho", "rewritePath": "Reescrever Caminho",
"rewritePathDescription": "Opcionalmente reescreva o caminho antes de encaminhar ao destino." "rewritePathDescription": "Opcionalmente reescreva o caminho antes de encaminhar ao destino.",
"continueToApplication": "Continuar para o aplicativo",
"checkingInvite": "Checando convite",
"setResourceHeaderAuth": "setResourceHeaderAuth",
"resourceHeaderAuthRemove": "Remover autenticação de cabeçalho",
"resourceHeaderAuthRemoveDescription": "Autenticação de cabeçalho removida com sucesso.",
"resourceErrorHeaderAuthRemove": "Falha ao remover autenticação de cabeçalho",
"resourceErrorHeaderAuthRemoveDescription": "Não foi possível remover a autenticação do cabeçalho para o recurso.",
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
"headerAuthRemove": "Remove Header Auth",
"headerAuthAdd": "Add Header Auth",
"resourceErrorHeaderAuthSetup": "Falha ao definir autenticação de cabeçalho",
"resourceErrorHeaderAuthSetupDescription": "Não foi possível definir a autenticação do cabeçalho para o recurso.",
"resourceHeaderAuthSetup": "Autenticação de Cabeçalho definida com sucesso",
"resourceHeaderAuthSetupDescription": "Autenticação de cabeçalho foi definida com sucesso.",
"resourceHeaderAuthSetupTitle": "Definir autenticação de cabeçalho",
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
"resourceHeaderAuthSubmit": "Definir autenticação de cabeçalho",
"actionSetResourceHeaderAuth": "Definir autenticação de cabeçalho",
"enterpriseEdition": "Enterprise Edition",
"unlicensed": "Unlicensed",
"beta": "Beta",
"manageClients": "Manage Clients",
"manageClientsDescription": "Clients are devices that can connect to your sites",
"licenseTableValidUntil": "Valid Until",
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
"sidebarEnterpriseLicenses": "Licenses",
"generateLicenseKey": "Generate License Key",
"generateLicenseKeyForm": {
"validation": {
"emailRequired": "Please enter a valid email address",
"useCaseTypeRequired": "Please select a use case type",
"firstNameRequired": "First name is required",
"lastNameRequired": "Last name is required",
"primaryUseRequired": "Please describe your primary use",
"jobTitleRequiredBusiness": "Job title is required for business use",
"industryRequiredBusiness": "Industry is required for business use",
"stateProvinceRegionRequired": "State/Province/Region is required",
"postalZipCodeRequired": "Postal/ZIP Code is required",
"companyNameRequiredBusiness": "Company name is required for business use",
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
"countryRequiredPersonal": "Country is required for personal use",
"agreeToTermsRequired": "You must agree to the terms",
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
},
"useCaseOptions": {
"personal": {
"title": "Personal Use",
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
},
"business": {
"title": "Business Use",
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
}
},
"steps": {
"emailLicenseType": {
"title": "Email & License Type",
"description": "Enter your email and choose your license type"
},
"personalInformation": {
"title": "Personal Information",
"description": "Tell us about yourself"
},
"contactInformation": {
"title": "Contact Information",
"description": "Your contact details"
},
"termsGenerate": {
"title": "Terms & Generate",
"description": "Review and accept terms to generate your license"
}
},
"alerts": {
"commercialUseDisclosure": {
"title": "Usage Disclosure",
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
},
"trialPeriodInformation": {
"title": "Trial Period Information",
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
}
},
"form": {
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
"firstName": "First Name",
"lastName": "Last Name",
"jobTitle": "Job Title",
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
"industryQuestion": "What is your industry?",
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
"companyName": "Company name",
"countryOfResidence": "Country of residence",
"stateProvinceRegion": "State / Province / Region",
"postalZipCode": "Postal / ZIP Code",
"companyWebsite": "Company website",
"companyPhoneNumber": "Company phone number",
"country": "Country",
"phoneNumberOptional": "Phone number (optional)",
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
},
"buttons": {
"close": "Close",
"previous": "Previous",
"next": "Next",
"generateLicenseKey": "Generate License Key"
},
"toasts": {
"success": {
"title": "License key generated successfully",
"description": "Your license key has been generated and is ready to use."
},
"error": {
"title": "Failed to generate license key",
"description": "An error occurred while generating the license key."
}
}
},
"priority": "Prioridade",
"priorityDescription": "Rotas de alta prioridade são avaliadas primeiro. Prioridade = 100 significa ordem automática (decisões do sistema). Use outro número para aplicar prioridade manual.",
"instanceName": "Instance Name",
"pathMatchModalTitle": "Configure Path Matching",
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
"pathMatchType": "Match Type",
"pathMatchPrefix": "Prefix",
"pathMatchExact": "Exact",
"pathMatchRegex": "Regex",
"pathMatchValue": "Path Value",
"clear": "Clear",
"saveChanges": "Save Changes",
"pathMatchRegexPlaceholder": "^/api/.*",
"pathMatchDefaultPlaceholder": "/path",
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
"pathMatchExactHelp": "Example: /api matches only /api",
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
"pathRewriteModalTitle": "Configure Path Rewriting",
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
"pathRewriteType": "Rewrite Type",
"pathRewritePrefixOption": "Prefix - Replace prefix",
"pathRewriteExactOption": "Exact - Replace entire path",
"pathRewriteRegexOption": "Regex - Pattern replacement",
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
"pathRewriteValue": "Rewrite Value",
"pathRewriteRegexPlaceholder": "/new/$1",
"pathRewriteDefaultPlaceholder": "/new-path",
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
"pathRewritePrefix": "Prefix",
"pathRewriteExact": "Exact",
"pathRewriteRegex": "Regex",
"pathRewriteStrip": "Strip",
"pathRewriteStripLabel": "strip"
} }

View File

@@ -96,7 +96,7 @@
"siteWgDescription": "Используйте любой клиент WireGuard для открытия туннеля. Требуется ручная настройка NAT.", "siteWgDescription": "Используйте любой клиент WireGuard для открытия туннеля. Требуется ручная настройка NAT.",
"siteWgDescriptionSaas": "Используйте любой клиент WireGuard для создания туннеля. Требуется ручная настройка NAT. РАБОТАЕТ ТОЛЬКО НА САМОСТОЯТЕЛЬНО РАЗМЕЩЕННЫХ УЗЛАХ", "siteWgDescriptionSaas": "Используйте любой клиент WireGuard для создания туннеля. Требуется ручная настройка NAT. РАБОТАЕТ ТОЛЬКО НА САМОСТОЯТЕЛЬНО РАЗМЕЩЕННЫХ УЗЛАХ",
"siteLocalDescription": "Только локальные ресурсы. Без туннелирования.", "siteLocalDescription": "Только локальные ресурсы. Без туннелирования.",
"siteLocalDescriptionSaas": "Только локальные ресурсы. Без туннелирования. РАБОТАЕТ ТОЛЬКО НА САМОСТОЯТЕЛЬНО РАЗМЕЩЕННЫХ УЗЛАХ", "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
"siteSeeAll": "Просмотреть все сайты", "siteSeeAll": "Просмотреть все сайты",
"siteTunnelDescription": "Выберите способ подключения к вашему сайту", "siteTunnelDescription": "Выберите способ подключения к вашему сайту",
"siteNewtCredentials": "Учётные данные Newt", "siteNewtCredentials": "Учётные данные Newt",
@@ -468,7 +468,10 @@
"createdAt": "Создано в", "createdAt": "Создано в",
"proxyErrorInvalidHeader": "Неверное значение пользовательского заголовка Host. Используйте формат доменного имени или оставьте пустым для сброса пользовательского заголовка Host.", "proxyErrorInvalidHeader": "Неверное значение пользовательского заголовка Host. Используйте формат доменного имени или оставьте пустым для сброса пользовательского заголовка Host.",
"proxyErrorTls": "Неверное имя TLS сервера. Используйте формат доменного имени или оставьте пустым для удаления имени TLS сервера.", "proxyErrorTls": "Неверное имя TLS сервера. Используйте формат доменного имени или оставьте пустым для удаления имени TLS сервера.",
"proxyEnableSSL": "Включить SSL (https)", "proxyEnableSSL": "Включить SSL",
"proxyEnableSSLDescription": "Включить шифрование SSL/TLS для безопасных HTTPS подключений к вашим целям.",
"target": "Target",
"configureTarget": "Настроить адресаты",
"targetErrorFetch": "Не удалось получить цели", "targetErrorFetch": "Не удалось получить цели",
"targetErrorFetchDescription": "Произошла ошибка при получении целей", "targetErrorFetchDescription": "Произошла ошибка при получении целей",
"siteErrorFetch": "Не удалось получить ресурс", "siteErrorFetch": "Не удалось получить ресурс",
@@ -495,7 +498,7 @@
"targetTlsSettings": "Конфигурация безопасного соединения", "targetTlsSettings": "Конфигурация безопасного соединения",
"targetTlsSettingsDescription": "Настройте параметры SSL/TLS для вашего ресурса", "targetTlsSettingsDescription": "Настройте параметры SSL/TLS для вашего ресурса",
"targetTlsSettingsAdvanced": "Расширенные настройки TLS", "targetTlsSettingsAdvanced": "Расширенные настройки TLS",
"targetTlsSni": "Имя TLS сервера (SNI)", "targetTlsSni": "Имя TLS сервера",
"targetTlsSniDescription": "Имя TLS сервера для использования в SNI. Оставьте пустым для использования по умолчанию.", "targetTlsSniDescription": "Имя TLS сервера для использования в SNI. Оставьте пустым для использования по умолчанию.",
"targetTlsSubmit": "Сохранить настройки", "targetTlsSubmit": "Сохранить настройки",
"targets": "Конфигурация целей", "targets": "Конфигурация целей",
@@ -504,9 +507,21 @@
"targetStickySessionsDescription": "Сохранять соединения на одной и той же целевой точке в течение всей сессии.", "targetStickySessionsDescription": "Сохранять соединения на одной и той же целевой точке в течение всей сессии.",
"methodSelect": "Выберите метод", "methodSelect": "Выберите метод",
"targetSubmit": "Добавить цель", "targetSubmit": "Добавить цель",
"targetNoOne": "Нет целей. Добавьте цель с помощью формы.", "targetNoOne": "Этот ресурс не имеет никаких целей. Добавьте цель для настройки, где отправлять запросы к вашему бэкэнду.",
"targetNoOneDescription": "Добавление более одной цели выше включит балансировку нагрузки.", "targetNoOneDescription": "Добавление более одной цели выше включит балансировку нагрузки.",
"targetsSubmit": "Сохранить цели", "targetsSubmit": "Сохранить цели",
"addTarget": "Добавить цель",
"targetErrorInvalidIp": "Неверный IP-адрес",
"targetErrorInvalidIpDescription": "Пожалуйста, введите действительный IP адрес или имя хоста",
"targetErrorInvalidPort": "Неверный порт",
"targetErrorInvalidPortDescription": "Пожалуйста, введите правильный номер порта",
"targetErrorNoSite": "Сайт не выбран",
"targetErrorNoSiteDescription": "Пожалуйста, выберите сайт для цели",
"targetCreated": "Цель создана",
"targetCreatedDescription": "Цель была успешно создана",
"targetErrorCreate": "Не удалось создать цель",
"targetErrorCreateDescription": "Произошла ошибка при создании цели",
"save": "Сохранить",
"proxyAdditional": "Дополнительные настройки прокси", "proxyAdditional": "Дополнительные настройки прокси",
"proxyAdditionalDescription": "Настройте, как ваш ресурс обрабатывает настройки прокси", "proxyAdditionalDescription": "Настройте, как ваш ресурс обрабатывает настройки прокси",
"proxyCustomHeader": "Пользовательский заголовок Host", "proxyCustomHeader": "Пользовательский заголовок Host",
@@ -715,7 +730,7 @@
"pangolinServerAdmin": "Администратор сервера - Pangolin", "pangolinServerAdmin": "Администратор сервера - Pangolin",
"licenseTierProfessional": "Профессиональная лицензия", "licenseTierProfessional": "Профессиональная лицензия",
"licenseTierEnterprise": "Корпоративная лицензия", "licenseTierEnterprise": "Корпоративная лицензия",
"licenseTierCommercial": "Коммерческая лицензия", "licenseTierPersonal": "Personal License",
"licensed": "Лицензировано", "licensed": "Лицензировано",
"yes": "Да", "yes": "Да",
"no": "Нет", "no": "Нет",
@@ -750,7 +765,7 @@
"idpDisplayName": "Отображаемое имя для этого поставщика удостоверений", "idpDisplayName": "Отображаемое имя для этого поставщика удостоверений",
"idpAutoProvisionUsers": "Автоматическое создание пользователей", "idpAutoProvisionUsers": "Автоматическое создание пользователей",
"idpAutoProvisionUsersDescription": "При включении пользователи будут автоматически создаваться в системе при первом входе с возможностью сопоставления пользователей с ролями и организациями.", "idpAutoProvisionUsersDescription": "При включении пользователи будут автоматически создаваться в системе при первом входе с возможностью сопоставления пользователей с ролями и организациями.",
"licenseBadge": "Профессиональная", "licenseBadge": "EE",
"idpType": "Тип поставщика", "idpType": "Тип поставщика",
"idpTypeDescription": "Выберите тип поставщика удостоверений, который вы хотите настроить", "idpTypeDescription": "Выберите тип поставщика удостоверений, который вы хотите настроить",
"idpOidcConfigure": "Конфигурация OAuth2/OIDC", "idpOidcConfigure": "Конфигурация OAuth2/OIDC",
@@ -1084,7 +1099,6 @@
"navbar": "Навигационное меню", "navbar": "Навигационное меню",
"navbarDescription": "Главное навигационное меню приложения", "navbarDescription": "Главное навигационное меню приложения",
"navbarDocsLink": "Документация", "navbarDocsLink": "Документация",
"commercialEdition": "Коммерческая версия",
"otpErrorEnable": "Невозможно включить 2FA", "otpErrorEnable": "Невозможно включить 2FA",
"otpErrorEnableDescription": "Произошла ошибка при включении 2FA", "otpErrorEnableDescription": "Произошла ошибка при включении 2FA",
"otpSetupCheckCode": "Пожалуйста, введите 6-значный код", "otpSetupCheckCode": "Пожалуйста, введите 6-значный код",
@@ -1140,7 +1154,7 @@
"sidebarAllUsers": "Все пользователи", "sidebarAllUsers": "Все пользователи",
"sidebarIdentityProviders": "Поставщики удостоверений", "sidebarIdentityProviders": "Поставщики удостоверений",
"sidebarLicense": "Лицензия", "sidebarLicense": "Лицензия",
"sidebarClients": "Клиенты (бета)", "sidebarClients": "Clients",
"sidebarDomains": "Домены", "sidebarDomains": "Домены",
"enableDockerSocket": "Включить чертёж Docker", "enableDockerSocket": "Включить чертёж Docker",
"enableDockerSocketDescription": "Включить scraping ярлыка Docker Socket для ярлыков чертежей. Путь к сокету должен быть предоставлен в Newt.", "enableDockerSocketDescription": "Включить scraping ярлыка Docker Socket для ярлыков чертежей. Путь к сокету должен быть предоставлен в Newt.",
@@ -1333,7 +1347,6 @@
"twoFactorRequired": "Для регистрации ключа безопасности требуется двухфакторная аутентификация.", "twoFactorRequired": "Для регистрации ключа безопасности требуется двухфакторная аутентификация.",
"twoFactor": "Двухфакторная аутентификация", "twoFactor": "Двухфакторная аутентификация",
"adminEnabled2FaOnYourAccount": "Ваш администратор включил двухфакторную аутентификацию для {email}. Пожалуйста, завершите процесс настройки, чтобы продолжить.", "adminEnabled2FaOnYourAccount": "Ваш администратор включил двухфакторную аутентификацию для {email}. Пожалуйста, завершите процесс настройки, чтобы продолжить.",
"continueToApplication": "Перейти к приложению",
"securityKeyAdd": "Добавить ключ безопасности", "securityKeyAdd": "Добавить ключ безопасности",
"securityKeyRegisterTitle": "Регистрация нового ключа безопасности", "securityKeyRegisterTitle": "Регистрация нового ключа безопасности",
"securityKeyRegisterDescription": "Подключите свой ключ безопасности и введите имя для его идентификации", "securityKeyRegisterDescription": "Подключите свой ключ безопасности и введите имя для его идентификации",
@@ -1411,6 +1424,7 @@
"externalProxyEnabled": "Внешний прокси включен", "externalProxyEnabled": "Внешний прокси включен",
"addNewTarget": "Добавить новую цель", "addNewTarget": "Добавить новую цель",
"targetsList": "Список целей", "targetsList": "Список целей",
"advancedMode": "Расширенный режим",
"targetErrorDuplicateTargetFound": "Обнаружена дублирующаяся цель", "targetErrorDuplicateTargetFound": "Обнаружена дублирующаяся цель",
"healthCheckHealthy": "Здоровый", "healthCheckHealthy": "Здоровый",
"healthCheckUnhealthy": "Нездоровый", "healthCheckUnhealthy": "Нездоровый",
@@ -1543,8 +1557,8 @@
"autoLoginError": "Ошибка автоматического входа", "autoLoginError": "Ошибка автоматического входа",
"autoLoginErrorNoRedirectUrl": "URL-адрес перенаправления не получен от провайдера удостоверения.", "autoLoginErrorNoRedirectUrl": "URL-адрес перенаправления не получен от провайдера удостоверения.",
"autoLoginErrorGeneratingUrl": "Не удалось сгенерировать URL-адрес аутентификации.", "autoLoginErrorGeneratingUrl": "Не удалось сгенерировать URL-адрес аутентификации.",
"remoteExitNodeManageRemoteExitNodes": "Управление самоуправляемым", "remoteExitNodeManageRemoteExitNodes": "Удаленные узлы",
"remoteExitNodeDescription": "Управляйте узлами для расширения сетевого подключения", "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
"remoteExitNodes": "Узлы", "remoteExitNodes": "Узлы",
"searchRemoteExitNodes": "Поиск узлов...", "searchRemoteExitNodes": "Поиск узлов...",
"remoteExitNodeAdd": "Добавить узел", "remoteExitNodeAdd": "Добавить узел",
@@ -1554,7 +1568,7 @@
"remoteExitNodeMessageConfirm": "Для подтверждения введите имя узла ниже.", "remoteExitNodeMessageConfirm": "Для подтверждения введите имя узла ниже.",
"remoteExitNodeConfirmDelete": "Подтвердите удаление узла", "remoteExitNodeConfirmDelete": "Подтвердите удаление узла",
"remoteExitNodeDelete": "Удалить узел", "remoteExitNodeDelete": "Удалить узел",
"sidebarRemoteExitNodes": "Узлы", "sidebarRemoteExitNodes": "Удаленные узлы",
"remoteExitNodeCreate": { "remoteExitNodeCreate": {
"title": "Создать узел", "title": "Создать узел",
"description": "Создайте новый узел, чтобы расширить сетевое подключение", "description": "Создайте новый узел, чтобы расширить сетевое подключение",
@@ -1723,5 +1737,161 @@
"authPageUpdated": "Страница авторизации успешно обновлена", "authPageUpdated": "Страница авторизации успешно обновлена",
"healthCheckNotAvailable": "Локальный", "healthCheckNotAvailable": "Локальный",
"rewritePath": "Переписать путь", "rewritePath": "Переписать путь",
"rewritePathDescription": "При необходимости, измените путь перед пересылкой к целевому адресу." "rewritePathDescription": "При необходимости, измените путь перед пересылкой к целевому адресу.",
"continueToApplication": "Перейти к приложению",
"checkingInvite": "Проверка приглашения",
"setResourceHeaderAuth": "установить заголовок ресурса",
"resourceHeaderAuthRemove": "Удалить проверку подлинности заголовка",
"resourceHeaderAuthRemoveDescription": "Проверка подлинности заголовка успешно удалена.",
"resourceErrorHeaderAuthRemove": "Не удалось удалить аутентификацию заголовка",
"resourceErrorHeaderAuthRemoveDescription": "Не удалось удалить проверку подлинности заголовка ресурса.",
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
"headerAuthRemove": "Remove Header Auth",
"headerAuthAdd": "Add Header Auth",
"resourceErrorHeaderAuthSetup": "Не удалось установить аутентификацию заголовка",
"resourceErrorHeaderAuthSetupDescription": "Не удалось установить проверку подлинности заголовка ресурса.",
"resourceHeaderAuthSetup": "Проверка подлинности заголовка успешно установлена",
"resourceHeaderAuthSetupDescription": "Проверка подлинности заголовка успешно установлена.",
"resourceHeaderAuthSetupTitle": "Установить проверку подлинности заголовка",
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
"resourceHeaderAuthSubmit": "Установить проверку подлинности заголовка",
"actionSetResourceHeaderAuth": "Установить проверку подлинности заголовка",
"enterpriseEdition": "Enterprise Edition",
"unlicensed": "Unlicensed",
"beta": "Beta",
"manageClients": "Manage Clients",
"manageClientsDescription": "Clients are devices that can connect to your sites",
"licenseTableValidUntil": "Valid Until",
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
"sidebarEnterpriseLicenses": "Licenses",
"generateLicenseKey": "Generate License Key",
"generateLicenseKeyForm": {
"validation": {
"emailRequired": "Please enter a valid email address",
"useCaseTypeRequired": "Please select a use case type",
"firstNameRequired": "First name is required",
"lastNameRequired": "Last name is required",
"primaryUseRequired": "Please describe your primary use",
"jobTitleRequiredBusiness": "Job title is required for business use",
"industryRequiredBusiness": "Industry is required for business use",
"stateProvinceRegionRequired": "State/Province/Region is required",
"postalZipCodeRequired": "Postal/ZIP Code is required",
"companyNameRequiredBusiness": "Company name is required for business use",
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
"countryRequiredPersonal": "Country is required for personal use",
"agreeToTermsRequired": "You must agree to the terms",
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
},
"useCaseOptions": {
"personal": {
"title": "Personal Use",
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
},
"business": {
"title": "Business Use",
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
}
},
"steps": {
"emailLicenseType": {
"title": "Email & License Type",
"description": "Enter your email and choose your license type"
},
"personalInformation": {
"title": "Personal Information",
"description": "Tell us about yourself"
},
"contactInformation": {
"title": "Contact Information",
"description": "Your contact details"
},
"termsGenerate": {
"title": "Terms & Generate",
"description": "Review and accept terms to generate your license"
}
},
"alerts": {
"commercialUseDisclosure": {
"title": "Usage Disclosure",
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
},
"trialPeriodInformation": {
"title": "Trial Period Information",
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
}
},
"form": {
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
"firstName": "First Name",
"lastName": "Last Name",
"jobTitle": "Job Title",
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
"industryQuestion": "What is your industry?",
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
"companyName": "Company name",
"countryOfResidence": "Country of residence",
"stateProvinceRegion": "State / Province / Region",
"postalZipCode": "Postal / ZIP Code",
"companyWebsite": "Company website",
"companyPhoneNumber": "Company phone number",
"country": "Country",
"phoneNumberOptional": "Phone number (optional)",
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
},
"buttons": {
"close": "Close",
"previous": "Previous",
"next": "Next",
"generateLicenseKey": "Generate License Key"
},
"toasts": {
"success": {
"title": "License key generated successfully",
"description": "Your license key has been generated and is ready to use."
},
"error": {
"title": "Failed to generate license key",
"description": "An error occurred while generating the license key."
}
}
},
"priority": "Приоритет",
"priorityDescription": "Маршруты с более высоким приоритетом оцениваются первым. Приоритет = 100 означает автоматическое упорядочение (решение системы). Используйте другой номер для обеспечения ручного приоритета.",
"instanceName": "Instance Name",
"pathMatchModalTitle": "Configure Path Matching",
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
"pathMatchType": "Match Type",
"pathMatchPrefix": "Prefix",
"pathMatchExact": "Exact",
"pathMatchRegex": "Regex",
"pathMatchValue": "Path Value",
"clear": "Clear",
"saveChanges": "Save Changes",
"pathMatchRegexPlaceholder": "^/api/.*",
"pathMatchDefaultPlaceholder": "/path",
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
"pathMatchExactHelp": "Example: /api matches only /api",
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
"pathRewriteModalTitle": "Configure Path Rewriting",
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
"pathRewriteType": "Rewrite Type",
"pathRewritePrefixOption": "Prefix - Replace prefix",
"pathRewriteExactOption": "Exact - Replace entire path",
"pathRewriteRegexOption": "Regex - Pattern replacement",
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
"pathRewriteValue": "Rewrite Value",
"pathRewriteRegexPlaceholder": "/new/$1",
"pathRewriteDefaultPlaceholder": "/new-path",
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
"pathRewritePrefix": "Prefix",
"pathRewriteExact": "Exact",
"pathRewriteRegex": "Regex",
"pathRewriteStrip": "Strip",
"pathRewriteStripLabel": "strip"
} }

View File

@@ -96,7 +96,7 @@
"siteWgDescription": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir.", "siteWgDescription": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir.",
"siteWgDescriptionSaas": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir. YALNIZCA SELF HOSTED DÜĞÜMLERDE ÇALIŞIR", "siteWgDescriptionSaas": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir. YALNIZCA SELF HOSTED DÜĞÜMLERDE ÇALIŞIR",
"siteLocalDescription": "Yalnızca yerel kaynaklar. Tünelleme yok.", "siteLocalDescription": "Yalnızca yerel kaynaklar. Tünelleme yok.",
"siteLocalDescriptionSaas": "Yalnızca yerel kaynaklar. Tünel yok. YALNIZCA SELF HOSTED DÜĞÜMLERDE ÇALIŞIR", "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
"siteSeeAll": "Tüm Siteleri Gör", "siteSeeAll": "Tüm Siteleri Gör",
"siteTunnelDescription": "Sitenize nasıl bağlanmak istediğinizi belirleyin", "siteTunnelDescription": "Sitenize nasıl bağlanmak istediğinizi belirleyin",
"siteNewtCredentials": "Newt Kimlik Bilgileri", "siteNewtCredentials": "Newt Kimlik Bilgileri",
@@ -468,7 +468,10 @@
"createdAt": "Oluşturulma Tarihi", "createdAt": "Oluşturulma Tarihi",
"proxyErrorInvalidHeader": "Geçersiz özel Ana Bilgisayar Başlığı değeri. Alan adı formatını kullanın veya özel Ana Bilgisayar Başlığını ayarlamak için boş bırakın.", "proxyErrorInvalidHeader": "Geçersiz özel Ana Bilgisayar Başlığı değeri. Alan adı formatını kullanın veya özel Ana Bilgisayar Başlığını ayarlamak için boş bırakın.",
"proxyErrorTls": "Geçersiz TLS Sunucu Adı. Alan adı formatını kullanın veya TLS Sunucu Adını kaldırmak için boş bırakılsın.", "proxyErrorTls": "Geçersiz TLS Sunucu Adı. Alan adı formatını kullanın veya TLS Sunucu Adını kaldırmak için boş bırakılsın.",
"proxyEnableSSL": "SSL'yi Etkinleştir (https)", "proxyEnableSSL": "SSL Etkinleştir",
"proxyEnableSSLDescription": "Hedeflerinize güvenli HTTPS bağlantıları için SSL/TLS şifrelemesi etkinleştirin.",
"target": "Hedef",
"configureTarget": "Hedefleri Yapılandır",
"targetErrorFetch": "Hedefleri alamadı", "targetErrorFetch": "Hedefleri alamadı",
"targetErrorFetchDescription": "Hedefler alınırken bir hata oluştu", "targetErrorFetchDescription": "Hedefler alınırken bir hata oluştu",
"siteErrorFetch": "kaynağa ulaşılamadı", "siteErrorFetch": "kaynağa ulaşılamadı",
@@ -495,7 +498,7 @@
"targetTlsSettings": "HTTPS & TLS Settings", "targetTlsSettings": "HTTPS & TLS Settings",
"targetTlsSettingsDescription": "Configure TLS settings for your resource", "targetTlsSettingsDescription": "Configure TLS settings for your resource",
"targetTlsSettingsAdvanced": "Gelişmiş TLS Ayarları", "targetTlsSettingsAdvanced": "Gelişmiş TLS Ayarları",
"targetTlsSni": "TLS Sunucu Adı (SNI)", "targetTlsSni": "TLS Sunucu Adı",
"targetTlsSniDescription": "SNI için kullanılacak TLS Sunucu Adı'", "targetTlsSniDescription": "SNI için kullanılacak TLS Sunucu Adı'",
"targetTlsSubmit": "Ayarları Kaydet", "targetTlsSubmit": "Ayarları Kaydet",
"targets": "Hedefler Konfigürasyonu", "targets": "Hedefler Konfigürasyonu",
@@ -504,9 +507,21 @@
"targetStickySessionsDescription": "Bağlantıları oturum süresince aynı arka uç hedef üzerinde tutun.", "targetStickySessionsDescription": "Bağlantıları oturum süresince aynı arka uç hedef üzerinde tutun.",
"methodSelect": "Yöntemi Seç", "methodSelect": "Yöntemi Seç",
"targetSubmit": "Hedef Ekle", "targetSubmit": "Hedef Ekle",
"targetNoOne": "Hiçbir hedef yok. Formu kullanarak bir hedef ekleyin.", "targetNoOne": "Bu kaynağın hedefi yok. Arka planınıza istek göndereceğiniz bir hedef yapılandırmak için hedef ekleyin.",
"targetNoOneDescription": "Yukarıdaki birden fazla hedef ekleyerek yük dengeleme etkinleştirilecektir.", "targetNoOneDescription": "Yukarıdaki birden fazla hedef ekleyerek yük dengeleme etkinleştirilecektir.",
"targetsSubmit": "Hedefleri Kaydet", "targetsSubmit": "Hedefleri Kaydet",
"addTarget": "Hedef Ekle",
"targetErrorInvalidIp": "Geçersiz IP adresi",
"targetErrorInvalidIpDescription": "Lütfen geçerli bir IP adresi veya host adı girin",
"targetErrorInvalidPort": "Geçersiz port",
"targetErrorInvalidPortDescription": "Lütfen geçerli bir port numarası girin",
"targetErrorNoSite": "Hiçbir site seçili değil",
"targetErrorNoSiteDescription": "Lütfen hedef için bir site seçin",
"targetCreated": "Hedef oluşturuldu",
"targetCreatedDescription": "Hedef başarıyla oluşturuldu",
"targetErrorCreate": "Hedef oluşturma başarısız oldu",
"targetErrorCreateDescription": "Hedef oluşturulurken bir hata oluştu",
"save": "Kaydet",
"proxyAdditional": "Ek Proxy Ayarları", "proxyAdditional": "Ek Proxy Ayarları",
"proxyAdditionalDescription": "Kaynağınızın proxy ayarlarını nasıl yöneteceğini yapılandırın", "proxyAdditionalDescription": "Kaynağınızın proxy ayarlarını nasıl yöneteceğini yapılandırın",
"proxyCustomHeader": "Özel Ana Bilgisayar Başlığı", "proxyCustomHeader": "Özel Ana Bilgisayar Başlığı",
@@ -715,7 +730,7 @@
"pangolinServerAdmin": "Sunucu Yöneticisi - Pangolin", "pangolinServerAdmin": "Sunucu Yöneticisi - Pangolin",
"licenseTierProfessional": "Profesyonel Lisans", "licenseTierProfessional": "Profesyonel Lisans",
"licenseTierEnterprise": "Kurumsal Lisans", "licenseTierEnterprise": "Kurumsal Lisans",
"licenseTierCommercial": "Ticari Lisans", "licenseTierPersonal": "Personal License",
"licensed": "Lisanslı", "licensed": "Lisanslı",
"yes": "Evet", "yes": "Evet",
"no": "Hayır", "no": "Hayır",
@@ -750,7 +765,7 @@
"idpDisplayName": "Bu kimlik sağlayıcı için bir görüntü adı", "idpDisplayName": "Bu kimlik sağlayıcı için bir görüntü adı",
"idpAutoProvisionUsers": "Kullanıcıları Otomatik Sağla", "idpAutoProvisionUsers": "Kullanıcıları Otomatik Sağla",
"idpAutoProvisionUsersDescription": "Etkinleştirildiğinde, kullanıcılar rol ve organizasyonlara eşleme yeteneğiyle birlikte sistemde otomatik olarak oluşturulacak.", "idpAutoProvisionUsersDescription": "Etkinleştirildiğinde, kullanıcılar rol ve organizasyonlara eşleme yeteneğiyle birlikte sistemde otomatik olarak oluşturulacak.",
"licenseBadge": "Profesyonel", "licenseBadge": "EE",
"idpType": "Sağlayıcı Türü", "idpType": "Sağlayıcı Türü",
"idpTypeDescription": "Yapılandırmak istediğiniz kimlik sağlayıcısı türünü seçin", "idpTypeDescription": "Yapılandırmak istediğiniz kimlik sağlayıcısı türünü seçin",
"idpOidcConfigure": "OAuth2/OIDC Yapılandırması", "idpOidcConfigure": "OAuth2/OIDC Yapılandırması",
@@ -1084,7 +1099,6 @@
"navbar": "Navigasyon Menüsü", "navbar": "Navigasyon Menüsü",
"navbarDescription": "Uygulamanın ana navigasyon menüsü", "navbarDescription": "Uygulamanın ana navigasyon menüsü",
"navbarDocsLink": "Dokümantasyon", "navbarDocsLink": "Dokümantasyon",
"commercialEdition": "Ticari Sürüm",
"otpErrorEnable": "2FA etkinleştirilemedi", "otpErrorEnable": "2FA etkinleştirilemedi",
"otpErrorEnableDescription": "2FA etkinleştirilirken bir hata oluştu", "otpErrorEnableDescription": "2FA etkinleştirilirken bir hata oluştu",
"otpSetupCheckCode": "6 haneli bir kod girin", "otpSetupCheckCode": "6 haneli bir kod girin",
@@ -1140,7 +1154,7 @@
"sidebarAllUsers": "Tüm Kullanıcılar", "sidebarAllUsers": "Tüm Kullanıcılar",
"sidebarIdentityProviders": "Kimlik Sağlayıcılar", "sidebarIdentityProviders": "Kimlik Sağlayıcılar",
"sidebarLicense": "Lisans", "sidebarLicense": "Lisans",
"sidebarClients": "Müşteriler (Beta)", "sidebarClients": "Clients",
"sidebarDomains": "Alan Adları", "sidebarDomains": "Alan Adları",
"enableDockerSocket": "Docker Soketini Etkinleştir", "enableDockerSocket": "Docker Soketini Etkinleştir",
"enableDockerSocketDescription": "Plan etiketleri için Docker Socket etiket toplamasını etkinleştirin. Newt'e soket yolu sağlanmalıdır.", "enableDockerSocketDescription": "Plan etiketleri için Docker Socket etiket toplamasını etkinleştirin. Newt'e soket yolu sağlanmalıdır.",
@@ -1333,7 +1347,6 @@
"twoFactorRequired": "Güvenlik anahtarını kaydetmek için iki faktörlü kimlik doğrulama gereklidir.", "twoFactorRequired": "Güvenlik anahtarını kaydetmek için iki faktörlü kimlik doğrulama gereklidir.",
"twoFactor": "İki Faktörlü Kimlik Doğrulama", "twoFactor": "İki Faktörlü Kimlik Doğrulama",
"adminEnabled2FaOnYourAccount": "Yöneticiniz {email} için iki faktörlü kimlik doğrulamayı etkinleştirdi. Devam etmek için kurulum işlemini tamamlayın.", "adminEnabled2FaOnYourAccount": "Yöneticiniz {email} için iki faktörlü kimlik doğrulamayı etkinleştirdi. Devam etmek için kurulum işlemini tamamlayın.",
"continueToApplication": "Uygulamaya Devam Et",
"securityKeyAdd": "Güvenlik Anahtarı Ekle", "securityKeyAdd": "Güvenlik Anahtarı Ekle",
"securityKeyRegisterTitle": "Yeni Güvenlik Anahtarı Kaydet", "securityKeyRegisterTitle": "Yeni Güvenlik Anahtarı Kaydet",
"securityKeyRegisterDescription": "Güvenlik anahtarınızı bağlayın ve tanımlamak için bir ad girin", "securityKeyRegisterDescription": "Güvenlik anahtarınızı bağlayın ve tanımlamak için bir ad girin",
@@ -1411,6 +1424,7 @@
"externalProxyEnabled": "Dış Proxy Etkinleştirildi", "externalProxyEnabled": "Dış Proxy Etkinleştirildi",
"addNewTarget": "Yeni Hedef Ekle", "addNewTarget": "Yeni Hedef Ekle",
"targetsList": "Hedefler Listesi", "targetsList": "Hedefler Listesi",
"advancedMode": "Gelişmiş Mod",
"targetErrorDuplicateTargetFound": "Yinelenen hedef bulundu", "targetErrorDuplicateTargetFound": "Yinelenen hedef bulundu",
"healthCheckHealthy": "Sağlıklı", "healthCheckHealthy": "Sağlıklı",
"healthCheckUnhealthy": "Sağlıksız", "healthCheckUnhealthy": "Sağlıksız",
@@ -1543,8 +1557,8 @@
"autoLoginError": "Otomatik Giriş Hatası", "autoLoginError": "Otomatik Giriş Hatası",
"autoLoginErrorNoRedirectUrl": "Kimlik sağlayıcıdan yönlendirme URL'si alınamadı.", "autoLoginErrorNoRedirectUrl": "Kimlik sağlayıcıdan yönlendirme URL'si alınamadı.",
"autoLoginErrorGeneratingUrl": "Kimlik doğrulama URL'si oluşturulamadı.", "autoLoginErrorGeneratingUrl": "Kimlik doğrulama URL'si oluşturulamadı.",
"remoteExitNodeManageRemoteExitNodes": "Öz-Host Yönetim", "remoteExitNodeManageRemoteExitNodes": "Uzak Düğümler",
"remoteExitNodeDescription": "Ağ bağlantınızı genişletmek için düğümleri yönetin", "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
"remoteExitNodes": "Düğümler", "remoteExitNodes": "Düğümler",
"searchRemoteExitNodes": "Düğüm ara...", "searchRemoteExitNodes": "Düğüm ara...",
"remoteExitNodeAdd": "Düğüm Ekle", "remoteExitNodeAdd": "Düğüm Ekle",
@@ -1554,7 +1568,7 @@
"remoteExitNodeMessageConfirm": "Onaylamak için lütfen aşağıya düğümün adını yazın.", "remoteExitNodeMessageConfirm": "Onaylamak için lütfen aşağıya düğümün adını yazın.",
"remoteExitNodeConfirmDelete": "Düğüm Silmeyi Onayla", "remoteExitNodeConfirmDelete": "Düğüm Silmeyi Onayla",
"remoteExitNodeDelete": "Düğümü Sil", "remoteExitNodeDelete": "Düğümü Sil",
"sidebarRemoteExitNodes": "Düğümler", "sidebarRemoteExitNodes": "Uzak Düğümler",
"remoteExitNodeCreate": { "remoteExitNodeCreate": {
"title": "Düğüm Oluştur", "title": "Düğüm Oluştur",
"description": "Ağ bağlantınızı genişletmek için yeni bir düğüm oluşturun", "description": "Ağ bağlantınızı genişletmek için yeni bir düğüm oluşturun",
@@ -1723,5 +1737,161 @@
"authPageUpdated": "Kimlik doğrulama sayfası başarıyla güncellendi", "authPageUpdated": "Kimlik doğrulama sayfası başarıyla güncellendi",
"healthCheckNotAvailable": "Yerel", "healthCheckNotAvailable": "Yerel",
"rewritePath": "Yolu Yeniden Yaz", "rewritePath": "Yolu Yeniden Yaz",
"rewritePathDescription": "Seçenek olarak hedefe iletmeden önce yolu yeniden yazın." "rewritePathDescription": "Seçenek olarak hedefe iletmeden önce yolu yeniden yazın.",
"continueToApplication": "Uygulamaya Devam Et",
"checkingInvite": "Davet Kontrol Ediliyor",
"setResourceHeaderAuth": "setResourceHeaderAuth",
"resourceHeaderAuthRemove": "Başlık Kimlik Doğrulama Kaldır",
"resourceHeaderAuthRemoveDescription": "Başlık kimlik doğrulama başarıyla kaldırıldı.",
"resourceErrorHeaderAuthRemove": "Başlık Kimlik Doğrulama kaldırılamadı",
"resourceErrorHeaderAuthRemoveDescription": "Kaynak için başlık kimlik doğrulaması kaldırılamadı.",
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
"headerAuthRemove": "Remove Header Auth",
"headerAuthAdd": "Add Header Auth",
"resourceErrorHeaderAuthSetup": "Başlık Kimlik Doğrulama ayarlanamadı",
"resourceErrorHeaderAuthSetupDescription": "Kaynak için başlık kimlik doğrulaması ayarlanamadı.",
"resourceHeaderAuthSetup": "Başlık Kimlik Doğrulama başarıyla ayarlandı",
"resourceHeaderAuthSetupDescription": "Başlık kimlik doğrulaması başarıyla ayarlandı.",
"resourceHeaderAuthSetupTitle": "Başlık Kimlik Doğrulama Ayarla",
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
"resourceHeaderAuthSubmit": "Başlık Kimlik Doğrulama Ayarla",
"actionSetResourceHeaderAuth": "Başlık Kimlik Doğrulama Ayarla",
"enterpriseEdition": "Enterprise Edition",
"unlicensed": "Unlicensed",
"beta": "Beta",
"manageClients": "Manage Clients",
"manageClientsDescription": "Clients are devices that can connect to your sites",
"licenseTableValidUntil": "Valid Until",
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
"sidebarEnterpriseLicenses": "Licenses",
"generateLicenseKey": "Generate License Key",
"generateLicenseKeyForm": {
"validation": {
"emailRequired": "Please enter a valid email address",
"useCaseTypeRequired": "Please select a use case type",
"firstNameRequired": "First name is required",
"lastNameRequired": "Last name is required",
"primaryUseRequired": "Please describe your primary use",
"jobTitleRequiredBusiness": "Job title is required for business use",
"industryRequiredBusiness": "Industry is required for business use",
"stateProvinceRegionRequired": "State/Province/Region is required",
"postalZipCodeRequired": "Postal/ZIP Code is required",
"companyNameRequiredBusiness": "Company name is required for business use",
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
"countryRequiredPersonal": "Country is required for personal use",
"agreeToTermsRequired": "You must agree to the terms",
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
},
"useCaseOptions": {
"personal": {
"title": "Personal Use",
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
},
"business": {
"title": "Business Use",
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
}
},
"steps": {
"emailLicenseType": {
"title": "Email & License Type",
"description": "Enter your email and choose your license type"
},
"personalInformation": {
"title": "Personal Information",
"description": "Tell us about yourself"
},
"contactInformation": {
"title": "Contact Information",
"description": "Your contact details"
},
"termsGenerate": {
"title": "Terms & Generate",
"description": "Review and accept terms to generate your license"
}
},
"alerts": {
"commercialUseDisclosure": {
"title": "Usage Disclosure",
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
},
"trialPeriodInformation": {
"title": "Trial Period Information",
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
}
},
"form": {
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
"firstName": "First Name",
"lastName": "Last Name",
"jobTitle": "Job Title",
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
"industryQuestion": "What is your industry?",
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
"companyName": "Company name",
"countryOfResidence": "Country of residence",
"stateProvinceRegion": "State / Province / Region",
"postalZipCode": "Postal / ZIP Code",
"companyWebsite": "Company website",
"companyPhoneNumber": "Company phone number",
"country": "Country",
"phoneNumberOptional": "Phone number (optional)",
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
},
"buttons": {
"close": "Close",
"previous": "Previous",
"next": "Next",
"generateLicenseKey": "Generate License Key"
},
"toasts": {
"success": {
"title": "License key generated successfully",
"description": "Your license key has been generated and is ready to use."
},
"error": {
"title": "Failed to generate license key",
"description": "An error occurred while generating the license key."
}
}
},
"priority": "Öncelik",
"priorityDescription": "Daha yüksek öncelikli rotalar önce değerlendirilir. Öncelik = 100, otomatik sıralama anlamına gelir (sistem karar verir). Manuel öncelik uygulamak için başka bir numara kullanın.",
"instanceName": "Instance Name",
"pathMatchModalTitle": "Configure Path Matching",
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
"pathMatchType": "Match Type",
"pathMatchPrefix": "Prefix",
"pathMatchExact": "Exact",
"pathMatchRegex": "Regex",
"pathMatchValue": "Path Value",
"clear": "Clear",
"saveChanges": "Save Changes",
"pathMatchRegexPlaceholder": "^/api/.*",
"pathMatchDefaultPlaceholder": "/path",
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
"pathMatchExactHelp": "Example: /api matches only /api",
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
"pathRewriteModalTitle": "Configure Path Rewriting",
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
"pathRewriteType": "Rewrite Type",
"pathRewritePrefixOption": "Prefix - Replace prefix",
"pathRewriteExactOption": "Exact - Replace entire path",
"pathRewriteRegexOption": "Regex - Pattern replacement",
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
"pathRewriteValue": "Rewrite Value",
"pathRewriteRegexPlaceholder": "/new/$1",
"pathRewriteDefaultPlaceholder": "/new-path",
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
"pathRewritePrefix": "Prefix",
"pathRewriteExact": "Exact",
"pathRewriteRegex": "Regex",
"pathRewriteStrip": "Strip",
"pathRewriteStripLabel": "strip"
} }

View File

@@ -96,7 +96,7 @@
"siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。", "siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。",
"siteWgDescriptionSaas": "使用任何WireGuard客户端建立隧道。需要手动配置NAT。仅适用于自托管节点。", "siteWgDescriptionSaas": "使用任何WireGuard客户端建立隧道。需要手动配置NAT。仅适用于自托管节点。",
"siteLocalDescription": "仅限本地资源。不需要隧道。", "siteLocalDescription": "仅限本地资源。不需要隧道。",
"siteLocalDescriptionSaas": "仅本地资源。无需隧道。仅适用于自托管节点。", "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
"siteSeeAll": "查看所有站点", "siteSeeAll": "查看所有站点",
"siteTunnelDescription": "确定如何连接到您的网站", "siteTunnelDescription": "确定如何连接到您的网站",
"siteNewtCredentials": "Newt 凭据", "siteNewtCredentials": "Newt 凭据",
@@ -468,7 +468,10 @@
"createdAt": "创建于", "createdAt": "创建于",
"proxyErrorInvalidHeader": "无效的自定义主机头值。使用域名格式,或将空保存为取消自定义主机头。", "proxyErrorInvalidHeader": "无效的自定义主机头值。使用域名格式,或将空保存为取消自定义主机头。",
"proxyErrorTls": "无效的 TLS 服务器名称。使用域名格式,或保存空以删除 TLS 服务器名称。", "proxyErrorTls": "无效的 TLS 服务器名称。使用域名格式,或保存空以删除 TLS 服务器名称。",
"proxyEnableSSL": "启用 SSL (https)", "proxyEnableSSL": "启用 SSL",
"proxyEnableSSLDescription": "启用 SSL/TLS 加密以确保您目标的 HTTPS 连接。",
"target": "Target",
"configureTarget": "配置目标",
"targetErrorFetch": "获取目标失败", "targetErrorFetch": "获取目标失败",
"targetErrorFetchDescription": "获取目标时出错", "targetErrorFetchDescription": "获取目标时出错",
"siteErrorFetch": "获取资源失败", "siteErrorFetch": "获取资源失败",
@@ -495,7 +498,7 @@
"targetTlsSettings": "安全连接配置", "targetTlsSettings": "安全连接配置",
"targetTlsSettingsDescription": "配置资源的 SSL/TLS 设置", "targetTlsSettingsDescription": "配置资源的 SSL/TLS 设置",
"targetTlsSettingsAdvanced": "高级TLS设置", "targetTlsSettingsAdvanced": "高级TLS设置",
"targetTlsSni": "TLS 服务器名称 (SNI)", "targetTlsSni": "TLS 服务器名称",
"targetTlsSniDescription": "SNI使用的 TLS 服务器名称。留空使用默认值。", "targetTlsSniDescription": "SNI使用的 TLS 服务器名称。留空使用默认值。",
"targetTlsSubmit": "保存设置", "targetTlsSubmit": "保存设置",
"targets": "目标配置", "targets": "目标配置",
@@ -504,9 +507,21 @@
"targetStickySessionsDescription": "将连接保持在同一个后端目标的整个会话中。", "targetStickySessionsDescription": "将连接保持在同一个后端目标的整个会话中。",
"methodSelect": "选择方法", "methodSelect": "选择方法",
"targetSubmit": "添加目标", "targetSubmit": "添加目标",
"targetNoOne": "没有目标。使用表单添加目标。", "targetNoOne": "此资源没有任何目标。添加目标来配置向您后端发送请求的位置。",
"targetNoOneDescription": "在上面添加多个目标将启用负载平衡。", "targetNoOneDescription": "在上面添加多个目标将启用负载平衡。",
"targetsSubmit": "保存目标", "targetsSubmit": "保存目标",
"addTarget": "添加目标",
"targetErrorInvalidIp": "无效的 IP 地址",
"targetErrorInvalidIpDescription": "请输入有效的IP地址或主机名",
"targetErrorInvalidPort": "无效的端口",
"targetErrorInvalidPortDescription": "请输入有效的端口号",
"targetErrorNoSite": "没有选择站点",
"targetErrorNoSiteDescription": "请选择目标站点",
"targetCreated": "目标已创建",
"targetCreatedDescription": "目标已成功创建",
"targetErrorCreate": "创建目标失败",
"targetErrorCreateDescription": "创建目标时出错",
"save": "保存",
"proxyAdditional": "附加代理设置", "proxyAdditional": "附加代理设置",
"proxyAdditionalDescription": "配置你的资源如何处理代理设置", "proxyAdditionalDescription": "配置你的资源如何处理代理设置",
"proxyCustomHeader": "自定义主机标题", "proxyCustomHeader": "自定义主机标题",
@@ -715,7 +730,7 @@
"pangolinServerAdmin": "服务器管理员 - Pangolin", "pangolinServerAdmin": "服务器管理员 - Pangolin",
"licenseTierProfessional": "专业许可证", "licenseTierProfessional": "专业许可证",
"licenseTierEnterprise": "企业许可证", "licenseTierEnterprise": "企业许可证",
"licenseTierCommercial": "商业许可证", "licenseTierPersonal": "Personal License",
"licensed": "已授权", "licensed": "已授权",
"yes": "是", "yes": "是",
"no": "否", "no": "否",
@@ -750,7 +765,7 @@
"idpDisplayName": "此身份提供商的显示名称", "idpDisplayName": "此身份提供商的显示名称",
"idpAutoProvisionUsers": "自动提供用户", "idpAutoProvisionUsers": "自动提供用户",
"idpAutoProvisionUsersDescription": "如果启用,用户将在首次登录时自动在系统中创建,并且能够映射用户到角色和组织。", "idpAutoProvisionUsersDescription": "如果启用,用户将在首次登录时自动在系统中创建,并且能够映射用户到角色和组织。",
"licenseBadge": "专业版", "licenseBadge": "EE",
"idpType": "提供者类型", "idpType": "提供者类型",
"idpTypeDescription": "选择您想要配置的身份提供者类型", "idpTypeDescription": "选择您想要配置的身份提供者类型",
"idpOidcConfigure": "OAuth2/OIDC 配置", "idpOidcConfigure": "OAuth2/OIDC 配置",
@@ -1084,7 +1099,6 @@
"navbar": "导航菜单", "navbar": "导航菜单",
"navbarDescription": "应用程序的主导航菜单", "navbarDescription": "应用程序的主导航菜单",
"navbarDocsLink": "文件", "navbarDocsLink": "文件",
"commercialEdition": "商业版",
"otpErrorEnable": "无法启用 2FA", "otpErrorEnable": "无法启用 2FA",
"otpErrorEnableDescription": "启用 2FA 时出错", "otpErrorEnableDescription": "启用 2FA 时出错",
"otpSetupCheckCode": "请输入您的6位数字代码", "otpSetupCheckCode": "请输入您的6位数字代码",
@@ -1140,7 +1154,7 @@
"sidebarAllUsers": "所有用户", "sidebarAllUsers": "所有用户",
"sidebarIdentityProviders": "身份提供商", "sidebarIdentityProviders": "身份提供商",
"sidebarLicense": "证书", "sidebarLicense": "证书",
"sidebarClients": "客户端(测试版)", "sidebarClients": "Clients",
"sidebarDomains": "域", "sidebarDomains": "域",
"enableDockerSocket": "启用 Docker 蓝图", "enableDockerSocket": "启用 Docker 蓝图",
"enableDockerSocketDescription": "启用 Docker Socket 标签擦除蓝图标签。套接字路径必须提供给新的。", "enableDockerSocketDescription": "启用 Docker Socket 标签擦除蓝图标签。套接字路径必须提供给新的。",
@@ -1333,7 +1347,6 @@
"twoFactorRequired": "注册安全密钥需要两步验证。", "twoFactorRequired": "注册安全密钥需要两步验证。",
"twoFactor": "两步验证", "twoFactor": "两步验证",
"adminEnabled2FaOnYourAccount": "管理员已为{email}启用两步验证。请完成设置以继续。", "adminEnabled2FaOnYourAccount": "管理员已为{email}启用两步验证。请完成设置以继续。",
"continueToApplication": "继续应用",
"securityKeyAdd": "添加安全密钥", "securityKeyAdd": "添加安全密钥",
"securityKeyRegisterTitle": "注册新安全密钥", "securityKeyRegisterTitle": "注册新安全密钥",
"securityKeyRegisterDescription": "连接您的安全密钥并输入名称以便识别", "securityKeyRegisterDescription": "连接您的安全密钥并输入名称以便识别",
@@ -1411,6 +1424,7 @@
"externalProxyEnabled": "外部代理已启用", "externalProxyEnabled": "外部代理已启用",
"addNewTarget": "添加新目标", "addNewTarget": "添加新目标",
"targetsList": "目标列表", "targetsList": "目标列表",
"advancedMode": "高级模式",
"targetErrorDuplicateTargetFound": "找到重复的目标", "targetErrorDuplicateTargetFound": "找到重复的目标",
"healthCheckHealthy": "正常", "healthCheckHealthy": "正常",
"healthCheckUnhealthy": "不正常", "healthCheckUnhealthy": "不正常",
@@ -1543,8 +1557,8 @@
"autoLoginError": "自动登录错误", "autoLoginError": "自动登录错误",
"autoLoginErrorNoRedirectUrl": "未从身份提供商收到重定向URL。", "autoLoginErrorNoRedirectUrl": "未从身份提供商收到重定向URL。",
"autoLoginErrorGeneratingUrl": "生成身份验证URL失败。", "autoLoginErrorGeneratingUrl": "生成身份验证URL失败。",
"remoteExitNodeManageRemoteExitNodes": "管理自托管", "remoteExitNodeManageRemoteExitNodes": "远程节点",
"remoteExitNodeDescription": "管理节点以扩展您的网络连接", "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
"remoteExitNodes": "节点", "remoteExitNodes": "节点",
"searchRemoteExitNodes": "搜索节点...", "searchRemoteExitNodes": "搜索节点...",
"remoteExitNodeAdd": "添加节点", "remoteExitNodeAdd": "添加节点",
@@ -1554,7 +1568,7 @@
"remoteExitNodeMessageConfirm": "要确认,请输入以下节点的名称。", "remoteExitNodeMessageConfirm": "要确认,请输入以下节点的名称。",
"remoteExitNodeConfirmDelete": "确认删除节点", "remoteExitNodeConfirmDelete": "确认删除节点",
"remoteExitNodeDelete": "删除节点", "remoteExitNodeDelete": "删除节点",
"sidebarRemoteExitNodes": "节点", "sidebarRemoteExitNodes": "远程节点",
"remoteExitNodeCreate": { "remoteExitNodeCreate": {
"title": "创建节点", "title": "创建节点",
"description": "创建一个新节点来扩展您的网络连接", "description": "创建一个新节点来扩展您的网络连接",
@@ -1723,5 +1737,161 @@
"authPageUpdated": "身份验证页面更新成功", "authPageUpdated": "身份验证页面更新成功",
"healthCheckNotAvailable": "本地的", "healthCheckNotAvailable": "本地的",
"rewritePath": "重写路径", "rewritePath": "重写路径",
"rewritePathDescription": "在转发到目标之前,可以选择重写路径。" "rewritePathDescription": "在转发到目标之前,可以选择重写路径。",
"continueToApplication": "继续应用",
"checkingInvite": "正在检查邀请",
"setResourceHeaderAuth": "设置 ResourceHeaderAuth",
"resourceHeaderAuthRemove": "删除头部认证",
"resourceHeaderAuthRemoveDescription": "已成功删除头部身份验证。",
"resourceErrorHeaderAuthRemove": "删除头部身份验证失败",
"resourceErrorHeaderAuthRemoveDescription": "无法删除资源的头部身份验证。",
"resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
"resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
"headerAuthRemove": "Remove Header Auth",
"headerAuthAdd": "Add Header Auth",
"resourceErrorHeaderAuthSetup": "设置页眉认证失败",
"resourceErrorHeaderAuthSetupDescription": "无法设置资源的头部身份验证。",
"resourceHeaderAuthSetup": "头部认证设置成功",
"resourceHeaderAuthSetupDescription": "头部认证已成功设置。",
"resourceHeaderAuthSetupTitle": "设置头部身份验证",
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
"resourceHeaderAuthSubmit": "设置头部身份验证",
"actionSetResourceHeaderAuth": "设置头部身份验证",
"enterpriseEdition": "Enterprise Edition",
"unlicensed": "Unlicensed",
"beta": "Beta",
"manageClients": "Manage Clients",
"manageClientsDescription": "Clients are devices that can connect to your sites",
"licenseTableValidUntil": "Valid Until",
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
"sidebarEnterpriseLicenses": "Licenses",
"generateLicenseKey": "Generate License Key",
"generateLicenseKeyForm": {
"validation": {
"emailRequired": "Please enter a valid email address",
"useCaseTypeRequired": "Please select a use case type",
"firstNameRequired": "First name is required",
"lastNameRequired": "Last name is required",
"primaryUseRequired": "Please describe your primary use",
"jobTitleRequiredBusiness": "Job title is required for business use",
"industryRequiredBusiness": "Industry is required for business use",
"stateProvinceRegionRequired": "State/Province/Region is required",
"postalZipCodeRequired": "Postal/ZIP Code is required",
"companyNameRequiredBusiness": "Company name is required for business use",
"countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
"countryRequiredPersonal": "Country is required for personal use",
"agreeToTermsRequired": "You must agree to the terms",
"complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
},
"useCaseOptions": {
"personal": {
"title": "Personal Use",
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
},
"business": {
"title": "Business Use",
"description": "For use within organizations, companies, or commercial or revenue-generating activities."
}
},
"steps": {
"emailLicenseType": {
"title": "Email & License Type",
"description": "Enter your email and choose your license type"
},
"personalInformation": {
"title": "Personal Information",
"description": "Tell us about yourself"
},
"contactInformation": {
"title": "Contact Information",
"description": "Your contact details"
},
"termsGenerate": {
"title": "Terms & Generate",
"description": "Review and accept terms to generate your license"
}
},
"alerts": {
"commercialUseDisclosure": {
"title": "Usage Disclosure",
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
},
"trialPeriodInformation": {
"title": "Trial Period Information",
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
}
},
"form": {
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
"firstName": "First Name",
"lastName": "Last Name",
"jobTitle": "Job Title",
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
"industryQuestion": "What is your industry?",
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
"companyName": "Company name",
"countryOfResidence": "Country of residence",
"stateProvinceRegion": "State / Province / Region",
"postalZipCode": "Postal / ZIP Code",
"companyWebsite": "Company website",
"companyPhoneNumber": "Company phone number",
"country": "Country",
"phoneNumberOptional": "Phone number (optional)",
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
},
"buttons": {
"close": "Close",
"previous": "Previous",
"next": "Next",
"generateLicenseKey": "Generate License Key"
},
"toasts": {
"success": {
"title": "License key generated successfully",
"description": "Your license key has been generated and is ready to use."
},
"error": {
"title": "Failed to generate license key",
"description": "An error occurred while generating the license key."
}
}
},
"priority": "优先权",
"priorityDescription": "先评估更高优先级线路。优先级 = 100意味着自动排序(系统决定). 使用另一个数字强制执行手动优先级。",
"instanceName": "Instance Name",
"pathMatchModalTitle": "Configure Path Matching",
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
"pathMatchType": "Match Type",
"pathMatchPrefix": "Prefix",
"pathMatchExact": "Exact",
"pathMatchRegex": "Regex",
"pathMatchValue": "Path Value",
"clear": "Clear",
"saveChanges": "Save Changes",
"pathMatchRegexPlaceholder": "^/api/.*",
"pathMatchDefaultPlaceholder": "/path",
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
"pathMatchExactHelp": "Example: /api matches only /api",
"pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
"pathRewriteModalTitle": "Configure Path Rewriting",
"pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
"pathRewriteType": "Rewrite Type",
"pathRewritePrefixOption": "Prefix - Replace prefix",
"pathRewriteExactOption": "Exact - Replace entire path",
"pathRewriteRegexOption": "Regex - Pattern replacement",
"pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
"pathRewriteValue": "Rewrite Value",
"pathRewriteRegexPlaceholder": "/new/$1",
"pathRewriteDefaultPlaceholder": "/new-path",
"pathRewritePrefixHelp": "Replace the matched prefix with this value",
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
"pathRewritePrefix": "Prefix",
"pathRewriteExact": "Exact",
"pathRewriteRegex": "Regex",
"pathRewriteStrip": "Strip",
"pathRewriteStripLabel": "strip"
} }

5718
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,21 +19,21 @@
"db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts", "db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts",
"db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts", "db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts",
"db:clear-migrations": "rm -rf server/migrations", "db:clear-migrations": "rm -rf server/migrations",
"set:oss": "echo 'export const build = \"oss\" as any;' > server/build.ts", "set:oss": "echo 'export const build = \"oss\" as any;' > server/build.ts && cp tsconfig.oss.json tsconfig.json",
"set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts", "set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts && cp tsconfig.saas.json tsconfig.json",
"set:enterprise": "echo 'export const build = \"enterprise\" as any;' > server/build.ts", "set:enterprise": "echo 'export const build = \"enterprise\" as any;' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json",
"set:sqlite": "echo 'export * from \"./sqlite\";' > server/db/index.ts", "set:sqlite": "echo 'export * from \"./sqlite\";' > server/db/index.ts",
"set:pg": "echo 'export * from \"./pg\";' > server/db/index.ts", "set:pg": "echo 'export * from \"./pg\";' > server/db/index.ts",
"next:build": "next build",
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs", "build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs", "build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
"start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs", "start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs",
"email": "email dev --dir server/emails/templates --port 3005", "email": "email dev --dir server/emails/templates --port 3005",
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs", "build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs"
"db:sqlite:seed-exit-node": "sqlite3 config/db/db.sqlite \"INSERT INTO exitNodes (exitNodeId, name, address, endpoint, publicKey, listenPort, reachableAt, maxConnections, online, lastPing, type, region) VALUES (null, 'test', '10.0.0.1/24', 'localhost', 'MJ44MpnWGxMZURgxW/fWXDFsejhabnEFYDo60LQwK3A=', 1234, 'http://localhost:3003', 123, 1, null, 'gerbil', null);\""
}, },
"dependencies": { "dependencies": {
"@asteasolutions/zod-to-openapi": "^7.3.4", "@asteasolutions/zod-to-openapi": "^7.3.4",
"@aws-sdk/client-s3": "3.837.0", "@aws-sdk/client-s3": "3.908.0",
"@hookform/resolvers": "5.2.2", "@hookform/resolvers": "5.2.2",
"@node-rs/argon2": "^2.0.2", "@node-rs/argon2": "^2.0.2",
"@oslojs/crypto": "1.0.1", "@oslojs/crypto": "1.0.1",
@@ -56,11 +56,11 @@
"@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toast": "1.2.15",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@react-email/components": "0.5.5", "@react-email/components": "0.5.6",
"@react-email/render": "^1.2.0", "@react-email/render": "^1.3.2",
"@react-email/tailwind": "1.2.2", "@react-email/tailwind": "1.2.2",
"@simplewebauthn/browser": "^13.2.0", "@simplewebauthn/browser": "^13.2.2",
"@simplewebauthn/server": "^13.2.1", "@simplewebauthn/server": "^13.2.2",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@tanstack/react-table": "8.21.3", "@tanstack/react-table": "8.21.3",
"arctic": "^3.7.0", "arctic": "^3.7.0",
@@ -76,7 +76,7 @@
"cors": "2.8.5", "cors": "2.8.5",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"drizzle-orm": "0.44.6", "drizzle-orm": "0.44.6",
"eslint": "9.35.0", "eslint": "9.37.0",
"eslint-config-next": "15.5.4", "eslint-config-next": "15.5.4",
"express": "5.1.0", "express": "5.1.0",
"express-rate-limit": "8.1.0", "express-rate-limit": "8.1.0",
@@ -85,40 +85,40 @@
"http-errors": "2.0.0", "http-errors": "2.0.0",
"i": "^0.3.7", "i": "^0.3.7",
"input-otp": "1.4.2", "input-otp": "1.4.2",
"ioredis": "5.6.1", "ioredis": "5.8.1",
"jmespath": "^0.16.0", "jmespath": "^0.16.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lucide-react": "^0.544.0", "lucide-react": "^0.545.0",
"maxmind": "5.0.0", "maxmind": "5.0.0",
"moment": "2.30.1", "moment": "2.30.1",
"next": "15.5.4", "next": "15.5.4",
"next-intl": "^4.3.9", "next-intl": "^4.3.12",
"next-themes": "0.4.6", "next-themes": "0.4.6",
"node-cache": "5.1.2", "node-cache": "5.1.2",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"nodemailer": "7.0.6", "nodemailer": "7.0.9",
"npm": "^11.6.1", "npm": "^11.6.2",
"oslo": "1.2.1", "oslo": "1.2.1",
"pg": "^8.16.2", "pg": "^8.16.2",
"posthog-node": "^5.8.4", "posthog-node": "^5.9.5",
"qrcode.react": "4.2.0", "qrcode.react": "4.2.0",
"react": "19.1.1", "react": "19.2.0",
"react-dom": "19.1.1", "react-dom": "19.2.0",
"react-easy-sort": "^1.7.0", "react-easy-sort": "^1.8.0",
"react-hook-form": "7.62.0", "react-hook-form": "7.65.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"rebuild": "0.1.2", "rebuild": "0.1.2",
"reodotdev": "^1.0.0", "reodotdev": "^1.0.0",
"resend": "^6.1.1", "resend": "^6.1.2",
"semver": "^7.7.2", "semver": "^7.7.3",
"stripe": "18.2.1", "stripe": "18.2.1",
"swagger-ui-express": "^5.0.1", "swagger-ui-express": "^5.0.1",
"tailwind-merge": "3.3.1", "tailwind-merge": "3.3.1",
"tw-animate-css": "^1.3.8", "tw-animate-css": "^1.3.8",
"uuid": "^13.0.0", "uuid": "^13.0.0",
"vaul": "1.1.2", "vaul": "1.1.2",
"winston": "3.17.0", "winston": "3.18.3",
"winston-daily-rotate-file": "5.0.0", "winston-daily-rotate-file": "5.0.0",
"ws": "8.18.3", "ws": "8.18.3",
"yargs": "18.0.0", "yargs": "18.0.0",
@@ -128,7 +128,7 @@
"devDependencies": { "devDependencies": {
"@dotenvx/dotenvx": "1.51.0", "@dotenvx/dotenvx": "1.51.0",
"@esbuild-plugins/tsconfig-paths": "0.1.2", "@esbuild-plugins/tsconfig-paths": "0.1.2",
"@react-email/preview-server": "4.2.12", "@react-email/preview-server": "4.3.0",
"@tailwindcss/postcss": "^4.1.14", "@tailwindcss/postcss": "^4.1.14",
"@types/better-sqlite3": "7.6.12", "@types/better-sqlite3": "7.6.12",
"@types/cookie-parser": "1.4.9", "@types/cookie-parser": "1.4.9",
@@ -139,11 +139,11 @@
"@types/jmespath": "^0.15.2", "@types/jmespath": "^0.15.2",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
"@types/node": "24.6.2", "@types/node": "24.7.2",
"@types/nodemailer": "7.0.2", "@types/nodemailer": "7.0.2",
"@types/pg": "8.15.5", "@types/pg": "8.15.5",
"@types/react": "19.1.16", "@types/react": "19.2.2",
"@types/react-dom": "19.1.9", "@types/react-dom": "19.2.1",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"@types/swagger-ui-express": "^4.1.8", "@types/swagger-ui-express": "^4.1.8",
"@types/ws": "8.18.1", "@types/ws": "8.18.1",
@@ -152,12 +152,12 @@
"esbuild": "0.25.10", "esbuild": "0.25.10",
"esbuild-node-externals": "1.18.0", "esbuild-node-externals": "1.18.0",
"postcss": "^8", "postcss": "^8",
"react-email": "4.2.12", "react-email": "4.3.0",
"tailwindcss": "^4.1.4", "tailwindcss": "^4.1.4",
"tsc-alias": "1.8.16", "tsc-alias": "1.8.16",
"tsx": "4.20.6", "tsx": "4.20.6",
"typescript": "^5", "typescript": "^5",
"typescript-eslint": "^8.45.0" "typescript-eslint": "^8.46.0"
}, },
"overrides": { "overrides": {
"emblor": { "emblor": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -7,21 +7,21 @@ import {
errorHandlerMiddleware, errorHandlerMiddleware,
notFoundMiddleware notFoundMiddleware
} from "@server/middlewares"; } from "@server/middlewares";
import { corsWithLoginPageSupport } from "@server/middlewares/private/corsWithLoginPage"; import { authenticated, unauthenticated } from "#dynamic/routers/external";
import { authenticated, unauthenticated } from "@server/routers/external"; import { router as wsRouter, handleWSUpgrade } from "#dynamic/routers/ws";
import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
import { logIncomingMiddleware } from "./middlewares/logIncoming"; import { logIncomingMiddleware } from "./middlewares/logIncoming";
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection"; import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
import helmet from "helmet"; import helmet from "helmet";
import { stripeWebhookHandler } from "@server/routers/private/billing/webhooks";
import { build } from "./build"; import { build } from "./build";
import rateLimit, { ipKeyGenerator } from "express-rate-limit"; import rateLimit, { ipKeyGenerator } from "express-rate-limit";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "./types/HttpCode"; import HttpCode from "./types/HttpCode";
import requestTimeoutMiddleware from "./middlewares/requestTimeout"; import requestTimeoutMiddleware from "./middlewares/requestTimeout";
import { createStore } from "@server/lib/private/rateLimitStore"; import { createStore } from "#dynamic/lib/rateLimitStore";
import hybridRouter from "@server/routers/private/hybrid";
import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions"; import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions";
import { corsWithLoginPageSupport } from "@server/lib/corsWithLoginPage";
import { hybridRouter } from "#dynamic/routers/hybrid";
import { billingWebhookHandler } from "#dynamic/routers/billing/webhooks";
const dev = config.isDev; const dev = config.isDev;
const externalPort = config.getRawConfig().server.external_port; const externalPort = config.getRawConfig().server.external_port;
@@ -39,32 +39,30 @@ export function createApiServer() {
apiServer.post( apiServer.post(
`${prefix}/billing/webhooks`, `${prefix}/billing/webhooks`,
express.raw({ type: "application/json" }), express.raw({ type: "application/json" }),
stripeWebhookHandler billingWebhookHandler
); );
} }
const corsConfig = config.getRawConfig().server.cors; const corsConfig = config.getRawConfig().server.cors;
const options = {
...(corsConfig?.origins
? { origin: corsConfig.origins }
: {
origin: (origin: any, callback: any) => {
callback(null, true);
}
}),
...(corsConfig?.methods && { methods: corsConfig.methods }),
...(corsConfig?.allowed_headers && {
allowedHeaders: corsConfig.allowed_headers
}),
credentials: !(corsConfig?.credentials === false)
};
if (build == "oss") { if (build == "oss" || !corsConfig) {
const options = {
...(corsConfig?.origins
? { origin: corsConfig.origins }
: {
origin: (origin: any, callback: any) => {
callback(null, true);
}
}),
...(corsConfig?.methods && { methods: corsConfig.methods }),
...(corsConfig?.allowed_headers && {
allowedHeaders: corsConfig.allowed_headers
}),
credentials: !(corsConfig?.credentials === false)
};
logger.debug("Using CORS options", options); logger.debug("Using CORS options", options);
apiServer.use(cors(options)); apiServer.use(cors(options));
} else { } else if (corsConfig) {
// Use the custom CORS middleware with loginPage support // Use the custom CORS middleware with loginPage support
apiServer.use(corsWithLoginPageSupport(corsConfig)); apiServer.use(corsWithLoginPageSupport(corsConfig));
} }

View File

@@ -4,7 +4,6 @@ import { userActions, roleActions, userOrgs } from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { sendUsageNotification } from "@server/routers/org";
export enum ActionsEnum { export enum ActionsEnum {
createOrgUser = "createOrgUser", createOrgUser = "createOrgUser",
@@ -61,6 +60,7 @@ export enum ActionsEnum {
getUser = "getUser", getUser = "getUser",
setResourcePassword = "setResourcePassword", setResourcePassword = "setResourcePassword",
setResourcePincode = "setResourcePincode", setResourcePincode = "setResourcePincode",
setResourceHeaderAuth = "setResourceHeaderAuth",
setResourceWhitelist = "setResourceWhitelist", setResourceWhitelist = "setResourceWhitelist",
getResourceWhitelist = "getResourceWhitelist", getResourceWhitelist = "getResourceWhitelist",
generateAccessToken = "generateAccessToken", generateAccessToken = "generateAccessToken",
@@ -194,7 +194,6 @@ export async function checkUserActionPermission(
return roleActionPermission.length > 0; return roleActionPermission.length > 0;
return false;
} catch (error) { } catch (error) {
console.error("Error checking user action permission:", error); console.error("Error checking user action permission:", error);
throw createHttpError( throw createHttpError(

View File

@@ -4,9 +4,6 @@ import { resourceSessions, ResourceSession } from "@server/db";
import { db } from "@server/db"; import { db } from "@server/db";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import config from "@server/lib/config"; import config from "@server/lib/config";
import axios from "axios";
import logger from "@server/logger";
import { tokenManager } from "@server/lib/tokenManager";
export const SESSION_COOKIE_NAME = export const SESSION_COOKIE_NAME =
config.getRawConfig().server.session_cookie_name; config.getRawConfig().server.session_cookie_name;
@@ -65,29 +62,6 @@ export async function validateResourceSessionToken(
token: string, token: string,
resourceId: number resourceId: number
): Promise<ResourceSessionValidationResult> { ): Promise<ResourceSessionValidationResult> {
if (config.isManagedMode()) {
try {
const response = await axios.post(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/session/validate`, {
token: token
}, await tokenManager.getAuthHeader());
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error validating resource session token in hybrid mode:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error validating resource session token in hybrid mode:", error);
}
return { resourceSession: null };
}
}
const sessionId = encodeHexLowerCase( const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(token)) sha256(new TextEncoder().encode(token))
); );

13
server/cleanup.ts Normal file
View File

@@ -0,0 +1,13 @@
import { cleanup as wsCleanup } from "@server/routers/ws";
async function cleanup() {
await wsCleanup();
process.exit(0);
}
export async function initCleanup() {
// Handle process termination
process.on("SIGTERM", () => cleanup());
process.on("SIGINT", () => cleanup());
}

View File

@@ -35,11 +35,12 @@ function createDb() {
} }
// Create connection pools instead of individual connections // Create connection pools instead of individual connections
const poolConfig = config.postgres.pool;
const primaryPool = new Pool({ const primaryPool = new Pool({
connectionString, connectionString,
max: 20, max: poolConfig?.max_connections || 20,
idleTimeoutMillis: 30000, idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
connectionTimeoutMillis: 5000, connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000,
}); });
const replicas = []; const replicas = [];
@@ -50,9 +51,9 @@ function createDb() {
for (const conn of replicaConnections) { for (const conn of replicaConnections) {
const replicaPool = new Pool({ const replicaPool = new Pool({
connectionString: conn.connection_string, connectionString: conn.connection_string,
max: 10, max: poolConfig?.max_replica_connections || 20,
idleTimeoutMillis: 30000, idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
connectionTimeoutMillis: 5000, connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000,
}); });
replicas.push(DrizzlePostgres(replicaPool)); replicas.push(DrizzlePostgres(replicaPool));
} }

View File

@@ -1,3 +1,3 @@
export * from "./driver"; export * from "./driver";
export * from "./schema"; export * from "./schema/schema";
export * from "./privateSchema"; export * from "./schema/privateSchema";

View File

@@ -1,16 +1,3 @@
/*
* 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 { import {
pgTable, pgTable,
serial, serial,

View File

@@ -381,6 +381,14 @@ export const resourcePassword = pgTable("resourcePassword", {
passwordHash: varchar("passwordHash").notNull() passwordHash: varchar("passwordHash").notNull()
}); });
export const resourceHeaderAuth = pgTable("resourceHeaderAuth", {
headerAuthId: serial("headerAuthId").primaryKey(),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
headerAuthHash: varchar("headerAuthHash").notNull()
});
export const resourceAccessToken = pgTable("resourceAccessToken", { export const resourceAccessToken = pgTable("resourceAccessToken", {
accessTokenId: varchar("accessTokenId").primaryKey(), accessTokenId: varchar("accessTokenId").primaryKey(),
orgId: varchar("orgId") orgId: varchar("orgId")
@@ -466,8 +474,6 @@ export const resourceRules = pgTable("resourceRules", {
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, { onDelete: "cascade" }),
templateRuleId: integer("templateRuleId")
.references(() => templateRules.ruleId, { onDelete: "cascade" }),
enabled: boolean("enabled").notNull().default(true), enabled: boolean("enabled").notNull().default(true),
priority: integer("priority").notNull(), priority: integer("priority").notNull(),
action: varchar("action").notNull(), // ACCEPT, DROP, PASS action: varchar("action").notNull(), // ACCEPT, DROP, PASS
@@ -475,40 +481,6 @@ export const resourceRules = pgTable("resourceRules", {
value: varchar("value").notNull() value: varchar("value").notNull()
}); });
// Rule templates (reusable rule sets)
export const ruleTemplates = pgTable("ruleTemplates", {
templateId: varchar("templateId").primaryKey(),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
name: varchar("name").notNull(),
description: varchar("description"),
createdAt: bigint("createdAt", { mode: "number" }).notNull()
});
// Rules within templates
export const templateRules = pgTable("templateRules", {
ruleId: serial("ruleId").primaryKey(),
templateId: varchar("templateId")
.notNull()
.references(() => ruleTemplates.templateId, { onDelete: "cascade" }),
enabled: boolean("enabled").notNull().default(true),
priority: integer("priority").notNull(),
action: varchar("action").notNull(), // ACCEPT, DROP
match: varchar("match").notNull(), // CIDR, IP, PATH
value: varchar("value").notNull()
});
// Template assignments to resources
export const resourceTemplates = pgTable("resourceTemplates", {
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
templateId: varchar("templateId")
.notNull()
.references(() => ruleTemplates.templateId, { onDelete: "cascade" })
});
export const supporterKey = pgTable("supporterKey", { export const supporterKey = pgTable("supporterKey", {
keyId: serial("keyId").primaryKey(), keyId: serial("keyId").primaryKey(),
key: varchar("key").notNull(), key: varchar("key").notNull(),
@@ -726,6 +698,7 @@ export type UserOrg = InferSelectModel<typeof userOrgs>;
export type ResourceSession = InferSelectModel<typeof resourceSessions>; export type ResourceSession = InferSelectModel<typeof resourceSessions>;
export type ResourcePincode = InferSelectModel<typeof resourcePincode>; export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
export type ResourcePassword = InferSelectModel<typeof resourcePassword>; export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
export type ResourceOtp = InferSelectModel<typeof resourceOtp>; export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>; export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>; export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
@@ -748,6 +721,4 @@ export type SiteResource = InferSelectModel<typeof siteResources>;
export type SetupToken = InferSelectModel<typeof setupTokens>; export type SetupToken = InferSelectModel<typeof setupTokens>;
export type HostMeta = InferSelectModel<typeof hostMeta>; export type HostMeta = InferSelectModel<typeof hostMeta>;
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>; export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
export type RuleTemplate = InferSelectModel<typeof ruleTemplates>; export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
export type TemplateRule = InferSelectModel<typeof templateRules>;
export type ResourceTemplate = InferSelectModel<typeof resourceTemplates>;

View File

@@ -6,6 +6,8 @@ import {
ResourceRule, ResourceRule,
resourcePassword, resourcePassword,
resourcePincode, resourcePincode,
resourceHeaderAuth,
ResourceHeaderAuth,
resourceRules, resourceRules,
resources, resources,
roleResources, roleResources,
@@ -15,15 +17,12 @@ import {
users users
} from "@server/db"; } from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import axios from "axios";
import config from "@server/lib/config";
import logger from "@server/logger";
import { tokenManager } from "@server/lib/tokenManager";
export type ResourceWithAuth = { export type ResourceWithAuth = {
resource: Resource | null; resource: Resource | null;
pincode: ResourcePincode | null; pincode: ResourcePincode | null;
password: ResourcePassword | null; password: ResourcePassword | null;
headerAuth: ResourceHeaderAuth | null;
}; };
export type UserSessionWithUser = { export type UserSessionWithUser = {
@@ -37,30 +36,6 @@ export type UserSessionWithUser = {
export async function getResourceByDomain( export async function getResourceByDomain(
domain: string domain: string
): Promise<ResourceWithAuth | null> { ): Promise<ResourceWithAuth | null> {
if (config.isManagedMode()) {
try {
const response = await axios.get(
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/domain/${domain}`,
await tokenManager.getAuthHeader()
);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error fetching config in verify session:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error fetching config in verify session:", error);
}
return null;
}
}
const [result] = await db const [result] = await db
.select() .select()
.from(resources) .from(resources)
@@ -72,6 +47,10 @@ export async function getResourceByDomain(
resourcePassword, resourcePassword,
eq(resourcePassword.resourceId, resources.resourceId) eq(resourcePassword.resourceId, resources.resourceId)
) )
.leftJoin(
resourceHeaderAuth,
eq(resourceHeaderAuth.resourceId, resources.resourceId)
)
.where(eq(resources.fullDomain, domain)) .where(eq(resources.fullDomain, domain))
.limit(1); .limit(1);
@@ -82,7 +61,8 @@ export async function getResourceByDomain(
return { return {
resource: result.resources, resource: result.resources,
pincode: result.resourcePincode, pincode: result.resourcePincode,
password: result.resourcePassword password: result.resourcePassword,
headerAuth: result.resourceHeaderAuth
}; };
} }
@@ -92,30 +72,6 @@ export async function getResourceByDomain(
export async function getUserSessionWithUser( export async function getUserSessionWithUser(
userSessionId: string userSessionId: string
): Promise<UserSessionWithUser | null> { ): Promise<UserSessionWithUser | null> {
if (config.isManagedMode()) {
try {
const response = await axios.get(
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/session/${userSessionId}`,
await tokenManager.getAuthHeader()
);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error fetching config in verify session:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error fetching config in verify session:", error);
}
return null;
}
}
const [res] = await db const [res] = await db
.select() .select()
.from(sessions) .from(sessions)
@@ -136,30 +92,6 @@ export async function getUserSessionWithUser(
* Get user organization role * Get user organization role
*/ */
export async function getUserOrgRole(userId: string, orgId: string) { export async function getUserOrgRole(userId: string, orgId: string) {
if (config.isManagedMode()) {
try {
const response = await axios.get(
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/org/${orgId}/role`,
await tokenManager.getAuthHeader()
);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error fetching config in verify session:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error fetching config in verify session:", error);
}
return null;
}
}
const userOrgRole = await db const userOrgRole = await db
.select() .select()
.from(userOrgs) .from(userOrgs)
@@ -176,30 +108,6 @@ export async function getRoleResourceAccess(
resourceId: number, resourceId: number,
roleId: number roleId: number
) { ) {
if (config.isManagedMode()) {
try {
const response = await axios.get(
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/role/${roleId}/resource/${resourceId}/access`,
await tokenManager.getAuthHeader()
);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error fetching config in verify session:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error fetching config in verify session:", error);
}
return null;
}
}
const roleResourceAccess = await db const roleResourceAccess = await db
.select() .select()
.from(roleResources) .from(roleResources)
@@ -221,30 +129,6 @@ export async function getUserResourceAccess(
userId: string, userId: string,
resourceId: number resourceId: number
) { ) {
if (config.isManagedMode()) {
try {
const response = await axios.get(
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/resource/${resourceId}/access`,
await tokenManager.getAuthHeader()
);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error fetching config in verify session:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error fetching config in verify session:", error);
}
return null;
}
}
const userResourceAccess = await db const userResourceAccess = await db
.select() .select()
.from(userResources) .from(userResources)
@@ -265,30 +149,6 @@ export async function getUserResourceAccess(
export async function getResourceRules( export async function getResourceRules(
resourceId: number resourceId: number
): Promise<ResourceRule[]> { ): Promise<ResourceRule[]> {
if (config.isManagedMode()) {
try {
const response = await axios.get(
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/rules`,
await tokenManager.getAuthHeader()
);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error fetching config in verify session:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error fetching config in verify session:", error);
}
return [];
}
}
const rules = await db const rules = await db
.select() .select()
.from(resourceRules) .from(resourceRules)
@@ -303,30 +163,6 @@ export async function getResourceRules(
export async function getOrgLoginPage( export async function getOrgLoginPage(
orgId: string orgId: string
): Promise<LoginPage | null> { ): Promise<LoginPage | null> {
if (config.isManagedMode()) {
try {
const response = await axios.get(
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/org/${orgId}/login-page`,
await tokenManager.getAuthHeader()
);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error fetching config in verify session:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error fetching config in verify session:", error);
}
return null;
}
}
const [result] = await db const [result] = await db
.select() .select()
.from(loginPageOrg) .from(loginPageOrg)

View File

@@ -1,6 +1,6 @@
import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3"; import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3"; import Database from "better-sqlite3";
import * as schema from "./schema"; import * as schema from "./schema/schema";
import path from "path"; import path from "path";
import fs from "fs"; import fs from "fs";
import { APP_PATH } from "@server/lib/consts"; import { APP_PATH } from "@server/lib/consts";

View File

@@ -1,3 +1,3 @@
export * from "./driver"; export * from "./driver";
export * from "./schema"; export * from "./schema/schema";
export * from "./privateSchema"; export * from "./schema/privateSchema";

View File

@@ -1,16 +1,3 @@
/*
* 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 { import {
sqliteTable, sqliteTable,
integer, integer,

View File

@@ -514,6 +514,16 @@ export const resourcePassword = sqliteTable("resourcePassword", {
passwordHash: text("passwordHash").notNull() passwordHash: text("passwordHash").notNull()
}); });
export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", {
headerAuthId: integer("headerAuthId").primaryKey({
autoIncrement: true
}),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
headerAuthHash: text("headerAuthHash").notNull()
});
export const resourceAccessToken = sqliteTable("resourceAccessToken", { export const resourceAccessToken = sqliteTable("resourceAccessToken", {
accessTokenId: text("accessTokenId").primaryKey(), accessTokenId: text("accessTokenId").primaryKey(),
orgId: text("orgId") orgId: text("orgId")
@@ -600,8 +610,6 @@ export const resourceRules = sqliteTable("resourceRules", {
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, { onDelete: "cascade" }),
templateRuleId: integer("templateRuleId")
.references(() => templateRules.ruleId, { onDelete: "cascade" }),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
priority: integer("priority").notNull(), priority: integer("priority").notNull(),
action: text("action").notNull(), // ACCEPT, DROP, PASS action: text("action").notNull(), // ACCEPT, DROP, PASS
@@ -609,40 +617,6 @@ export const resourceRules = sqliteTable("resourceRules", {
value: text("value").notNull() value: text("value").notNull()
}); });
// Rule templates (reusable rule sets)
export const ruleTemplates = sqliteTable("ruleTemplates", {
templateId: text("templateId").primaryKey(),
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
name: text("name").notNull(),
description: text("description"),
createdAt: integer("createdAt").notNull()
});
// Rules within templates
export const templateRules = sqliteTable("templateRules", {
ruleId: integer("ruleId").primaryKey({ autoIncrement: true }),
templateId: text("templateId")
.notNull()
.references(() => ruleTemplates.templateId, { onDelete: "cascade" }),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
priority: integer("priority").notNull(),
action: text("action").notNull(), // ACCEPT, DROP
match: text("match").notNull(), // CIDR, IP, PATH
value: text("value").notNull()
});
// Template assignments to resources
export const resourceTemplates = sqliteTable("resourceTemplates", {
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
templateId: text("templateId")
.notNull()
.references(() => ruleTemplates.templateId, { onDelete: "cascade" })
});
export const supporterKey = sqliteTable("supporterKey", { export const supporterKey = sqliteTable("supporterKey", {
keyId: integer("keyId").primaryKey({ autoIncrement: true }), keyId: integer("keyId").primaryKey({ autoIncrement: true }),
key: text("key").notNull(), key: text("key").notNull(),
@@ -765,6 +739,7 @@ export type UserOrg = InferSelectModel<typeof userOrgs>;
export type ResourceSession = InferSelectModel<typeof resourceSessions>; export type ResourceSession = InferSelectModel<typeof resourceSessions>;
export type ResourcePincode = InferSelectModel<typeof resourcePincode>; export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
export type ResourcePassword = InferSelectModel<typeof resourcePassword>; export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
export type ResourceOtp = InferSelectModel<typeof resourceOtp>; export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>; export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>; export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
@@ -785,6 +760,4 @@ export type OrgDomains = InferSelectModel<typeof orgDomains>;
export type SetupToken = InferSelectModel<typeof setupTokens>; export type SetupToken = InferSelectModel<typeof setupTokens>;
export type HostMeta = InferSelectModel<typeof hostMeta>; export type HostMeta = InferSelectModel<typeof hostMeta>;
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>; export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
export type RuleTemplate = InferSelectModel<typeof ruleTemplates>; export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
export type TemplateRule = InferSelectModel<typeof templateRules>;
export type ResourceTemplate = InferSelectModel<typeof resourceTemplates>;

View File

@@ -6,11 +6,6 @@ import logger from "@server/logger";
import SMTPTransport from "nodemailer/lib/smtp-transport"; import SMTPTransport from "nodemailer/lib/smtp-transport";
function createEmailClient() { function createEmailClient() {
if (config.isManagedMode()) {
// LETS NOT WORRY ABOUT EMAILS IN HYBRID
return;
}
const emailConfig = config.getRawConfig().email; const emailConfig = config.getRawConfig().email;
if (!emailConfig) { if (!emailConfig) {
logger.warn( logger.warn(

View File

@@ -2,7 +2,6 @@ import { render } from "@react-email/render";
import { ReactElement } from "react"; import { ReactElement } from "react";
import emailClient from "@server/emails"; import emailClient from "@server/emails";
import logger from "@server/logger"; import logger from "@server/logger";
import config from "@server/lib/config";
export async function sendEmail( export async function sendEmail(
template: ReactElement, template: ReactElement,
@@ -25,7 +24,7 @@ export async function sendEmail(
const emailHtml = await render(template); const emailHtml = await render(template);
const appName = config.getRawPrivateConfig().branding?.app_name || "Pangolin"; const appName = process.env.BRANDING_APP_NAME || "Pangolin"; // From the private config loading into env vars to seperate away the private config
await emailClient.sendMail({ await emailClient.sendMail({
from: { from: {

View File

@@ -1,16 +1,3 @@
/*
* 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 React from "react"; import React from "react";
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components"; import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
import { themeColors } from "./lib/theme"; import { themeColors } from "./lib/theme";

View File

@@ -1,16 +1,3 @@
/*
* 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 React from "react"; import React from "react";
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components"; import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
import { themeColors } from "./lib/theme"; import { themeColors } from "./lib/theme";

View File

@@ -1,151 +0,0 @@
import logger from "@server/logger";
import config from "@server/lib/config";
import { createWebSocketClient } from "./routers/ws/client";
import { addPeer, deletePeer } from "./routers/gerbil/peers";
import { db, exitNodes } from "./db";
import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager";
import { tokenManager } from "./lib/tokenManager";
import { APP_VERSION } from "./lib/consts";
import axios from "axios";
export async function createHybridClientServer() {
logger.info("Starting hybrid client server...");
// Start the token manager
await tokenManager.start();
const token = await tokenManager.getToken();
const monitor = new TraefikConfigManager();
await monitor.start();
// Create client
const client = createWebSocketClient(
token,
config.getRawConfig().managed!.endpoint!,
{
reconnectInterval: 5000,
pingInterval: 30000,
pingTimeout: 10000
}
);
// Register message handlers
client.registerHandler("remoteExitNode/peers/add", async (message) => {
const { publicKey, allowedIps } = message.data;
// TODO: we are getting the exit node twice here
// NOTE: there should only be one gerbil registered so...
const [exitNode] = await db.select().from(exitNodes).limit(1);
await addPeer(exitNode.exitNodeId, {
publicKey: publicKey,
allowedIps: allowedIps || []
});
});
client.registerHandler("remoteExitNode/peers/remove", async (message) => {
const { publicKey } = message.data;
// TODO: we are getting the exit node twice here
// NOTE: there should only be one gerbil registered so...
const [exitNode] = await db.select().from(exitNodes).limit(1);
await deletePeer(exitNode.exitNodeId, publicKey);
});
// /update-proxy-mapping
client.registerHandler("remoteExitNode/update-proxy-mapping", async (message) => {
try {
const [exitNode] = await db.select().from(exitNodes).limit(1);
if (!exitNode) {
logger.error("No exit node found for proxy mapping update");
return;
}
const response = await axios.post(`${exitNode.endpoint}/update-proxy-mapping`, message.data);
logger.info(`Successfully updated proxy mapping: ${response.status}`);
} catch (error) {
// pull data out of the axios error to log
if (axios.isAxiosError(error)) {
logger.error("Error updating proxy mapping:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error updating proxy mapping:", error);
}
}
});
// /update-destinations
client.registerHandler("remoteExitNode/update-destinations", async (message) => {
try {
const [exitNode] = await db.select().from(exitNodes).limit(1);
if (!exitNode) {
logger.error("No exit node found for destinations update");
return;
}
const response = await axios.post(`${exitNode.endpoint}/update-destinations`, message.data);
logger.info(`Successfully updated destinations: ${response.status}`);
} catch (error) {
// pull data out of the axios error to log
if (axios.isAxiosError(error)) {
logger.error("Error updating destinations:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error updating destinations:", error);
}
}
});
client.registerHandler("remoteExitNode/traefik/reload", async (message) => {
await monitor.HandleTraefikConfig();
});
// Listen to connection events
client.on("connect", () => {
logger.info("Connected to WebSocket server");
client.sendMessage("remoteExitNode/register", {
remoteExitNodeVersion: APP_VERSION
});
});
client.on("disconnect", () => {
logger.info("Disconnected from WebSocket server");
});
client.on("message", (message) => {
logger.info(
`Received message: ${message.type} ${JSON.stringify(message.data)}`
);
});
// Connect to the server
try {
await client.connect();
logger.info("Connection initiated");
} catch (error) {
logger.error("Failed to connect:", error);
}
// Store the ping interval stop function for cleanup if needed
const stopPingInterval = client.sendMessageInterval(
"remoteExitNode/ping",
{ timestamp: Date.now() / 1000 },
60000
); // send every minute
// Return client and cleanup function for potential use
return { client, stopPingInterval };
}

View File

@@ -5,18 +5,30 @@ import { runSetupFunctions } from "./setup";
import { createApiServer } from "./apiServer"; import { createApiServer } from "./apiServer";
import { createNextServer } from "./nextServer"; import { createNextServer } from "./nextServer";
import { createInternalServer } from "./internalServer"; import { createInternalServer } from "./internalServer";
import { ApiKey, ApiKeyOrg, RemoteExitNode, Session, User, UserOrg } from "@server/db"; import {
ApiKey,
ApiKeyOrg,
RemoteExitNode,
Session,
User,
UserOrg
} from "@server/db";
import { createIntegrationApiServer } from "./integrationApiServer"; import { createIntegrationApiServer } from "./integrationApiServer";
import { createHybridClientServer } from "./hybridServer";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { setHostMeta } from "@server/lib/hostMeta"; import { setHostMeta } from "@server/lib/hostMeta";
import { initTelemetryClient } from "./lib/telemetry.js"; import { initTelemetryClient } from "./lib/telemetry.js";
import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager.js"; import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager.js";
import { initCleanup } from "#dynamic/cleanup";
import license from "#dynamic/license/license";
async function startServers() { async function startServers() {
await setHostMeta(); await setHostMeta();
await config.initServer(); await config.initServer();
license.setServerSecret(config.getRawConfig().server.secret!);
await license.check();
await runSetupFunctions(); await runSetupFunctions();
initTelemetryClient(); initTelemetryClient();
@@ -25,16 +37,11 @@ async function startServers() {
const apiServer = createApiServer(); const apiServer = createApiServer();
const internalServer = createInternalServer(); const internalServer = createInternalServer();
let hybridClientServer;
let nextServer; let nextServer;
if (config.isManagedMode()) { nextServer = await createNextServer();
hybridClientServer = await createHybridClientServer(); if (config.getRawConfig().traefik.file_mode) {
} else { const monitor = new TraefikConfigManager();
nextServer = await createNextServer(); await monitor.start();
if (config.getRawConfig().traefik.file_mode) {
const monitor = new TraefikConfigManager();
await monitor.start();
}
} }
let integrationServer; let integrationServer;
@@ -42,12 +49,13 @@ async function startServers() {
integrationServer = createIntegrationApiServer(); integrationServer = createIntegrationApiServer();
} }
await initCleanup();
return { return {
apiServer, apiServer,
nextServer, nextServer,
internalServer, internalServer,
integrationServer, integrationServer
hybridClientServer
}; };
} }

View File

@@ -7,7 +7,7 @@ import {
errorHandlerMiddleware, errorHandlerMiddleware,
notFoundMiddleware, notFoundMiddleware,
} from "@server/middlewares"; } from "@server/middlewares";
import { authenticated, unauthenticated } from "@server/routers/integration"; import { authenticated, unauthenticated } from "#dynamic/routers/integration";
import { logIncomingMiddleware } from "./middlewares/logIncoming"; import { logIncomingMiddleware } from "./middlewares/logIncoming";
import helmet from "helmet"; import helmet from "helmet";
import swaggerUi from "swagger-ui-express"; import swaggerUi from "swagger-ui-express";

View File

@@ -8,7 +8,7 @@ import {
errorHandlerMiddleware, errorHandlerMiddleware,
notFoundMiddleware notFoundMiddleware
} from "@server/middlewares"; } from "@server/middlewares";
import internal from "@server/routers/internal"; import { internalRouter } from "#dynamic/routers/internal";
import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions"; import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions";
const internalPort = config.getRawConfig().server.internal_port; const internalPort = config.getRawConfig().server.internal_port;
@@ -23,7 +23,7 @@ export function createInternalServer() {
internalServer.use(express.json()); internalServer.use(express.json());
const prefix = `/api/v1`; const prefix = `/api/v1`;
internalServer.use(prefix, internal); internalServer.use(prefix, internalRouter);
internalServer.use(notFoundMiddleware); internalServer.use(notFoundMiddleware);
internalServer.use(errorHandlerMiddleware); internalServer.use(errorHandlerMiddleware);

View File

@@ -0,0 +1,6 @@
export async function createCustomer(
orgId: string,
email: string | null | undefined
): Promise<string | undefined> {
return;
}

View File

@@ -1,16 +1,3 @@
/*
* 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 Stripe from "stripe"; import Stripe from "stripe";
export enum FeatureId { export enum FeatureId {

View File

@@ -0,0 +1,8 @@
export async function getOrgTierData(
orgId: string
): Promise<{ tier: string | null; active: boolean }> {
let tier = null;
let active = false;
return { tier, active };
}

View File

@@ -0,0 +1,5 @@
export * from "./limitSet";
export * from "./features";
export * from "./limitsService";
export * from "./getOrgTierData";
export * from "./createCustomer";

View File

@@ -1,16 +1,3 @@
/*
* 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 { FeatureId } from "./features"; import { FeatureId } from "./features";
export type LimitSet = { export type LimitSet = {

View File

@@ -1,16 +1,3 @@
/*
* 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 { db, limits } from "@server/db"; import { db, limits } from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { LimitSet } from "./limitSet"; import { LimitSet } from "./limitSet";

View File

@@ -1,16 +1,3 @@
/*
* 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.
*/
export enum TierId { export enum TierId {
STANDARD = "standard", STANDARD = "standard",
} }

View File

@@ -1,21 +1,7 @@
/*
* 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 { eq, sql, and } from "drizzle-orm"; import { eq, sql, and } from "drizzle-orm";
import NodeCache from "node-cache"; import NodeCache from "node-cache";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { PutObjectCommand } from "@aws-sdk/client-s3"; import { PutObjectCommand } from "@aws-sdk/client-s3";
import { s3Client } from "../s3";
import * as fs from "fs/promises"; import * as fs from "fs/promises";
import * as path from "path"; import * as path from "path";
import { import {
@@ -30,10 +16,10 @@ import {
Transaction Transaction
} from "@server/db"; } from "@server/db";
import { FeatureId, getFeatureMeterId } from "./features"; import { FeatureId, getFeatureMeterId } from "./features";
import config from "@server/lib/config";
import logger from "@server/logger"; import logger from "@server/logger";
import { sendToClient } from "@server/routers/ws"; import { sendToClient } from "#dynamic/routers/ws";
import { build } from "@server/build"; import { build } from "@server/build";
import { s3Client } from "@server/lib/s3";
interface StripeEvent { interface StripeEvent {
identifier?: string; identifier?: string;
@@ -45,6 +31,17 @@ interface StripeEvent {
}; };
} }
export function noop() {
if (
build !== "saas" ||
!process.env.S3_BUCKET ||
!process.env.LOCAL_FILE_PATH
) {
return true;
}
return false;
}
export class UsageService { export class UsageService {
private cache: NodeCache; private cache: NodeCache;
private bucketName: string | undefined; private bucketName: string | undefined;
@@ -55,11 +52,13 @@ export class UsageService {
constructor() { constructor() {
this.cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL this.cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL
if (build !== "saas") { if (noop()) {
return; return;
} }
this.bucketName = config.getRawPrivateConfig().stripe?.s3Bucket; // this.bucketName = privateConfig.getRawPrivateConfig().stripe?.s3Bucket;
this.eventsDir = config.getRawPrivateConfig().stripe?.localFilePath; // this.eventsDir = privateConfig.getRawPrivateConfig().stripe?.localFilePath;
this.bucketName = process.env.S3_BUCKET || undefined;
this.eventsDir = process.env.LOCAL_FILE_PATH || undefined;
// Ensure events directory exists // Ensure events directory exists
this.initializeEventsDirectory().then(() => { this.initializeEventsDirectory().then(() => {
@@ -83,7 +82,9 @@ export class UsageService {
private async initializeEventsDirectory(): Promise<void> { private async initializeEventsDirectory(): Promise<void> {
if (!this.eventsDir) { if (!this.eventsDir) {
logger.warn("Stripe local file path is not configured, skipping events directory initialization."); logger.warn(
"Stripe local file path is not configured, skipping events directory initialization."
);
return; return;
} }
try { try {
@@ -95,7 +96,9 @@ export class UsageService {
private async uploadPendingEventFilesOnStartup(): Promise<void> { private async uploadPendingEventFilesOnStartup(): Promise<void> {
if (!this.eventsDir || !this.bucketName) { if (!this.eventsDir || !this.bucketName) {
logger.warn("Stripe local file path or bucket name is not configured, skipping leftover event file upload."); logger.warn(
"Stripe local file path or bucket name is not configured, skipping leftover event file upload."
);
return; return;
} }
try { try {
@@ -118,15 +121,17 @@ export class UsageService {
ContentType: "application/json" ContentType: "application/json"
}); });
await s3Client.send(uploadCommand); await s3Client.send(uploadCommand);
// Check if file still exists before unlinking // Check if file still exists before unlinking
try { try {
await fs.access(filePath); await fs.access(filePath);
await fs.unlink(filePath); await fs.unlink(filePath);
} catch (unlinkError) { } catch (unlinkError) {
logger.debug(`Startup file ${file} was already deleted`); logger.debug(
`Startup file ${file} was already deleted`
);
} }
logger.info( logger.info(
`Uploaded leftover event file ${file} to S3 with ${events.length} events` `Uploaded leftover event file ${file} to S3 with ${events.length} events`
); );
@@ -136,7 +141,9 @@ export class UsageService {
await fs.access(filePath); await fs.access(filePath);
await fs.unlink(filePath); await fs.unlink(filePath);
} catch (unlinkError) { } catch (unlinkError) {
logger.debug(`Empty startup file ${file} was already deleted`); logger.debug(
`Empty startup file ${file} was already deleted`
);
} }
} }
} catch (err) { } catch (err) {
@@ -147,8 +154,8 @@ export class UsageService {
} }
} }
} }
} catch (err) { } catch (error) {
logger.error("Failed to scan for leftover event files:", err); logger.error("Failed to scan for leftover event files");
} }
} }
@@ -158,17 +165,17 @@ export class UsageService {
value: number, value: number,
transaction: any = null transaction: any = null
): Promise<Usage | null> { ): Promise<Usage | null> {
if (build !== "saas") { if (noop()) {
return null; return null;
} }
// Truncate value to 11 decimal places // Truncate value to 11 decimal places
value = this.truncateValue(value); value = this.truncateValue(value);
// Implement retry logic for deadlock handling // Implement retry logic for deadlock handling
const maxRetries = 3; const maxRetries = 3;
let attempt = 0; let attempt = 0;
while (attempt <= maxRetries) { while (attempt <= maxRetries) {
try { try {
// Get subscription data for this org (with caching) // Get subscription data for this org (with caching)
@@ -191,7 +198,12 @@ export class UsageService {
); );
} else { } else {
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
usage = await this.internalAddUsage(orgId, featureId, value, trx); usage = await this.internalAddUsage(
orgId,
featureId,
value,
trx
);
}); });
} }
@@ -201,25 +213,26 @@ export class UsageService {
return usage || null; return usage || null;
} catch (error: any) { } catch (error: any) {
// Check if this is a deadlock error // Check if this is a deadlock error
const isDeadlock = error?.code === '40P01' || const isDeadlock =
error?.cause?.code === '40P01' || error?.code === "40P01" ||
(error?.message && error.message.includes('deadlock')); error?.cause?.code === "40P01" ||
(error?.message && error.message.includes("deadlock"));
if (isDeadlock && attempt < maxRetries) { if (isDeadlock && attempt < maxRetries) {
attempt++; attempt++;
// Exponential backoff with jitter: 50-150ms, 100-300ms, 200-600ms // Exponential backoff with jitter: 50-150ms, 100-300ms, 200-600ms
const baseDelay = Math.pow(2, attempt - 1) * 50; const baseDelay = Math.pow(2, attempt - 1) * 50;
const jitter = Math.random() * baseDelay; const jitter = Math.random() * baseDelay;
const delay = baseDelay + jitter; const delay = baseDelay + jitter;
logger.warn( logger.warn(
`Deadlock detected for ${orgId}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms` `Deadlock detected for ${orgId}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`
); );
await new Promise(resolve => setTimeout(resolve, delay)); await new Promise((resolve) => setTimeout(resolve, delay));
continue; continue;
} }
logger.error( logger.error(
`Failed to add usage for ${orgId}/${featureId} after ${attempt} attempts:`, `Failed to add usage for ${orgId}/${featureId} after ${attempt} attempts:`,
error error
@@ -239,10 +252,10 @@ export class UsageService {
): Promise<Usage> { ): Promise<Usage> {
// Truncate value to 11 decimal places // Truncate value to 11 decimal places
value = this.truncateValue(value); value = this.truncateValue(value);
const usageId = `${orgId}-${featureId}`; const usageId = `${orgId}-${featureId}`;
const meterId = getFeatureMeterId(featureId); const meterId = getFeatureMeterId(featureId);
// Use upsert: insert if not exists, otherwise increment // Use upsert: insert if not exists, otherwise increment
const [returnUsage] = await trx const [returnUsage] = await trx
.insert(usage) .insert(usage)
@@ -259,7 +272,8 @@ export class UsageService {
set: { set: {
latestValue: sql`${usage.latestValue} + ${value}` latestValue: sql`${usage.latestValue} + ${value}`
} }
}).returning(); })
.returning();
return returnUsage; return returnUsage;
} }
@@ -280,7 +294,7 @@ export class UsageService {
value?: number, value?: number,
customerId?: string customerId?: string
): Promise<void> { ): Promise<void> {
if (build !== "saas") { if (noop()) {
return; return;
} }
try { try {
@@ -351,7 +365,7 @@ export class UsageService {
.set({ .set({
latestValue: newRunningTotal, latestValue: newRunningTotal,
instantaneousValue: value, instantaneousValue: value,
updatedAt: Math.floor(Date.now() / 1000) updatedAt: Math.floor(Date.now() / 1000)
}) })
.where(eq(usage.usageId, usageId)); .where(eq(usage.usageId, usageId));
} }
@@ -366,7 +380,7 @@ export class UsageService {
meterId, meterId,
instantaneousValue: truncatedValue, instantaneousValue: truncatedValue,
latestValue: truncatedValue, latestValue: truncatedValue,
updatedAt: Math.floor(Date.now() / 1000) updatedAt: Math.floor(Date.now() / 1000)
}); });
} }
}); });
@@ -427,7 +441,7 @@ export class UsageService {
): Promise<void> { ): Promise<void> {
// Truncate value to 11 decimal places before sending to Stripe // Truncate value to 11 decimal places before sending to Stripe
const truncatedValue = this.truncateValue(value); const truncatedValue = this.truncateValue(value);
const event: StripeEvent = { const event: StripeEvent = {
identifier: uuidv4(), identifier: uuidv4(),
timestamp: Math.floor(new Date().getTime() / 1000), timestamp: Math.floor(new Date().getTime() / 1000),
@@ -444,7 +458,9 @@ export class UsageService {
private async writeEventToFile(event: StripeEvent): Promise<void> { private async writeEventToFile(event: StripeEvent): Promise<void> {
if (!this.eventsDir || !this.bucketName) { if (!this.eventsDir || !this.bucketName) {
logger.warn("Stripe local file path or bucket name is not configured, skipping event file write."); logger.warn(
"Stripe local file path or bucket name is not configured, skipping event file write."
);
return; return;
} }
if (!this.currentEventFile) { if (!this.currentEventFile) {
@@ -493,7 +509,9 @@ export class UsageService {
private async uploadFileToS3(): Promise<void> { private async uploadFileToS3(): Promise<void> {
if (!this.bucketName || !this.eventsDir) { if (!this.bucketName || !this.eventsDir) {
logger.warn("Stripe local file path or bucket name is not configured, skipping S3 upload."); logger.warn(
"Stripe local file path or bucket name is not configured, skipping S3 upload."
);
return; return;
} }
if (!this.currentEventFile) { if (!this.currentEventFile) {
@@ -505,7 +523,9 @@ export class UsageService {
// Check if this file is already being uploaded // Check if this file is already being uploaded
if (this.uploadingFiles.has(fileName)) { if (this.uploadingFiles.has(fileName)) {
logger.debug(`File ${fileName} is already being uploaded, skipping`); logger.debug(
`File ${fileName} is already being uploaded, skipping`
);
return; return;
} }
@@ -517,7 +537,9 @@ export class UsageService {
try { try {
await fs.access(filePath); await fs.access(filePath);
} catch (error) { } catch (error) {
logger.debug(`File ${fileName} does not exist, may have been already processed`); logger.debug(
`File ${fileName} does not exist, may have been already processed`
);
this.uploadingFiles.delete(fileName); this.uploadingFiles.delete(fileName);
// Reset current file if it was this file // Reset current file if it was this file
if (this.currentEventFile === fileName) { if (this.currentEventFile === fileName) {
@@ -537,7 +559,9 @@ export class UsageService {
await fs.unlink(filePath); await fs.unlink(filePath);
} catch (unlinkError) { } catch (unlinkError) {
// File may have been already deleted // File may have been already deleted
logger.debug(`File ${fileName} was already deleted during cleanup`); logger.debug(
`File ${fileName} was already deleted during cleanup`
);
} }
this.currentEventFile = null; this.currentEventFile = null;
this.uploadingFiles.delete(fileName); this.uploadingFiles.delete(fileName);
@@ -560,7 +584,9 @@ export class UsageService {
await fs.unlink(filePath); await fs.unlink(filePath);
} catch (unlinkError) { } catch (unlinkError) {
// File may have been already deleted by another process // File may have been already deleted by another process
logger.debug(`File ${fileName} was already deleted during upload`); logger.debug(
`File ${fileName} was already deleted during upload`
);
} }
logger.info( logger.info(
@@ -571,10 +597,7 @@ export class UsageService {
this.currentEventFile = null; this.currentEventFile = null;
this.currentFileStartTime = 0; this.currentFileStartTime = 0;
} catch (error) { } catch (error) {
logger.error( logger.error(`Failed to upload ${fileName} to S3:`, error);
`Failed to upload ${fileName} to S3:`,
error
);
} finally { } finally {
// Always remove from uploading set // Always remove from uploading set
this.uploadingFiles.delete(fileName); this.uploadingFiles.delete(fileName);
@@ -591,7 +614,7 @@ export class UsageService {
orgId: string, orgId: string,
featureId: FeatureId featureId: FeatureId
): Promise<Usage | null> { ): Promise<Usage | null> {
if (build !== "saas") { if (noop()) {
return null; return null;
} }
@@ -610,7 +633,7 @@ export class UsageService {
`Creating new usage record for ${orgId}/${featureId}` `Creating new usage record for ${orgId}/${featureId}`
); );
const meterId = getFeatureMeterId(featureId); const meterId = getFeatureMeterId(featureId);
try { try {
const [newUsage] = await db const [newUsage] = await db
.insert(usage) .insert(usage)
@@ -665,7 +688,7 @@ export class UsageService {
orgId: string, orgId: string,
featureId: FeatureId featureId: FeatureId
): Promise<Usage | null> { ): Promise<Usage | null> {
if (build !== "saas") { if (noop()) {
return null; return null;
} }
await this.updateDaily(orgId, featureId); // Ensure daily usage is updated await this.updateDaily(orgId, featureId); // Ensure daily usage is updated
@@ -685,7 +708,9 @@ export class UsageService {
*/ */
private async uploadOldEventFiles(): Promise<void> { private async uploadOldEventFiles(): Promise<void> {
if (!this.eventsDir || !this.bucketName) { if (!this.eventsDir || !this.bucketName) {
logger.warn("Stripe local file path or bucket name is not configured, skipping old event file upload."); logger.warn(
"Stripe local file path or bucket name is not configured, skipping old event file upload."
);
return; return;
} }
try { try {
@@ -693,15 +718,17 @@ export class UsageService {
const now = Date.now(); const now = Date.now();
for (const file of files) { for (const file of files) {
if (!file.endsWith(".json")) continue; if (!file.endsWith(".json")) continue;
// Skip files that are already being uploaded // Skip files that are already being uploaded
if (this.uploadingFiles.has(file)) { if (this.uploadingFiles.has(file)) {
logger.debug(`Skipping file ${file} as it's already being uploaded`); logger.debug(
`Skipping file ${file} as it's already being uploaded`
);
continue; continue;
} }
const filePath = path.join(this.eventsDir, file); const filePath = path.join(this.eventsDir, file);
try { try {
// Check if file still exists before processing // Check if file still exists before processing
try { try {
@@ -716,7 +743,7 @@ export class UsageService {
if (age >= 90000) { if (age >= 90000) {
// 1.5 minutes - Mark as being uploaded // 1.5 minutes - Mark as being uploaded
this.uploadingFiles.add(file); this.uploadingFiles.add(file);
try { try {
const fileContent = await fs.readFile( const fileContent = await fs.readFile(
filePath, filePath,
@@ -732,15 +759,17 @@ export class UsageService {
ContentType: "application/json" ContentType: "application/json"
}); });
await s3Client.send(uploadCommand); await s3Client.send(uploadCommand);
// Check if file still exists before unlinking // Check if file still exists before unlinking
try { try {
await fs.access(filePath); await fs.access(filePath);
await fs.unlink(filePath); await fs.unlink(filePath);
} catch (unlinkError) { } catch (unlinkError) {
logger.debug(`File ${file} was already deleted during interval upload`); logger.debug(
`File ${file} was already deleted during interval upload`
);
} }
logger.info( logger.info(
`Interval: Uploaded event file ${file} to S3 with ${events.length} events` `Interval: Uploaded event file ${file} to S3 with ${events.length} events`
); );
@@ -755,7 +784,9 @@ export class UsageService {
await fs.access(filePath); await fs.access(filePath);
await fs.unlink(filePath); await fs.unlink(filePath);
} catch (unlinkError) { } catch (unlinkError) {
logger.debug(`Empty file ${file} was already deleted`); logger.debug(
`Empty file ${file} was already deleted`
);
} }
} }
} finally { } finally {
@@ -777,12 +808,17 @@ export class UsageService {
} }
} }
public async checkLimitSet(orgId: string, kickSites = false, featureId?: FeatureId, usage?: Usage): Promise<boolean> { public async checkLimitSet(
if (build !== "saas") { orgId: string,
kickSites = false,
featureId?: FeatureId,
usage?: Usage
): Promise<boolean> {
if (noop()) {
return false; return false;
} }
// This method should check the current usage against the limits set for the organization // This method should check the current usage against the limits set for the organization
// and kick out all of the sites on the org // and kick out all of the sites on the org
let hasExceededLimits = false; let hasExceededLimits = false;
try { try {
@@ -817,16 +853,30 @@ export class UsageService {
if (usage) { if (usage) {
currentUsage = usage; currentUsage = usage;
} else { } else {
currentUsage = await this.getUsage(orgId, limit.featureId as FeatureId); currentUsage = await this.getUsage(
orgId,
limit.featureId as FeatureId
);
} }
const usageValue = currentUsage?.instantaneousValue || currentUsage?.latestValue || 0; const usageValue =
logger.debug(`Current usage for org ${orgId} on feature ${limit.featureId}: ${usageValue}`); currentUsage?.instantaneousValue ||
logger.debug(`Limit for org ${orgId} on feature ${limit.featureId}: ${limit.value}`); currentUsage?.latestValue ||
if (currentUsage && limit.value !== null && usageValue > limit.value) { 0;
logger.debug(
`Current usage for org ${orgId} on feature ${limit.featureId}: ${usageValue}`
);
logger.debug(
`Limit for org ${orgId} on feature ${limit.featureId}: ${limit.value}`
);
if (
currentUsage &&
limit.value !== null &&
usageValue > limit.value
) {
logger.debug( logger.debug(
`Org ${orgId} has exceeded limit for ${limit.featureId}: ` + `Org ${orgId} has exceeded limit for ${limit.featureId}: ` +
`${usageValue} > ${limit.value}` `${usageValue} > ${limit.value}`
); );
hasExceededLimits = true; hasExceededLimits = true;
break; // Exit early if any limit is exceeded break; // Exit early if any limit is exceeded
@@ -835,7 +885,9 @@ export class UsageService {
// If any limits are exceeded, disconnect all sites for this organization // If any limits are exceeded, disconnect all sites for this organization
if (hasExceededLimits && kickSites) { if (hasExceededLimits && kickSites) {
logger.warn(`Disconnecting all sites for org ${orgId} due to exceeded limits`); logger.warn(
`Disconnecting all sites for org ${orgId} due to exceeded limits`
);
// Get all sites for this organization // Get all sites for this organization
const orgSites = await db const orgSites = await db
@@ -844,7 +896,7 @@ export class UsageService {
.where(eq(sites.orgId, orgId)); .where(eq(sites.orgId, orgId));
// Mark all sites as offline and send termination messages // Mark all sites as offline and send termination messages
const siteUpdates = orgSites.map(site => site.siteId); const siteUpdates = orgSites.map((site) => site.siteId);
if (siteUpdates.length > 0) { if (siteUpdates.length > 0) {
// Send termination messages to newt sites // Send termination messages to newt sites
@@ -865,17 +917,21 @@ export class UsageService {
}; };
// Don't await to prevent blocking // Don't await to prevent blocking
sendToClient(newt.newtId, payload).catch((error: any) => { sendToClient(newt.newtId, payload).catch(
logger.error( (error: any) => {
`Failed to send termination message to newt ${newt.newtId}:`, logger.error(
error `Failed to send termination message to newt ${newt.newtId}:`,
); error
}); );
}
);
} }
} }
} }
logger.info(`Disconnected ${orgSites.length} sites for org ${orgId} due to exceeded limits`); logger.info(
`Disconnected ${orgSites.length} sites for org ${orgId} due to exceeded limits`
);
} }
} }
} catch (error) { } catch (error) {

View File

@@ -1,4 +1,4 @@
import { sendToClient } from "@server/routers/ws"; import { sendToClient } from "#dynamic/routers/ws";
import { processContainerLabels } from "./parseDockerContainers"; import { processContainerLabels } from "./parseDockerContainers";
import { applyBlueprint } from "./applyBlueprint"; import { applyBlueprint } from "./applyBlueprint";
import { db, sites } from "@server/db"; import { db, sites } from "@server/db";

View File

@@ -2,6 +2,7 @@ import {
domains, domains,
orgDomains, orgDomains,
Resource, Resource,
resourceHeaderAuth,
resourcePincode, resourcePincode,
resourceRules, resourceRules,
resourceWhitelist, resourceWhitelist,
@@ -24,7 +25,7 @@ import {
TargetData TargetData
} from "./types"; } from "./types";
import logger from "@server/logger"; import logger from "@server/logger";
import { createCertificate } from "@server/routers/private/certificates/createCertificate"; import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { pickPort } from "@server/routers/target/helpers"; 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";
@@ -123,7 +124,9 @@ export async function updateProxyResources(
const healthcheckData = targetData.healthcheck; const healthcheckData = targetData.healthcheck;
const hcHeaders = healthcheckData?.headers ? JSON.stringify(healthcheckData.headers) : null; const hcHeaders = healthcheckData?.headers
? JSON.stringify(healthcheckData.headers)
: null;
const [newHealthcheck] = await trx const [newHealthcheck] = await trx
.insert(targetHealthCheck) .insert(targetHealthCheck)
@@ -264,6 +267,32 @@ export async function updateProxyResources(
}); });
} }
await trx
.delete(resourceHeaderAuth)
.where(
eq(
resourceHeaderAuth.resourceId,
existingResource.resourceId
)
);
if (resourceData.auth?.["basic-auth"]) {
const headerAuthUser =
resourceData.auth?.["basic-auth"]?.user;
const headerAuthPassword =
resourceData.auth?.["basic-auth"]?.password;
if (headerAuthUser && headerAuthPassword) {
const headerAuthHash = await hashPassword(
Buffer.from(
`${headerAuthUser}:${headerAuthPassword}`
).toString("base64")
);
await trx.insert(resourceHeaderAuth).values({
resourceId: existingResource.resourceId,
headerAuthHash
});
}
}
if (resourceData.auth?.["sso-roles"]) { if (resourceData.auth?.["sso-roles"]) {
const ssoRoles = resourceData.auth?.["sso-roles"]; const ssoRoles = resourceData.auth?.["sso-roles"];
await syncRoleResources( await syncRoleResources(
@@ -408,7 +437,9 @@ export async function updateProxyResources(
) )
.limit(1); .limit(1);
const hcHeaders = healthcheckData?.headers ? JSON.stringify(healthcheckData.headers) : null; const hcHeaders = healthcheckData?.headers
? JSON.stringify(healthcheckData.headers)
: null;
const [newHealthcheck] = await trx const [newHealthcheck] = await trx
.update(targetHealthCheck) .update(targetHealthCheck)
@@ -593,6 +624,25 @@ export async function updateProxyResources(
}); });
} }
if (resourceData.auth?.["basic-auth"]) {
const headerAuthUser = resourceData.auth?.["basic-auth"]?.user;
const headerAuthPassword =
resourceData.auth?.["basic-auth"]?.password;
if (headerAuthUser && headerAuthPassword) {
const headerAuthHash = await hashPassword(
Buffer.from(
`${headerAuthUser}:${headerAuthPassword}`
).toString("base64")
);
await trx.insert(resourceHeaderAuth).values({
resourceId: newResource.resourceId,
headerAuthHash
});
}
}
resource = newResource; resource = newResource;
const [adminRole] = await trx const [adminRole] = await trx

View File

@@ -42,6 +42,10 @@ export const AuthSchema = z.object({
// pincode has to have 6 digits // pincode has to have 6 digits
pincode: z.number().min(100000).max(999999).optional(), pincode: z.number().min(100000).max(999999).optional(),
password: z.string().min(1).optional(), password: z.string().min(1).optional(),
"basic-auth": z.object({
user: z.string().min(1),
password: z.string().min(1)
}).optional(),
"sso-enabled": z.boolean().optional().default(false), "sso-enabled": z.boolean().optional().default(false),
"sso-roles": z "sso-roles": z
.array(z.string()) .array(z.string())

View File

@@ -0,0 +1,13 @@
export async function getValidCertificatesForDomains(domains: Set<string>): Promise<
Array<{
id: number;
domain: string;
wildcard: boolean | null;
certFile: string | null;
keyFile: string | null;
expiresAt: number | null;
updatedAt?: number | null;
}>
> {
return []; // stub
}

View File

@@ -3,19 +3,12 @@ import { __DIRNAME, APP_VERSION } from "@server/lib/consts";
import { db } from "@server/db"; import { db } from "@server/db";
import { SupporterKey, supporterKey } from "@server/db"; import { SupporterKey, supporterKey } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { license } from "@server/license/license";
import { configSchema, readConfigFile } from "./readConfigFile"; import { configSchema, readConfigFile } from "./readConfigFile";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import {
privateConfigSchema,
readPrivateConfigFile
} from "@server/lib/private/readConfigFile";
import logger from "@server/logger";
import { build } from "@server/build"; import { build } from "@server/build";
export class Config { export class Config {
private rawConfig!: z.infer<typeof configSchema>; private rawConfig!: z.infer<typeof configSchema>;
private rawPrivateConfig!: z.infer<typeof privateConfigSchema>;
supporterData: SupporterKey | null = null; supporterData: SupporterKey | null = null;
@@ -37,19 +30,6 @@ export class Config {
throw new Error(`Invalid configuration file: ${errors}`); throw new Error(`Invalid configuration file: ${errors}`);
} }
const privateEnvironment = readPrivateConfigFile();
const {
data: parsedPrivateConfig,
success: privateSuccess,
error: privateError
} = privateConfigSchema.safeParse(privateEnvironment);
if (!privateSuccess) {
const errors = fromError(privateError);
throw new Error(`Invalid private configuration file: ${errors}`);
}
if ( if (
// @ts-ignore // @ts-ignore
parsedConfig.users || parsedConfig.users ||
@@ -109,132 +89,23 @@ export class Config {
? "true" ? "true"
: "false"; : "false";
if (parsedPrivateConfig.branding?.colors) {
process.env.BRANDING_COLORS = JSON.stringify(
parsedPrivateConfig.branding?.colors
);
}
if (parsedPrivateConfig.branding?.logo?.light_path) {
process.env.BRANDING_LOGO_LIGHT_PATH =
parsedPrivateConfig.branding?.logo?.light_path;
}
if (parsedPrivateConfig.branding?.logo?.dark_path) {
process.env.BRANDING_LOGO_DARK_PATH =
parsedPrivateConfig.branding?.logo?.dark_path || undefined;
}
process.env.HIDE_SUPPORTER_KEY = parsedPrivateConfig.flags
?.hide_supporter_key
? "true"
: "false";
if (build != "oss") {
if (parsedPrivateConfig.branding?.logo?.light_path) {
process.env.BRANDING_LOGO_LIGHT_PATH =
parsedPrivateConfig.branding?.logo?.light_path;
}
if (parsedPrivateConfig.branding?.logo?.dark_path) {
process.env.BRANDING_LOGO_DARK_PATH =
parsedPrivateConfig.branding?.logo?.dark_path || undefined;
}
process.env.BRANDING_LOGO_AUTH_WIDTH = parsedPrivateConfig.branding
?.logo?.auth_page?.width
? parsedPrivateConfig.branding?.logo?.auth_page?.width.toString()
: undefined;
process.env.BRANDING_LOGO_AUTH_HEIGHT = parsedPrivateConfig.branding
?.logo?.auth_page?.height
? parsedPrivateConfig.branding?.logo?.auth_page?.height.toString()
: undefined;
process.env.BRANDING_LOGO_NAVBAR_WIDTH = parsedPrivateConfig
.branding?.logo?.navbar?.width
? parsedPrivateConfig.branding?.logo?.navbar?.width.toString()
: undefined;
process.env.BRANDING_LOGO_NAVBAR_HEIGHT = parsedPrivateConfig
.branding?.logo?.navbar?.height
? parsedPrivateConfig.branding?.logo?.navbar?.height.toString()
: undefined;
process.env.BRANDING_FAVICON_PATH =
parsedPrivateConfig.branding?.favicon_path;
process.env.BRANDING_APP_NAME =
parsedPrivateConfig.branding?.app_name || "Pangolin";
if (parsedPrivateConfig.branding?.footer) {
process.env.BRANDING_FOOTER = JSON.stringify(
parsedPrivateConfig.branding?.footer
);
}
process.env.LOGIN_PAGE_TITLE_TEXT =
parsedPrivateConfig.branding?.login_page?.title_text || "";
process.env.LOGIN_PAGE_SUBTITLE_TEXT =
parsedPrivateConfig.branding?.login_page?.subtitle_text || "";
process.env.SIGNUP_PAGE_TITLE_TEXT =
parsedPrivateConfig.branding?.signup_page?.title_text || "";
process.env.SIGNUP_PAGE_SUBTITLE_TEXT =
parsedPrivateConfig.branding?.signup_page?.subtitle_text || "";
process.env.RESOURCE_AUTH_PAGE_HIDE_POWERED_BY =
parsedPrivateConfig.branding?.resource_auth_page
?.hide_powered_by === true
? "true"
: "false";
process.env.RESOURCE_AUTH_PAGE_SHOW_LOGO =
parsedPrivateConfig.branding?.resource_auth_page?.show_logo ===
true
? "true"
: "false";
process.env.RESOURCE_AUTH_PAGE_TITLE_TEXT =
parsedPrivateConfig.branding?.resource_auth_page?.title_text ||
"";
process.env.RESOURCE_AUTH_PAGE_SUBTITLE_TEXT =
parsedPrivateConfig.branding?.resource_auth_page
?.subtitle_text || "";
if (parsedPrivateConfig.branding?.background_image_path) {
process.env.BACKGROUND_IMAGE_PATH =
parsedPrivateConfig.branding?.background_image_path;
}
if (parsedPrivateConfig.server.reo_client_id) {
process.env.REO_CLIENT_ID =
parsedPrivateConfig.server.reo_client_id;
}
}
if (parsedConfig.server.maxmind_db_path) { if (parsedConfig.server.maxmind_db_path) {
process.env.MAXMIND_DB_PATH = parsedConfig.server.maxmind_db_path; process.env.MAXMIND_DB_PATH = parsedConfig.server.maxmind_db_path;
} }
this.rawConfig = parsedConfig; this.rawConfig = parsedConfig;
this.rawPrivateConfig = parsedPrivateConfig;
} }
public async initServer() { public async initServer() {
if (!this.rawConfig) { if (!this.rawConfig) {
throw new Error("Config not loaded. Call load() first."); throw new Error("Config not loaded. Call load() first.");
} }
if (this.rawConfig.managed) {
// LETS NOT WORRY ABOUT THE SERVER SECRET WHEN MANAGED
return;
}
license.setServerSecret(this.rawConfig.server.secret!);
await this.checkKeyStatus(); await this.checkKeyStatus();
} }
private async checkKeyStatus() { private async checkKeyStatus() {
const licenseStatus = await license.check(); if (build == "oss") {
if (
!this.rawPrivateConfig.flags?.hide_supporter_key &&
build != "oss" &&
!licenseStatus.isHostLicensed
) {
this.checkSupporterKey(); this.checkSupporterKey();
} }
} }
@@ -243,10 +114,6 @@ export class Config {
return this.rawConfig; return this.rawConfig;
} }
public getRawPrivateConfig() {
return this.rawPrivateConfig;
}
public getNoReplyEmail(): string | undefined { public getNoReplyEmail(): string | undefined {
return ( return (
this.rawConfig.email?.no_reply || this.rawConfig.email?.smtp_user this.rawConfig.email?.no_reply || this.rawConfig.email?.smtp_user
@@ -280,10 +147,6 @@ export class Config {
return false; return false;
} }
public isManagedMode() {
return typeof this.rawConfig?.managed === "object";
}
public async checkSupporterKey() { public async checkSupporterKey() {
const [key] = await db.select().from(supporterKey).limit(1); const [key] = await db.select().from(supporterKey).limit(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.10.4"; export const APP_VERSION = "1.11.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);

View File

@@ -1,16 +1,3 @@
/*
* 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 { Request, Response, NextFunction } from "express";
import cors, { CorsOptions } from "cors"; import cors, { CorsOptions } from "cors";
import config from "@server/lib/config"; import config from "@server/lib/config";

View File

@@ -1,16 +1,3 @@
/*
* 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 { isValidCIDR } from "@server/lib/validators"; import { isValidCIDR } from "@server/lib/validators";
import { getNextAvailableOrgSubnet } from "@server/lib/ip"; import { getNextAvailableOrgSubnet } from "@server/lib/ip";
import { import {
@@ -28,9 +15,9 @@ import {
} from "@server/db"; } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { defaultRoleAllowedActions } from "@server/routers/role"; import { defaultRoleAllowedActions } from "@server/routers/role";
import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/private/billing"; import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/billing";
import { createCustomer } from "@server/routers/private/billing/createCustomer"; import { createCustomer } from "#dynamic/lib/billing";
import { usageService } from "@server/lib/private/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
export async function createUserAccountOrg( export async function createUserAccountOrg(
userId: string, userId: string,

View File

@@ -16,7 +16,11 @@ export async function verifyExitNodeOrgAccess(
return { hasAccess: true, exitNode }; return { hasAccess: true, exitNode };
} }
export async function listExitNodes(orgId: string, filterOnline = false, noCloud = false) { export async function listExitNodes(
orgId: string,
filterOnline = false,
noCloud = false
) {
// TODO: pick which nodes to send and ping better than just all of them that are not remote // TODO: pick which nodes to send and ping better than just all of them that are not remote
const allExitNodes = await db const allExitNodes = await db
.select({ .select({
@@ -59,7 +63,16 @@ export async function checkExitNodeOrg(exitNodeId: number, orgId: string) {
return false; return false;
} }
export async function resolveExitNodes(hostname: string, publicKey: string) { export async function resolveExitNodes(
hostname: string,
publicKey: string
): Promise<
{
endpoint: string;
publicKey: string;
orgId: string;
}[]
> {
// OSS version: simple implementation that returns empty array // OSS version: simple implementation that returns empty array
return []; return [];
} }

View File

@@ -1,33 +1,4 @@
import { build } from "@server/build"; export * from "./exitNodes";
export * from "./exitNodeComms";
// Import both modules
import * as exitNodesModule from "./exitNodes";
import * as privateExitNodesModule from "./privateExitNodes";
// Conditionally export exit nodes implementation based on build type
const exitNodesImplementation = build === "oss" ? exitNodesModule : privateExitNodesModule;
// Re-export all items from the selected implementation
export const {
verifyExitNodeOrgAccess,
listExitNodes,
selectBestExitNode,
checkExitNodeOrg,
resolveExitNodes
} = exitNodesImplementation;
// Import communications modules
import * as exitNodeCommsModule from "./exitNodeComms";
import * as privateExitNodeCommsModule from "./privateExitNodeComms";
// Conditionally export communications implementation based on build type
const exitNodeCommsImplementation = build === "oss" ? exitNodeCommsModule : privateExitNodeCommsModule;
// Re-export communications functions from the selected implementation
export const {
sendToExitNode
} = exitNodeCommsImplementation;
// Re-export shared modules
export * from "./subnet"; export * from "./subnet";
export * from "./getCurrentExitNodeId"; export * from "./getCurrentExitNodeId";

View File

@@ -1,8 +1,5 @@
import logger from "@server/logger"; import logger from "@server/logger";
import { maxmindLookup } from "@server/db/maxmind"; import { maxmindLookup } from "@server/db/maxmind";
import axios from "axios";
import config from "./config";
import { tokenManager } from "./tokenManager";
export async function getCountryCodeForIp( export async function getCountryCodeForIp(
ip: string ip: string
@@ -33,32 +30,4 @@ export async function getCountryCodeForIp(
} }
return; return;
} }
export async function remoteGetCountryCodeForIp(
ip: string
): Promise<string | undefined> {
try {
const response = await axios.get(
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/geoip/${ip}`,
await tokenManager.getAuthHeader()
);
return response.data.data.countryCode;
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error fetching config in verify session:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error fetching config in verify session:", error);
}
}
return;
}

View File

@@ -1,25 +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 RedisStore from "@server/db/private/redisStore";
import { MemoryStore, Store } from "express-rate-limit";
export function createStore(): Store {
const rateLimitStore: Store = new RedisStore({
prefix: 'api-rate-limit', // Optional: customize Redis key prefix
skipFailedRequests: true, // Don't count failed requests
skipSuccessfulRequests: false, // Count successful requests
});
return rateLimitStore;
}

View File

@@ -1,192 +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 fs from "fs";
import yaml from "js-yaml";
import { privateConfigFilePath1 } from "@server/lib/consts";
import { z } from "zod";
import { colorsSchema } from "@server/lib/colorsSchema";
import { build } from "@server/build";
const portSchema = z.number().positive().gt(0).lte(65535);
export const privateConfigSchema = z
.object({
app: z.object({
region: z.string().optional().default("default"),
base_domain: z.string().optional()
}).optional().default({
region: "default"
}),
server: z.object({
encryption_key_path: z
.string()
.optional()
.default("./config/encryption.pem")
.pipe(z.string().min(8)),
resend_api_key: z.string().optional(),
reo_client_id: z.string().optional(),
}).optional().default({
encryption_key_path: "./config/encryption.pem"
}),
redis: z
.object({
host: z.string(),
port: portSchema,
password: z.string().optional(),
db: z.number().int().nonnegative().optional().default(0),
replicas: z
.array(
z.object({
host: z.string(),
port: portSchema,
password: z.string().optional(),
db: z.number().int().nonnegative().optional().default(0)
})
)
.optional()
// tls: z
// .object({
// reject_unauthorized: z
// .boolean()
// .optional()
// .default(true)
// })
// .optional()
})
.optional(),
gerbil: z
.object({
local_exit_node_reachable_at: z.string().optional().default("http://gerbil:3003")
})
.optional()
.default({}),
flags: z
.object({
enable_redis: z.boolean().optional(),
hide_supporter_key: z.boolean().optional()
})
.optional(),
branding: z
.object({
app_name: z.string().optional(),
background_image_path: z.string().optional(),
colors: z
.object({
light: colorsSchema.optional(),
dark: colorsSchema.optional()
})
.optional(),
logo: z
.object({
light_path: z.string().optional(),
dark_path: z.string().optional(),
auth_page: z
.object({
width: z.number().optional(),
height: z.number().optional()
})
.optional(),
navbar: z
.object({
width: z.number().optional(),
height: z.number().optional()
})
.optional()
})
.optional(),
favicon_path: z.string().optional(),
footer: z
.array(
z.object({
text: z.string(),
href: z.string().optional()
})
)
.optional(),
login_page: z
.object({
subtitle_text: z.string().optional(),
title_text: z.string().optional()
})
.optional(),
signup_page: z
.object({
subtitle_text: z.string().optional(),
title_text: z.string().optional()
})
.optional(),
resource_auth_page: z
.object({
show_logo: z.boolean().optional(),
hide_powered_by: z.boolean().optional(),
title_text: z.string().optional(),
subtitle_text: z.string().optional()
})
.optional(),
emails: z
.object({
signature: z.string().optional(),
colors: z
.object({
primary: z.string().optional()
})
.optional()
})
.optional()
})
.optional(),
stripe: z
.object({
secret_key: z.string(),
webhook_secret: z.string(),
s3Bucket: z.string(),
s3Region: z.string().default("us-east-1"),
localFilePath: z.string()
})
.optional(),
});
export function readPrivateConfigFile() {
if (build == "oss") {
return {};
}
const loadConfig = (configPath: string) => {
try {
const yamlContent = fs.readFileSync(configPath, "utf8");
const config = yaml.load(yamlContent);
return config;
} catch (error) {
if (error instanceof Error) {
throw new Error(
`Error loading configuration file: ${error.message}`
);
}
throw error;
}
};
let environment: any;
if (fs.existsSync(privateConfigFilePath1)) {
environment = loadConfig(privateConfigFilePath1);
}
if (!environment) {
throw new Error(
"No private configuration file found."
);
}
return environment;
}

View File

@@ -1,19 +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 { S3Client } from "@aws-sdk/client-s3";
import config from "@server/lib/config";
export const s3Client = new S3Client({
region: config.getRawPrivateConfig().stripe?.s3Region || "us-east-1",
});

View File

@@ -12,42 +12,36 @@ const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => {
export const configSchema = z export const configSchema = z
.object({ .object({
app: z.object({ app: z
dashboard_url: z
.string()
.url()
.pipe(z.string().url())
.transform((url) => url.toLowerCase())
.optional(),
log_level: z
.enum(["debug", "info", "warn", "error"])
.optional()
.default("info"),
save_logs: z.boolean().optional().default(false),
log_failed_attempts: z.boolean().optional().default(false),
telemetry: z
.object({
anonymous_usage: z.boolean().optional().default(true)
})
.optional()
.default({}),
}).optional().default({
log_level: "info",
save_logs: false,
log_failed_attempts: false,
telemetry: {
anonymous_usage: true
}
}),
managed: z
.object({ .object({
name: z.string().optional(), dashboard_url: z
id: z.string().optional(), .string()
secret: z.string().optional(), .url()
endpoint: z.string().optional().default("https://pangolin.fossorial.io"), .pipe(z.string().url())
redirect_endpoint: z.string().optional() .transform((url) => url.toLowerCase())
.optional(),
log_level: z
.enum(["debug", "info", "warn", "error"])
.optional()
.default("info"),
save_logs: z.boolean().optional().default(false),
log_failed_attempts: z.boolean().optional().default(false),
telemetry: z
.object({
anonymous_usage: z.boolean().optional().default(true)
})
.optional()
.default({})
}) })
.optional(), .optional()
.default({
log_level: "info",
save_logs: false,
log_failed_attempts: false,
telemetry: {
anonymous_usage: true
}
}),
domains: z domains: z
.record( .record(
z.string(), z.string(),
@@ -61,94 +55,95 @@ export const configSchema = z
}) })
) )
.optional(), .optional(),
server: z.object({ server: z
integration_port: portSchema .object({
.optional() integration_port: portSchema
.default(3003) .optional()
.transform(stoi) .default(3003)
.pipe(portSchema.optional()), .transform(stoi)
external_port: portSchema .pipe(portSchema.optional()),
.optional() external_port: portSchema
.default(3000) .optional()
.transform(stoi) .default(3000)
.pipe(portSchema), .transform(stoi)
internal_port: portSchema .pipe(portSchema),
.optional() internal_port: portSchema
.default(3001) .optional()
.transform(stoi) .default(3001)
.pipe(portSchema), .transform(stoi)
next_port: portSchema .pipe(portSchema),
.optional() next_port: portSchema
.default(3002) .optional()
.transform(stoi) .default(3002)
.pipe(portSchema), .transform(stoi)
internal_hostname: z .pipe(portSchema),
.string() internal_hostname: z
.optional() .string()
.default("pangolin") .optional()
.transform((url) => url.toLowerCase()), .default("pangolin")
session_cookie_name: z .transform((url) => url.toLowerCase()),
.string() session_cookie_name: z
.optional() .string()
.default("p_session_token"), .optional()
resource_access_token_param: z .default("p_session_token"),
.string() resource_access_token_param: z
.optional() .string()
.default("p_token"), .optional()
resource_access_token_headers: z .default("p_token"),
.object({ resource_access_token_headers: z
id: z.string().optional().default("P-Access-Token-Id"), .object({
token: z.string().optional().default("P-Access-Token") id: z.string().optional().default("P-Access-Token-Id"),
}) token: z.string().optional().default("P-Access-Token")
.optional() })
.default({}), .optional()
resource_session_request_param: z .default({}),
.string() resource_session_request_param: z
.optional() .string()
.default("resource_session_request_param"), .optional()
dashboard_session_length_hours: z .default("resource_session_request_param"),
.number() dashboard_session_length_hours: z
.positive() .number()
.gt(0) .positive()
.optional() .gt(0)
.default(720), .optional()
resource_session_length_hours: z .default(720),
.number() resource_session_length_hours: z
.positive() .number()
.gt(0) .positive()
.optional() .gt(0)
.default(720), .optional()
cors: z .default(720),
.object({ cors: z
origins: z.array(z.string()).optional(), .object({
methods: z.array(z.string()).optional(), origins: z.array(z.string()).optional(),
allowed_headers: z.array(z.string()).optional(), methods: z.array(z.string()).optional(),
credentials: z.boolean().optional() allowed_headers: z.array(z.string()).optional(),
}) credentials: z.boolean().optional()
.optional(), })
trust_proxy: z.number().int().gte(0).optional().default(1), .optional(),
secret: z trust_proxy: z.number().int().gte(0).optional().default(1),
.string() secret: z.string().pipe(z.string().min(8)).optional(),
.pipe(z.string().min(8)) maxmind_db_path: z.string().optional()
.optional(), })
maxmind_db_path: z.string().optional() .optional()
}).optional().default({ .default({
integration_port: 3003, integration_port: 3003,
external_port: 3000, external_port: 3000,
internal_port: 3001, internal_port: 3001,
next_port: 3002, next_port: 3002,
internal_hostname: "pangolin", internal_hostname: "pangolin",
session_cookie_name: "p_session_token", session_cookie_name: "p_session_token",
resource_access_token_param: "p_token", resource_access_token_param: "p_token",
resource_access_token_headers: { resource_access_token_headers: {
id: "P-Access-Token-Id", id: "P-Access-Token-Id",
token: "P-Access-Token" token: "P-Access-Token"
}, },
resource_session_request_param: "resource_session_request_param", resource_session_request_param:
dashboard_session_length_hours: 720, "resource_session_request_param",
resource_session_length_hours: 720, dashboard_session_length_hours: 720,
trust_proxy: 1 resource_session_length_hours: 720,
}), trust_proxy: 1
}),
postgres: z postgres: z
.object({ .object({
connection_string: z.string().optional(), connection_string: z.string().optional(),
@@ -158,7 +153,32 @@ export const configSchema = z
connection_string: z.string() connection_string: z.string()
}) })
) )
.optional(),
pool: z
.object({
max_connections: z
.number()
.positive()
.optional()
.default(20),
max_replica_connections: z
.number()
.positive()
.optional()
.default(10),
idle_timeout_ms: z
.number()
.positive()
.optional()
.default(30000),
connection_timeout_ms: z
.number()
.positive()
.optional()
.default(5000)
})
.optional() .optional()
.default({})
}) })
.optional(), .optional(),
traefik: z traefik: z
@@ -179,7 +199,10 @@ export const configSchema = z
.optional() .optional()
.default("/var/dynamic/router_config.yml"), .default("/var/dynamic/router_config.yml"),
static_domains: z.array(z.string()).optional().default([]), static_domains: z.array(z.string()).optional().default([]),
site_types: z.array(z.string()).optional().default(["newt", "wireguard", "local"]), site_types: z
.array(z.string())
.optional()
.default(["newt", "wireguard", "local"]),
allow_raw_resources: z.boolean().optional().default(true), allow_raw_resources: z.boolean().optional().default(true),
file_mode: z.boolean().optional().default(false) file_mode: z.boolean().optional().default(false)
}) })
@@ -306,10 +329,7 @@ export const configSchema = z
if (data.flags?.disable_config_managed_domains) { if (data.flags?.disable_config_managed_domains) {
return true; return true;
} }
// If hybrid is defined, domains are not required
if (data.managed) {
return true;
}
if (keys.length === 0) { if (keys.length === 0) {
return false; return false;
} }
@@ -321,15 +341,14 @@ export const configSchema = z
) )
.refine( .refine(
(data) => { (data) => {
// If hybrid is defined, server secret is not required
if (data.managed) {
return true;
}
// If hybrid is not defined, server secret must be defined. If its not defined already then pull it from env // If hybrid is not defined, server secret must be defined. If its not defined already then pull it from env
if (data.server?.secret === undefined) { if (data.server?.secret === undefined) {
data.server.secret = process.env.SERVER_SECRET; data.server.secret = process.env.SERVER_SECRET;
} }
return data.server?.secret !== undefined && data.server.secret.length > 0; return (
data.server?.secret !== undefined &&
data.server.secret.length > 0
);
}, },
{ {
message: "Server secret must be defined" message: "Server secret must be defined"
@@ -337,12 +356,11 @@ export const configSchema = z
) )
.refine( .refine(
(data) => { (data) => {
// If hybrid is defined, dashboard_url is not required
if (data.managed) {
return true;
}
// If hybrid is not defined, dashboard_url must be defined // If hybrid is not defined, dashboard_url must be defined
return data.app.dashboard_url !== undefined && data.app.dashboard_url.length > 0; return (
data.app.dashboard_url !== undefined &&
data.app.dashboard_url.length > 0
);
}, },
{ {
message: "Dashboard URL must be defined" message: "Dashboard URL must be defined"

View File

@@ -1,80 +0,0 @@
import axios from "axios";
import { tokenManager } from "../tokenManager";
import logger from "@server/logger";
import config from "../config";
/**
* Get valid certificates for the specified domains
*/
export async function getValidCertificatesForDomainsHybrid(domains: Set<string>): Promise<
Array<{
id: number;
domain: string;
wildcard: boolean | null;
certFile: string | null;
keyFile: string | null;
expiresAt: number | null;
updatedAt?: number | null;
}>
> {
if (domains.size === 0) {
return [];
}
const domainArray = Array.from(domains);
try {
const response = await axios.get(
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/certificates/domains`,
{
params: {
domains: domainArray
},
headers: (await tokenManager.getAuthHeader()).headers
}
);
if (response.status !== 200) {
logger.error(
`Failed to fetch certificates for domains: ${response.status} ${response.statusText}`,
{ responseData: response.data, domains: domainArray }
);
return [];
}
// logger.debug(
// `Successfully retrieved ${response.data.data?.length || 0} certificates for ${domainArray.length} domains`
// );
return response.data.data;
} catch (error) {
// pull data out of the axios error to log
if (axios.isAxiosError(error)) {
logger.error("Error getting certificates:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error getting certificates:", error);
}
return [];
}
}
export async function getValidCertificatesForDomains(domains: Set<string>): Promise<
Array<{
id: number;
domain: string;
wildcard: boolean | null;
certFile: string | null;
keyFile: string | null;
expiresAt: number | null;
updatedAt?: number | null;
}>
> {
return []; // stub
}

View File

@@ -1,14 +0,0 @@
import { build } from "@server/build";
// Import both modules
import * as certificateModule from "./certificates";
import * as privateCertificateModule from "./privateCertificates";
// Conditionally export Remote Certificates implementation based on build type
const remoteCertificatesImplementation = build === "oss" ? certificateModule : privateCertificateModule;
// Re-export all items from the selected implementation
export const {
getValidCertificatesForDomains,
getValidCertificatesForDomainsHybrid
} = remoteCertificatesImplementation;

View File

@@ -1,116 +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 config from "../config";
import { certificates, db } from "@server/db";
import { and, eq, isNotNull } from "drizzle-orm";
import { decryptData } from "../encryption";
import * as fs from "fs";
export async function getValidCertificatesForDomains(
domains: Set<string>
): Promise<
Array<{
id: number;
domain: string;
wildcard: boolean | null;
certFile: string | null;
keyFile: string | null;
expiresAt: number | null;
updatedAt?: number | null;
}>
> {
if (domains.size === 0) {
return [];
}
const domainArray = Array.from(domains);
// TODO: add more foreign keys to make this query more efficient - we dont need to keep getting every certificate
const validCerts = await db
.select({
id: certificates.certId,
domain: certificates.domain,
certFile: certificates.certFile,
keyFile: certificates.keyFile,
expiresAt: certificates.expiresAt,
updatedAt: certificates.updatedAt,
wildcard: certificates.wildcard
})
.from(certificates)
.where(
and(
eq(certificates.status, "valid"),
isNotNull(certificates.certFile),
isNotNull(certificates.keyFile)
)
);
// Filter certificates for the specified domains and if it is a wildcard then you can match on everything up to the first dot
const validCertsFiltered = validCerts.filter((cert) => {
return (
domainArray.includes(cert.domain) ||
(cert.wildcard &&
domainArray.some((domain) =>
domain.endsWith(`.${cert.domain}`)
))
);
});
const encryptionKeyPath = config.getRawPrivateConfig().server.encryption_key_path;
if (!fs.existsSync(encryptionKeyPath)) {
throw new Error(
"Encryption key file not found. Please generate one first."
);
}
const encryptionKeyHex = fs.readFileSync(encryptionKeyPath, "utf8").trim();
const encryptionKey = Buffer.from(encryptionKeyHex, "hex");
const validCertsDecrypted = validCertsFiltered.map((cert) => {
// Decrypt and save certificate file
const decryptedCert = decryptData(
cert.certFile!, // is not null from query
encryptionKey
);
// Decrypt and save key file
const decryptedKey = decryptData(cert.keyFile!, encryptionKey);
// Return only the certificate data without org information
return {
...cert,
certFile: decryptedCert,
keyFile: decryptedKey
};
});
return validCertsDecrypted;
}
export async function getValidCertificatesForDomainsHybrid(
domains: Set<string>
): Promise<
Array<{
id: number;
domain: string;
wildcard: boolean | null;
certFile: string | null;
keyFile: string | null;
expiresAt: number | null;
updatedAt?: number | null;
}>
> {
return []; // stub
}

View File

@@ -1,73 +0,0 @@
import { Request, Response, NextFunction } from "express";
import { Router } from "express";
import axios from "axios";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import config from "@server/lib/config";
import { tokenManager } from "./tokenManager";
/**
* Proxy function that forwards requests to the remote cloud server
*/
export const proxyToRemote = async (
req: Request,
res: Response,
next: NextFunction,
endpoint: string
): Promise<any> => {
try {
const remoteUrl = `${config.getRawConfig().managed?.endpoint?.replace(/\/$/, '')}/api/v1/${endpoint}`;
logger.debug(`Proxying request to remote server: ${remoteUrl}`);
// Forward the request to the remote server
const response = await axios({
method: req.method as any,
url: remoteUrl,
data: req.body,
headers: {
'Content-Type': 'application/json',
...(await tokenManager.getAuthHeader()).headers
},
params: req.query,
timeout: 30000, // 30 second timeout
validateStatus: () => true // Don't throw on non-2xx status codes
});
logger.debug(`Proxy response: ${JSON.stringify(response.data)}`);
// Forward the response status and data
return res.status(response.status).json(response.data);
} catch (error) {
logger.error("Error proxying request to remote server:", error);
if (axios.isAxiosError(error)) {
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
return next(
createHttpError(
HttpCode.SERVICE_UNAVAILABLE,
"Remote server is unavailable"
)
);
}
if (error.code === 'ECONNABORTED') {
return next(
createHttpError(
HttpCode.REQUEST_TIMEOUT,
"Request to remote server timed out"
)
);
}
}
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error communicating with remote server"
)
);
}
};

15
server/lib/resend.ts Normal file
View File

@@ -0,0 +1,15 @@
export enum AudienceIds {
General = "",
Subscribed = "",
Churned = ""
}
let resend;
export default resend;
export async function moveEmailToAudience(
email: string,
audienceId: AudienceIds
) {
return;
}

5
server/lib/s3.ts Normal file
View File

@@ -0,0 +1,5 @@
import { S3Client } from "@aws-sdk/client-s3";
export const s3Client = new S3Client({
region: process.env.S3_REGION || "us-east-1",
});

View File

@@ -9,6 +9,7 @@ import { APP_VERSION } from "./consts";
import crypto from "crypto"; import crypto from "crypto";
import { UserType } from "@server/types/UserTypes"; import { UserType } from "@server/types/UserTypes";
import { build } from "@server/build"; import { build } from "@server/build";
import license from "@server/license/license";
class TelemetryClient { class TelemetryClient {
private client: PostHog | null = null; private client: PostHog | null = null;
@@ -176,17 +177,36 @@ class TelemetryClient {
const stats = await this.getSystemStats(); const stats = await this.getSystemStats();
this.client.capture({ if (build === "enterprise") {
distinctId: hostMeta.hostMetaId, const licenseStatus = await license.check();
event: "supporter_status", const payload = {
properties: { distinctId: hostMeta.hostMetaId,
valid: stats.supporterStatus.valid, event: "enterprise_status",
tier: stats.supporterStatus.tier, properties: {
github_username: stats.supporterStatus.githubUsername is_host_licensed: licenseStatus.isHostLicensed,
? this.anon(stats.supporterStatus.githubUsername) is_license_valid: licenseStatus.isLicenseValid,
: "None" license_tier: licenseStatus.tier || "unknown"
} }
}); };
logger.debug("Sending enterprise startup telemtry payload:", {
payload
});
// this.client.capture(payload);
}
if (build === "oss") {
this.client.capture({
distinctId: hostMeta.hostMetaId,
event: "supporter_status",
properties: {
valid: stats.supporterStatus.valid,
tier: stats.supporterStatus.tier,
github_username: stats.supporterStatus.githubUsername
? this.anon(stats.supporterStatus.githubUsername)
: "None"
}
});
}
this.client.capture({ this.client.capture({
distinctId: hostMeta.hostMetaId, distinctId: hostMeta.hostMetaId,

View File

@@ -1,274 +0,0 @@
import axios from "axios";
import config from "@server/lib/config";
import logger from "@server/logger";
export interface TokenResponse {
success: boolean;
message?: string;
data: {
token: string;
};
}
/**
* Token Manager - Handles automatic token refresh for hybrid server authentication
*
* Usage throughout the application:
* ```typescript
* import { tokenManager } from "@server/lib/tokenManager";
*
* // Get the current valid token
* const token = await tokenManager.getToken();
*
* // Force refresh if needed
* await tokenManager.refreshToken();
* ```
*
* The token manager automatically refreshes tokens every 24 hours by default
* and is started once in the privateHybridServer.ts file.
*/
export class TokenManager {
private token: string | null = null;
private refreshInterval: NodeJS.Timeout | null = null;
private isRefreshing: boolean = false;
private refreshIntervalMs: number;
private retryInterval: NodeJS.Timeout | null = null;
private retryIntervalMs: number;
private tokenAvailablePromise: Promise<void> | null = null;
private tokenAvailableResolve: (() => void) | null = null;
constructor(refreshIntervalMs: number = 24 * 60 * 60 * 1000, retryIntervalMs: number = 5000) {
// Default to 24 hours for refresh, 5 seconds for retry
this.refreshIntervalMs = refreshIntervalMs;
this.retryIntervalMs = retryIntervalMs;
this.setupTokenAvailablePromise();
}
/**
* Set up promise that resolves when token becomes available
*/
private setupTokenAvailablePromise(): void {
this.tokenAvailablePromise = new Promise((resolve) => {
this.tokenAvailableResolve = resolve;
});
}
/**
* Resolve the token available promise
*/
private resolveTokenAvailable(): void {
if (this.tokenAvailableResolve) {
this.tokenAvailableResolve();
this.tokenAvailableResolve = null;
}
}
/**
* Start the token manager - gets initial token and sets up refresh interval
* If initial token fetch fails, keeps retrying every few seconds until successful
*/
async start(): Promise<void> {
logger.info("Starting token manager...");
try {
await this.refreshToken();
this.setupRefreshInterval();
this.resolveTokenAvailable();
logger.info("Token manager started successfully");
} catch (error) {
logger.warn(`Failed to get initial token, will retry in ${this.retryIntervalMs / 1000} seconds:`, error);
this.setupRetryInterval();
}
}
/**
* Set up retry interval for initial token acquisition
*/
private setupRetryInterval(): void {
if (this.retryInterval) {
clearInterval(this.retryInterval);
}
this.retryInterval = setInterval(async () => {
try {
logger.debug("Retrying initial token acquisition");
await this.refreshToken();
this.setupRefreshInterval();
this.clearRetryInterval();
this.resolveTokenAvailable();
logger.info("Token manager started successfully after retry");
} catch (error) {
logger.debug("Token acquisition retry failed, will try again");
}
}, this.retryIntervalMs);
}
/**
* Clear retry interval
*/
private clearRetryInterval(): void {
if (this.retryInterval) {
clearInterval(this.retryInterval);
this.retryInterval = null;
}
}
/**
* Stop the token manager and clear all intervals
*/
stop(): void {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
this.clearRetryInterval();
logger.info("Token manager stopped");
}
/**
* Get the current valid token
*/
// TODO: WE SHOULD NOT BE GETTING A TOKEN EVERY TIME WE REQUEST IT
async getToken(): Promise<string> {
// If we don't have a token yet, wait for it to become available
if (!this.token && this.tokenAvailablePromise) {
await this.tokenAvailablePromise;
}
if (!this.token) {
if (this.isRefreshing) {
// Wait for current refresh to complete
await this.waitForRefresh();
} else {
throw new Error("No valid token available");
}
}
if (!this.token) {
throw new Error("No valid token available");
}
return this.token;
}
async getAuthHeader() {
return {
headers: {
Authorization: `Bearer ${await this.getToken()}`,
"X-CSRF-Token": "x-csrf-protection",
}
};
}
/**
* Force refresh the token
*/
async refreshToken(): Promise<void> {
if (this.isRefreshing) {
await this.waitForRefresh();
return;
}
this.isRefreshing = true;
try {
const hybridConfig = config.getRawConfig().managed;
if (
!hybridConfig?.id ||
!hybridConfig?.secret ||
!hybridConfig?.endpoint
) {
throw new Error("Hybrid configuration is not defined");
}
const tokenEndpoint = `${hybridConfig.endpoint}/api/v1/auth/remoteExitNode/get-token`;
const tokenData = {
remoteExitNodeId: hybridConfig.id,
secret: hybridConfig.secret
};
logger.debug("Requesting new token from server");
const response = await axios.post<TokenResponse>(
tokenEndpoint,
tokenData,
{
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": "x-csrf-protection"
},
timeout: 10000 // 10 second timeout
}
);
if (!response.data.success) {
throw new Error(
`Failed to get token: ${response.data.message}`
);
}
if (!response.data.data.token) {
throw new Error("Received empty token from server");
}
this.token = response.data.data.token;
logger.debug("Token refreshed successfully");
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error("Error updating proxy mapping:", {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url,
method: error.config?.method
});
} else {
logger.error("Error updating proxy mapping:", error);
}
throw new Error("Failed to refresh token");
} finally {
this.isRefreshing = false;
}
}
/**
* Set up automatic token refresh interval
*/
private setupRefreshInterval(): void {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
this.refreshInterval = setInterval(async () => {
try {
logger.debug("Auto-refreshing token");
await this.refreshToken();
} catch (error) {
logger.error("Failed to auto-refresh token:", error);
}
}, this.refreshIntervalMs);
}
/**
* Wait for current refresh operation to complete
*/
private async waitForRefresh(): Promise<void> {
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
if (!this.isRefreshing) {
clearInterval(checkInterval);
resolve();
}
}, 100);
});
}
}
// Export a singleton instance for use throughout the application
export const tokenManager = new TokenManager();

View File

@@ -6,14 +6,10 @@ import * as yaml from "js-yaml";
import axios from "axios"; import axios from "axios";
import { db, exitNodes } from "@server/db"; import { db, exitNodes } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { tokenManager } from "../tokenManager";
import { getCurrentExitNodeId } from "@server/lib/exitNodes"; import { getCurrentExitNodeId } from "@server/lib/exitNodes";
import { getTraefikConfig } from "./"; import { getTraefikConfig } from "#dynamic/lib/traefik";
import { import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
getValidCertificatesForDomains, import { sendToExitNode } from "#dynamic/lib/exitNodes";
getValidCertificatesForDomainsHybrid
} from "../remoteCertificates";
import { sendToExitNode } from "../exitNodes";
import { build } from "@server/build"; import { build } from "@server/build";
export class TraefikConfigManager { export class TraefikConfigManager {
@@ -313,93 +309,92 @@ export class TraefikConfigManager {
this.lastActiveDomains = new Set(domains); this.lastActiveDomains = new Set(domains);
} }
// Scan current local certificate state if (
this.lastLocalCertificateState = process.env.USE_PANGOLIN_DNS === "true" &&
await this.scanLocalCertificateState(); build != "oss"
) {
// Scan current local certificate state
this.lastLocalCertificateState =
await this.scanLocalCertificateState();
// Only fetch certificates if needed (domain changes, missing certs, or daily renewal check) // Only fetch certificates if needed (domain changes, missing certs, or daily renewal check)
let validCertificates: Array<{ let validCertificates: Array<{
id: number; id: number;
domain: string; domain: string;
wildcard: boolean | null; wildcard: boolean | null;
certFile: string | null; certFile: string | null;
keyFile: string | null; keyFile: string | null;
expiresAt: number | null; expiresAt: number | null;
updatedAt?: number | null; updatedAt?: number | null;
}> = []; }> = [];
if (this.shouldFetchCertificates(domains)) { if (this.shouldFetchCertificates(domains)) {
// Filter out domains that are already covered by wildcard certificates // Filter out domains that are already covered by wildcard certificates
const domainsToFetch = new Set<string>(); const domainsToFetch = new Set<string>();
for (const domain of domains) { for (const domain of domains) {
if ( if (
!isDomainCoveredByWildcard( !isDomainCoveredByWildcard(
domain, domain,
this.lastLocalCertificateState this.lastLocalCertificateState
) )
) { ) {
domainsToFetch.add(domain); domainsToFetch.add(domain);
} else { } else {
logger.debug( logger.debug(
`Domain ${domain} is covered by existing wildcard certificate, skipping fetch` `Domain ${domain} is covered by existing wildcard certificate, skipping fetch`
);
}
}
if (domainsToFetch.size > 0) {
// Get valid certificates for domains not covered by wildcards
if (config.isManagedMode()) {
validCertificates =
await getValidCertificatesForDomainsHybrid(
domainsToFetch
); );
} else { }
}
if (domainsToFetch.size > 0) {
// Get valid certificates for domains not covered by wildcards
validCertificates = validCertificates =
await getValidCertificatesForDomains( await getValidCertificatesForDomains(
domainsToFetch domainsToFetch
); );
this.lastCertificateFetch = new Date();
this.lastKnownDomains = new Set(domains);
logger.info(
`Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)`
);
// Download and decrypt new certificates
await this.processValidCertificates(validCertificates);
} else {
logger.info(
"All domains are covered by existing wildcard certificates, no fetch needed"
);
this.lastCertificateFetch = new Date();
this.lastKnownDomains = new Set(domains);
} }
this.lastCertificateFetch = new Date();
this.lastKnownDomains = new Set(domains);
logger.info( // Always ensure all existing certificates (including wildcards) are in the config
`Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)` await this.updateDynamicConfigFromLocalCerts(domains);
);
// Download and decrypt new certificates
await this.processValidCertificates(validCertificates);
} else { } else {
logger.info( const timeSinceLastFetch = this.lastCertificateFetch
"All domains are covered by existing wildcard certificates, no fetch needed" ? Math.round(
); (Date.now() -
this.lastCertificateFetch = new Date(); this.lastCertificateFetch.getTime()) /
this.lastKnownDomains = new Set(domains); (1000 * 60)
)
: 0;
// logger.debug(
// `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)`
// );
// Still need to ensure config is up to date with existing certificates
await this.updateDynamicConfigFromLocalCerts(domains);
} }
// Always ensure all existing certificates (including wildcards) are in the config // Clean up certificates for domains no longer in use
await this.updateDynamicConfigFromLocalCerts(domains); await this.cleanupUnusedCertificates(domains);
} else {
const timeSinceLastFetch = this.lastCertificateFetch
? Math.round(
(Date.now() - this.lastCertificateFetch.getTime()) /
(1000 * 60)
)
: 0;
// logger.debug( // wait 1 second for traefik to pick up the new certificates
// `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)` await new Promise((resolve) => setTimeout(resolve, 500));
// );
// Still need to ensure config is up to date with existing certificates
await this.updateDynamicConfigFromLocalCerts(domains);
} }
// Clean up certificates for domains no longer in use
await this.cleanupUnusedCertificates(domains);
// wait 1 second for traefik to pick up the new certificates
await new Promise((resolve) => setTimeout(resolve, 500));
// Write traefik config as YAML to a second dynamic config file if changed // Write traefik config as YAML to a second dynamic config file if changed
await this.writeTraefikDynamicConfig(traefikConfig); await this.writeTraefikDynamicConfig(traefikConfig);
@@ -448,32 +443,15 @@ export class TraefikConfigManager {
} | null> { } | null> {
let traefikConfig; let traefikConfig;
try { try {
if (config.isManagedMode()) { const currentExitNode = await getCurrentExitNodeId();
const resp = await axios.get( // logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`);
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/traefik-config`, traefikConfig = await getTraefikConfig(
await tokenManager.getAuthHeader() // this is called by the local exit node to get its own config
); currentExitNode,
config.getRawConfig().traefik.site_types,
if (resp.status !== 200) { build == "oss", // filter out the namespace domains in open source
logger.error( build != "oss" // generate the login pages on the cloud and hybrid
`Failed to fetch traefik config: ${resp.status} ${resp.statusText}`, );
{ responseData: resp.data }
);
return null;
}
traefikConfig = resp.data.data;
} else {
const currentExitNode = await getCurrentExitNodeId();
// logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`);
traefikConfig = await getTraefikConfig(
// this is called by the local exit node to get its own config
currentExitNode,
config.getRawConfig().traefik.site_types,
build == "oss", // filter out the namespace domains in open source
build != "oss" // generate the login pages on the cloud and hybrid
);
}
const domains = new Set<string>(); const domains = new Set<string>();
@@ -718,7 +696,12 @@ export class TraefikConfigManager {
for (const cert of validCertificates) { for (const cert of validCertificates) {
try { try {
if (!cert.certFile || !cert.keyFile) { if (
!cert.certFile ||
!cert.keyFile ||
cert.certFile.length === 0 ||
cert.keyFile.length === 0
) {
logger.warn( logger.warn(
`Certificate for domain ${cert.domain} is missing cert or key file` `Certificate for domain ${cert.domain} is missing cert or key file`
); );
@@ -842,7 +825,9 @@ export class TraefikConfigManager {
const lastUpdateStr = fs const lastUpdateStr = fs
.readFileSync(lastUpdatePath, "utf8") .readFileSync(lastUpdatePath, "utf8")
.trim(); .trim();
lastUpdateTime = Math.floor(new Date(lastUpdateStr).getTime() / 1000); lastUpdateTime = Math.floor(
new Date(lastUpdateStr).getTime() / 1000
);
} catch { } catch {
lastUpdateTime = null; lastUpdateTime = null;
} }

View File

@@ -1,9 +1,18 @@
import { db, exitNodes, targetHealthCheck } from "@server/db"; import { db, targetHealthCheck } from "@server/db";
import { and, eq, inArray, or, isNull, ne, isNotNull, desc } from "drizzle-orm"; import {
and,
eq,
inArray,
or,
isNull,
ne,
isNotNull,
desc,
sql
} from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { orgs, resources, sites, Target, targets } from "@server/db"; import { resources, sites, Target, targets } from "@server/db";
import { build } from "@server/build";
import createPathRewriteMiddleware from "./middleware"; import createPathRewriteMiddleware from "./middleware";
import { sanitize, validatePathRewriteConfig } from "./utils"; import { sanitize, validatePathRewriteConfig } from "./utils";
@@ -79,7 +88,13 @@ export async function getTraefikConfig(
and( and(
eq(targets.enabled, true), eq(targets.enabled, true),
eq(resources.enabled, true), eq(resources.enabled, true),
or(eq(sites.exitNodeId, exitNodeId), isNull(sites.exitNodeId)), or(
eq(sites.exitNodeId, exitNodeId),
and(
isNull(sites.exitNodeId),
sql`(${siteTypes.includes("local") ? 1 : 0} = 1)` // only allow local sites if "local" is in siteTypes
)
),
or( or(
ne(targetHealthCheck.hcHealth, "unhealthy"), // Exclude unhealthy targets ne(targetHealthCheck.hcHealth, "unhealthy"), // Exclude unhealthy targets
isNull(targetHealthCheck.hcHealth) // Include targets with no health check record isNull(targetHealthCheck.hcHealth) // Include targets with no health check record
@@ -105,7 +120,12 @@ export async function getTraefikConfig(
const priority = row.priority ?? 100; const priority = row.priority ?? 100;
// Create a unique key combining resourceId, path config, and rewrite config // Create a unique key combining resourceId, path config, and rewrite config
const pathKey = [targetPath, pathMatchType, rewritePath, rewritePathType] const pathKey = [
targetPath,
pathMatchType,
rewritePath,
rewritePathType
]
.filter(Boolean) .filter(Boolean)
.join("-"); .join("-");
const mapKey = [resourceId, pathKey].filter(Boolean).join("-"); const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
@@ -120,13 +140,15 @@ export async function getTraefikConfig(
); );
if (!validation.isValid) { if (!validation.isValid) {
logger.error(`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`); logger.error(
`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`
);
return; return;
} }
resourcesMap.set(key, { resourcesMap.set(key, {
resourceId: row.resourceId, resourceId: row.resourceId,
name: resourceName, name: resourceName,
fullDomain: row.fullDomain, fullDomain: row.fullDomain,
ssl: row.ssl, ssl: row.ssl,
http: row.http, http: row.http,
@@ -158,9 +180,6 @@ export async function getTraefikConfig(
port: row.port, port: row.port,
internalPort: row.internalPort, internalPort: row.internalPort,
enabled: row.targetEnabled, enabled: row.targetEnabled,
rewritePath: row.rewritePath,
rewritePathType: row.rewritePathType,
priority: row.priority,
site: { site: {
siteId: row.siteId, siteId: row.siteId,
type: row.siteType, type: row.siteType,
@@ -239,21 +258,18 @@ export async function getTraefikConfig(
preferWildcardCert = configDomain.prefer_wildcard_cert; preferWildcardCert = configDomain.prefer_wildcard_cert;
} }
let tls = {}; const tls = {
if (build == "oss") { certResolver: certResolver,
tls = { ...(preferWildcardCert
certResolver: certResolver, ? {
...(preferWildcardCert domains: [
? { {
domains: [ main: wildCard
{ }
main: wildCard ]
} }
] : {})
} };
: {})
};
}
const additionalMiddlewares = const additionalMiddlewares =
config.getRawConfig().traefik.additional_middlewares || []; config.getRawConfig().traefik.additional_middlewares || [];
@@ -264,11 +280,12 @@ export async function getTraefikConfig(
]; ];
// Handle path rewriting middleware // Handle path rewriting middleware
if (resource.rewritePath && if (
resource.path && resource.rewritePath !== null &&
resource.path !== null &&
resource.pathMatchType && resource.pathMatchType &&
resource.rewritePathType) { resource.rewritePathType
) {
// Create a unique middleware name // Create a unique middleware name
const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`; const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`;
@@ -287,7 +304,10 @@ export async function getTraefikConfig(
} }
// the middleware to the config // the middleware to the config
Object.assign(config_output.http.middlewares, rewriteResult.middlewares); Object.assign(
config_output.http.middlewares,
rewriteResult.middlewares
);
// middlewares to the router middleware chain // middlewares to the router middleware chain
if (rewriteResult.chain) { if (rewriteResult.chain) {
@@ -298,9 +318,13 @@ export async function getTraefikConfig(
routerMiddlewares.push(rewriteMiddlewareName); routerMiddlewares.push(rewriteMiddlewareName);
} }
logger.debug(`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`); logger.debug(
`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`
);
} catch (error) { } catch (error) {
logger.error(`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`); logger.error(
`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`
);
} }
} }
@@ -316,7 +340,9 @@ export async function getTraefikConfig(
value: string; value: string;
}[]; }[];
} catch (e) { } catch (e) {
logger.warn(`Failed to parse headers for resource ${resource.resourceId}: ${e}`); logger.warn(
`Failed to parse headers for resource ${resource.resourceId}: ${e}`
);
} }
headersArr.forEach((header) => { headersArr.forEach((header) => {
@@ -482,14 +508,14 @@ export async function getTraefikConfig(
})(), })(),
...(resource.stickySession ...(resource.stickySession
? { ? {
sticky: { sticky: {
cookie: { cookie: {
name: "p_sticky", // TODO: make this configurable via config.yml like other cookies name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
secure: resource.ssl, secure: resource.ssl,
httpOnly: true httpOnly: true
} }
} }
} }
: {}) : {})
} }
}; };
@@ -590,13 +616,13 @@ export async function getTraefikConfig(
})(), })(),
...(resource.stickySession ...(resource.stickySession
? { ? {
sticky: { sticky: {
ipStrategy: { ipStrategy: {
depth: 0, depth: 0,
sourcePort: true sourcePort: true
} }
} }
} }
: {}) : {})
} }
}; };

View File

@@ -1,11 +1 @@
import { build } from "@server/build"; export * from "./getTraefikConfig";
// Import both modules
import * as traefikModule from "./getTraefikConfig";
import * as privateTraefikModule from "./privateGetTraefikConfig";
// Conditionally export Traefik configuration implementation based on build type
const traefikImplementation = build === "oss" ? traefikModule : privateTraefikModule;
// Re-export all items from the selected implementation
export const { getTraefikConfig } = traefikImplementation;

View File

@@ -1,26 +1,17 @@
import { db } from "@server/db"; import { db, hostMeta, HostMeta } from "@server/db";
import { hostMeta, licenseKey, sites } from "@server/db";
import logger from "@server/logger";
import NodeCache from "node-cache";
import { validateJWT } from "./licenseJwt";
import { count, eq } from "drizzle-orm";
import moment from "moment";
import { setHostMeta } from "@server/lib/hostMeta"; import { setHostMeta } from "@server/lib/hostMeta";
import { encrypt, decrypt } from "@server/lib/crypto";
const keyTypes = ["HOST", "SITES"] as const; const keyTypes = ["host"] as const;
type KeyType = (typeof keyTypes)[number]; export type LicenseKeyType = (typeof keyTypes)[number];
const keyTiers = ["PROFESSIONAL", "ENTERPRISE"] as const; const keyTiers = ["personal", "enterprise"] as const;
type KeyTier = (typeof keyTiers)[number]; export type LicenseKeyTier = (typeof keyTiers)[number];
export type LicenseStatus = { export type LicenseStatus = {
isHostLicensed: boolean; // Are there any license keys? isHostLicensed: boolean; // Are there any license keys?
isLicenseValid: boolean; // Is the license key valid? isLicenseValid: boolean; // Is the license key valid?
hostId: string; // Host ID hostId: string; // Host ID
maxSites?: number; tier?: LicenseKeyTier;
usedSites?: number;
tier?: KeyTier;
}; };
export type LicenseKeyCache = { export type LicenseKeyCache = {
@@ -28,451 +19,27 @@ export type LicenseKeyCache = {
licenseKeyEncrypted: string; licenseKeyEncrypted: string;
valid: boolean; valid: boolean;
iat?: Date; iat?: Date;
type?: KeyType; type?: LicenseKeyType;
tier?: KeyTier; tier?: LicenseKeyTier;
numSites?: number; terminateAt?: Date;
};
type ActivateLicenseKeyAPIResponse = {
data: {
instanceId: string;
};
success: boolean;
error: string;
message: string;
status: number;
};
type ValidateLicenseAPIResponse = {
data: {
licenseKeys: {
[key: string]: string;
};
};
success: boolean;
error: string;
message: string;
status: number;
};
type TokenPayload = {
valid: boolean;
type: KeyType;
tier: KeyTier;
quantity: number;
terminateAt: string; // ISO
iat: number; // Issued at
}; };
export class License { export class License {
private phoneHomeInterval = 6 * 60 * 60; // 6 hours = 6 * 60 * 60 = 21600 seconds
private validationServerUrl =
"https://api.fossorial.io/api/v1/license/professional/validate";
private activationServerUrl =
"https://api.fossorial.io/api/v1/license/professional/activate";
private statusCache = new NodeCache({ stdTTL: this.phoneHomeInterval });
private licenseKeyCache = new NodeCache();
private ephemeralKey!: string;
private statusKey = "status";
private serverSecret!: string; private serverSecret!: string;
private publicKey = `-----BEGIN PUBLIC KEY----- constructor(private hostMeta: HostMeta) {}
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RKc8cw+G8r7h/xeozF
FNkRDggQfYO6Ae+EWHGujZ9WYAZ10spLh9F/zoLhhr3XhsjpoRXwMfgNuO5HstWf
CYM20I0l7EUUMWEyWd4tZLd+5XQ4jY5xWOCWyFJAGQSp7flcRmxdfde+l+xg9eKl
apbY84aVp09/GqM96hCS+CsQZrhohu/aOqYVB/eAhF01qsbmiZ7Y3WtdhTldveYt
h4mZWGmjf8d/aEgePf/tk1gp0BUxf+Ae5yqoAqU+6aiFbjJ7q1kgxc18PWFGfE9y
zSk+OZk887N5ThQ52154+oOUCMMR2Y3t5OH1hVZod51vuY2u5LsQXsf+87PwB91y
LQIDAQAB
-----END PUBLIC KEY-----`;
constructor(private hostId: string) { public async check(): Promise<LicenseStatus> {
this.ephemeralKey = Buffer.from( return {
JSON.stringify({ ts: new Date().toISOString() }) hostId: this.hostMeta.hostMetaId,
).toString("base64"); isHostLicensed: false,
isLicenseValid: false
setInterval( };
async () => {
await this.check();
},
1000 * 60 * 60
); // 1 hour = 60 * 60 = 3600 seconds
}
public listKeys(): LicenseKeyCache[] {
const keys = this.licenseKeyCache.keys();
return keys.map((key) => {
return this.licenseKeyCache.get<LicenseKeyCache>(key)!;
});
} }
public setServerSecret(secret: string) { public setServerSecret(secret: string) {
this.serverSecret = secret; this.serverSecret = secret;
} }
public async forceRecheck() {
this.statusCache.flushAll();
this.licenseKeyCache.flushAll();
return await this.check();
}
public async isUnlocked(): Promise<boolean> {
const status = await this.check();
if (status.isHostLicensed) {
if (status.isLicenseValid) {
return true;
}
}
return false;
}
public async check(): Promise<LicenseStatus> {
// Set used sites
const [siteCount] = await db
.select({
value: count()
})
.from(sites);
const status: LicenseStatus = {
hostId: this.hostId,
isHostLicensed: true,
isLicenseValid: false,
maxSites: undefined,
usedSites: siteCount.value
};
try {
if (this.statusCache.has(this.statusKey)) {
const res = this.statusCache.get("status") as LicenseStatus;
res.usedSites = status.usedSites;
return res;
}
// Invalidate all
this.licenseKeyCache.flushAll();
const allKeysRes = await db.select().from(licenseKey);
if (allKeysRes.length === 0) {
status.isHostLicensed = false;
return status;
}
let foundHostKey = false;
// Validate stored license keys
for (const key of allKeysRes) {
try {
// Decrypt the license key and token
const decryptedKey = decrypt(
key.licenseKeyId,
this.serverSecret
);
const decryptedToken = decrypt(
key.token,
this.serverSecret
);
const payload = validateJWT<TokenPayload>(
decryptedToken,
this.publicKey
);
this.licenseKeyCache.set<LicenseKeyCache>(decryptedKey, {
licenseKey: decryptedKey,
licenseKeyEncrypted: key.licenseKeyId,
valid: payload.valid,
type: payload.type,
tier: payload.tier,
numSites: payload.quantity,
iat: new Date(payload.iat * 1000)
});
if (payload.type === "HOST") {
foundHostKey = true;
}
} catch (e) {
logger.error(
`Error validating license key: ${key.licenseKeyId}`
);
logger.error(e);
this.licenseKeyCache.set<LicenseKeyCache>(
key.licenseKeyId,
{
licenseKey: key.licenseKeyId,
licenseKeyEncrypted: key.licenseKeyId,
valid: false
}
);
}
}
if (!foundHostKey && allKeysRes.length) {
logger.debug("No host license key found");
status.isHostLicensed = false;
}
const keys = allKeysRes.map((key) => ({
licenseKey: decrypt(key.licenseKeyId, this.serverSecret),
instanceId: decrypt(key.instanceId, this.serverSecret)
}));
let apiResponse: ValidateLicenseAPIResponse | undefined;
try {
// Phone home to validate license keys
apiResponse = await this.phoneHome(keys);
if (!apiResponse?.success) {
throw new Error(apiResponse?.error);
}
} catch (e) {
logger.error("Error communicating with license server:");
logger.error(e);
}
logger.debug("Validate response", apiResponse);
// Check and update all license keys with server response
for (const key of keys) {
try {
const cached = this.licenseKeyCache.get<LicenseKeyCache>(
key.licenseKey
)!;
const licenseKeyRes =
apiResponse?.data?.licenseKeys[key.licenseKey];
if (!apiResponse || !licenseKeyRes) {
logger.debug(
`No response from server for license key: ${key.licenseKey}`
);
if (cached.iat) {
const exp = moment(cached.iat)
.add(7, "days")
.toDate();
if (exp > new Date()) {
logger.debug(
`Using cached license key: ${key.licenseKey}, valid ${cached.valid}`
);
continue;
}
}
logger.debug(
`Can't trust license key: ${key.licenseKey}`
);
cached.valid = false;
this.licenseKeyCache.set<LicenseKeyCache>(
key.licenseKey,
cached
);
continue;
}
const payload = validateJWT<TokenPayload>(
licenseKeyRes,
this.publicKey
);
cached.valid = payload.valid;
cached.type = payload.type;
cached.tier = payload.tier;
cached.numSites = payload.quantity;
cached.iat = new Date(payload.iat * 1000);
// Encrypt the updated token before storing
const encryptedKey = encrypt(
key.licenseKey,
this.serverSecret
);
const encryptedToken = encrypt(
licenseKeyRes,
this.serverSecret
);
await db
.update(licenseKey)
.set({
token: encryptedToken
})
.where(eq(licenseKey.licenseKeyId, encryptedKey));
this.licenseKeyCache.set<LicenseKeyCache>(
key.licenseKey,
cached
);
} catch (e) {
logger.error(`Error validating license key: ${key}`);
logger.error(e);
}
}
// Compute host status
for (const key of keys) {
const cached = this.licenseKeyCache.get<LicenseKeyCache>(
key.licenseKey
)!;
logger.debug("Checking key", cached);
if (cached.type === "HOST") {
status.isLicenseValid = cached.valid;
status.tier = cached.tier;
}
if (!cached.valid) {
continue;
}
if (!status.maxSites) {
status.maxSites = 0;
}
status.maxSites += cached.numSites || 0;
}
} catch (error) {
logger.error("Error checking license status:");
logger.error(error);
}
this.statusCache.set(this.statusKey, status);
return status;
}
public async activateLicenseKey(key: string) {
// Encrypt the license key before storing
const encryptedKey = encrypt(key, this.serverSecret);
const [existingKey] = await db
.select()
.from(licenseKey)
.where(eq(licenseKey.licenseKeyId, encryptedKey))
.limit(1);
if (existingKey) {
throw new Error("License key already exists");
}
let instanceId: string | undefined;
try {
// Call activate
const apiResponse = await fetch(this.activationServerUrl, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
licenseKey: key,
instanceName: this.hostId
})
});
const data = await apiResponse.json();
if (!data.success) {
throw new Error(`${data.message || data.error}`);
}
const response = data as ActivateLicenseKeyAPIResponse;
if (!response.data) {
throw new Error("No response from server");
}
if (!response.data.instanceId) {
throw new Error("No instance ID in response");
}
instanceId = response.data.instanceId;
} catch (error) {
throw Error(`Error activating license key: ${error}`);
}
// Phone home to validate license key
const keys = [
{
licenseKey: key,
instanceId: instanceId!
}
];
let validateResponse: ValidateLicenseAPIResponse;
try {
validateResponse = await this.phoneHome(keys);
if (!validateResponse) {
throw new Error("No response from server");
}
if (!validateResponse.success) {
throw new Error(validateResponse.error);
}
// Validate the license key
const licenseKeyRes = validateResponse.data.licenseKeys[key];
if (!licenseKeyRes) {
throw new Error("Invalid license key");
}
const payload = validateJWT<TokenPayload>(
licenseKeyRes,
this.publicKey
);
if (!payload.valid) {
throw new Error("Invalid license key");
}
const encryptedToken = encrypt(licenseKeyRes, this.serverSecret);
// Encrypt the instanceId before storing
const encryptedInstanceId = encrypt(instanceId!, this.serverSecret);
// Store the license key in the database
await db.insert(licenseKey).values({
licenseKeyId: encryptedKey,
token: encryptedToken,
instanceId: encryptedInstanceId
});
} catch (error) {
throw Error(`Error validating license key: ${error}`);
}
// Invalidate the cache and re-compute the status
return await this.forceRecheck();
}
private async phoneHome(
keys: {
licenseKey: string;
instanceId: string;
}[]
): Promise<ValidateLicenseAPIResponse> {
// Decrypt the instanceIds before sending to the server
const decryptedKeys = keys.map((key) => ({
licenseKey: key.licenseKey,
instanceId: key.instanceId
? decrypt(key.instanceId, this.serverSecret)
: key.instanceId
}));
const response = await fetch(this.validationServerUrl, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
licenseKeys: decryptedKeys,
ephemeralKey: this.ephemeralKey,
instanceName: this.hostId
})
});
const data = await response.json();
return data as ValidateLicenseAPIResponse;
}
} }
await setHostMeta(); await setHostMeta();
@@ -483,6 +50,6 @@ if (!info) {
throw new Error("Host information not found"); throw new Error("Host information not found");
} }
export const license = new License(info.hostMetaId); export const license = new License(info);
export default license; export default license;

View File

@@ -21,10 +21,9 @@ export * from "./verifyIsLoggedInUser";
export * from "./verifyIsLoggedInUser"; export * from "./verifyIsLoggedInUser";
export * from "./verifyClientAccess"; export * from "./verifyClientAccess";
export * from "./integration"; export * from "./integration";
export * from "./verifyValidLicense";
export * from "./verifyUserHasAction"; export * from "./verifyUserHasAction";
export * from "./verifyApiKeyAccess"; export * from "./verifyApiKeyAccess";
export * from "./verifyDomainAccess"; export * from "./verifyDomainAccess";
export * from "./verifyClientsEnabled"; export * from "./verifyClientsEnabled";
export * from "./verifyUserIsOrgOwner"; export * from "./verifyUserIsOrgOwner";
export * from "./verifySiteResourceAccess"; export * from "./verifySiteResourceAccess";

View File

@@ -11,7 +11,6 @@ export enum OpenAPITags {
Invitation = "Invitation", Invitation = "Invitation",
Target = "Target", Target = "Target",
Rule = "Rule", Rule = "Rule",
RuleTemplate = "Rule Template",
AccessToken = "Access Token", AccessToken = "Access Token",
Idp = "Identity Provider", Idp = "Identity Provider",
Client = "Client", Client = "Client",

28
server/private/cleanup.ts Normal file
View File

@@ -0,0 +1,28 @@
/*
* 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 { rateLimitService } from "#private/lib/rateLimit";
import { cleanup as wsCleanup } from "#private/routers/ws";
async function cleanup() {
await rateLimitService.cleanup();
await wsCleanup();
process.exit(0);
}
export async function initCleanup() {
// Handle process termination
process.on("SIGTERM", () => cleanup());
process.on("SIGINT", () => cleanup());
}

View File

@@ -13,7 +13,7 @@
import { customers, db } from "@server/db"; import { customers, db } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import stripe from "@server/lib/private/stripe"; import stripe from "#private/lib/stripe";
import { build } from "@server/build"; import { build } from "@server/build";
export async function createCustomer( export async function createCustomer(

View File

@@ -0,0 +1,46 @@
/*
* 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 { getTierPriceSet } from "@server/lib/billing/tiers";
import { getOrgSubscriptionData } from "#private/routers/billing/getOrgSubscription";
import { build } from "@server/build";
export async function getOrgTierData(
orgId: string
): Promise<{ tier: string | null; active: boolean }> {
let tier = null;
let active = false;
if (build !== "saas") {
return { tier, active };
}
const { subscription, items } = await getOrgSubscriptionData(orgId);
if (items && items.length > 0) {
const tierPriceSet = getTierPriceSet();
// Iterate through tiers in order (earlier keys are higher tiers)
for (const [tierId, priceId] of Object.entries(tierPriceSet)) {
// Check if any subscription item matches this tier's price ID
const matchingItem = items.find((item) => item.priceId === priceId);
if (matchingItem) {
tier = tierId;
break;
}
}
}
if (subscription && subscription.status === "active") {
active = true;
}
return { tier, active };
}

View File

@@ -11,6 +11,5 @@
* This file is not licensed under the AGPLv3. * This file is not licensed under the AGPLv3.
*/ */
export * from "./limitSet"; export * from "./getOrgTierData";
export * from "./features"; export * from "./createCustomer";
export * from "./limitsService";

Some files were not shown because too many files have changed in this diff Show More