Compare commits

..

434 Commits

Author SHA1 Message Date
miloschwartz
a3b852ef45 Merge branch 'dev' into clients-user 2025-12-05 15:17:32 -05:00
miloschwartz
53bb4efbb2 change tunnel to site 2025-12-05 14:58:09 -05:00
miloschwartz
96dbec9352 small fixes from testing 2025-12-05 14:48:33 -05:00
miloschwartz
2d3fbb9704 translate setup page 2025-12-05 12:19:40 -05:00
miloschwartz
d3be1fbf4c update descriptions and add adress back 2025-12-05 12:09:13 -05:00
Owen
89ee57cdf9 Enforce fqdn 2025-12-05 12:03:00 -05:00
miloschwartz
bdfc7fbcdb change phrase 2025-12-05 11:53:13 -05:00
miloschwartz
8726a7f931 remove device code ip check and fix edit resource dialog state issue 2025-12-05 11:47:59 -05:00
miloschwartz
1cae815be5 split install and run commands 2025-12-05 10:51:38 -05:00
miloschwartz
c5befee134 fix close button spacing on mobile 2025-12-05 10:08:35 -05:00
miloschwartz
9cf2dbc2cc fix login page spacing on mobile 2025-12-05 10:04:12 -05:00
Owen
35f9c67cfe Merge branch 'main' into dev 2025-12-05 09:45:17 -05:00
Owen
6707b3c7fe Merge branch 'main' of github.com:fosrl/pangolin 2025-12-05 09:42:58 -05:00
Owen Schwartz
dfb85f2c89 Merge pull request #1980 from bjoernch/patch-1
Update de-DE.json
2025-12-05 09:42:30 -05:00
Björn Felgner
17dec6cf0b Update de-DE.json
I noticed an odd translation in the Pangolin dashboard for the Client feature. It is currently translated into German as “Kunden”, which actually means customers. In German, there is no 1:1 translation for the IT term client, so this wording is misleading. I would suggest removing the translation entirely and leaving it as "Client" which correctly conveys the meaning of client devices.
2025-12-05 10:41:54 +01:00
miloschwartz
8ee4ee7baf remove bg-muted on target sep 2025-12-04 22:11:27 -05:00
Owen
b1b0702886 Make query optional 2025-12-04 22:07:48 -05:00
Owen
92aed108cd Update package 2025-12-04 22:07:48 -05:00
miloschwartz
2dcc94cd14 fix hc port NaN issue 2025-12-04 22:03:37 -05:00
miloschwartz
a7185ff913 add auth info tip 2025-12-04 21:28:42 -05:00
miloschwartz
04e73515b8 add alias to client resources table 2025-12-04 21:21:48 -05:00
miloschwartz
2bad9daaea move edit resource to proxy subpath 2025-12-04 21:18:17 -05:00
miloschwartz
54670e150d simplify create site wizard 2025-12-04 21:12:14 -05:00
miloschwartz
761ed1de9a ensure unique niceId for site resources and normal resources 2025-12-04 21:07:14 -05:00
miloschwartz
078692c818 invalidate queries on save 2025-12-04 17:56:11 -05:00
Owen
53ab51691a update packages 2025-12-04 17:26:24 -05:00
Milo Schwartz
54e2d95b55 Merge pull request #1977 from Fredkiss3/fix/some-fixes
fix: bugs introduced in `separate-tables`
2025-12-04 14:25:30 -08:00
miloschwartz
6e6fa77625 bump version 2025-12-04 17:10:59 -05:00
Owen
5c0c12cabe Update lock 2025-12-04 17:02:45 -05:00
Owen
b3ed7c0129 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-12-04 17:00:54 -05:00
miloschwartz
10a00ff225 update next version 2025-12-04 16:56:39 -05:00
Fred KISSIE
ba09479827 ♻️ organize imports 2025-12-04 22:50:17 +01:00
Fred KISSIE
1c5c36fc12 ♻️ set the staleTime to Zero for queries so that they are refetched everytime 2025-12-04 22:50:04 +01:00
Fred KISSIE
d37ff6e15b 🐛 resource rols & resource clients shouldn't have the same query key 2025-12-04 22:49:40 +01:00
Owen Schwartz
9288575341 Merge pull request #1971 from water-sucks/add-tls-server-name-to-health-check-fields
feat(healthcheck): add SNI support for target healthchecks
2025-12-04 14:42:25 -05:00
Fred KISSIE
0ceed4c812 📦 update lockfile 2025-12-04 20:30:41 +01:00
Owen
4b61a38501 Merge branch 'add-tls-server-name-to-health-check-fields' of github.com:water-sucks/pangolin into dev 2025-12-04 12:11:41 -05:00
Varun Narravula
ca9273c9ea feat(healthcheck): add SNI input field to target healthcheck config 2025-12-04 12:11:25 -05:00
Owen
810704e190 Merge branch 'add-tls-server-name-to-health-check-fields' of github.com:water-sucks/pangolin into dev 2025-12-04 12:00:51 -05:00
Varun Narravula
f33be1434b feat(schema): add TLS server name column to target healthcheck tables 2025-12-04 12:00:40 -05:00
Varun Narravula
82a9f2b24f feat(healthcheck): add SNI input field to target healthcheck config 2025-12-04 12:00:40 -05:00
Owen
7204b5f0de Merge branch 'add-tls-server-name-to-health-check-fields' of github.com:water-sucks/pangolin into dev 2025-12-04 12:00:04 -05:00
Owen
9b372780bd Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-12-04 11:59:53 -05:00
Varun Narravula
9065385b87 feat(healthcheck): add SNI input field to target healthcheck config 2025-12-04 11:59:18 -05:00
miloschwartz
77306e8c97 add integration routes 2025-12-04 11:48:01 -05:00
miloschwartz
a746ef36a8 Merge branch 'dev' into clients-user 2025-12-04 11:38:05 -05:00
Owen
6e565f1331 Merge branch 'add-tls-server-name-to-health-check-fields' of github.com:water-sucks/pangolin into dev 2025-12-04 11:28:47 -05:00
Varun Narravula
84c608c2cf feat(healthcheck): add SNI input field to target healthcheck config 2025-12-04 11:27:18 -05:00
Milo Schwartz
6da7f58ced Merge pull request #1897 from Fredkiss3/feat/log-analytics
feat: request log analytics
2025-12-04 07:38:54 -08:00
Varun Narravula
351097b04d feat(healthcheck): add SNI input field to target healthcheck config 2025-12-04 10:33:01 -05:00
Varun Narravula
bd3d339905 feat(schema): add TLS server name column to target healthcheck tables 2025-12-04 10:18:20 -05:00
miloschwartz
c6ad36d78e update to next 15.5.7 2025-12-04 09:58:21 -05:00
miloschwartz
eaeb65e9b4 update wording 2025-12-03 22:26:22 -05:00
miloschwartz
4176bdbc81 clarify rules action types closes #1679 2025-12-03 21:30:44 -05:00
miloschwartz
a2cdd8484c changes to wording 2025-12-03 21:17:10 -05:00
Milo Schwartz
23ab76ae08 Merge pull request #1967 from Fredkiss3/refactor/separate-tables-2
Refactor: separate tables (2)
2025-12-03 17:31:46 -08:00
Owen
8eec122114 Fixing holepunching and other bugs 2025-12-03 20:31:37 -05:00
Fred KISSIE
79ccbc8e92 ♻️ compute everything in useQueries 2025-12-04 00:51:56 +01:00
Fred KISSIE
d70da2aa70 🐛 fix paths 2025-12-04 00:51:40 +01:00
Fred KISSIE
c695f50122 ♻️ use Queries 2025-12-04 00:42:59 +01:00
Fred KISSIE
1b09e5b9f9 🚚 move subpages to correct paths 2025-12-04 00:42:50 +01:00
miloschwartz
7efc947e26 auto collapse sidebar on small screens 2025-12-03 18:33:46 -05:00
miloschwartz
4b580105cd change default sort on logs tables closes #1907 2025-12-03 18:20:28 -05:00
miloschwartz
a61c82570a add logs routes to integration api routes closes #1963 2025-12-03 17:45:19 -05:00
Fred KISSIE
6734003d85 ⬆️ upgrade react & next to fix **CVE-2025-55182** 2025-12-03 22:58:02 +01:00
miloschwartz
e49d796b06 fix headers getting cleared on resource save and hide domain type without pangolin dns 2025-12-03 16:04:44 -05:00
miloschwartz
4ab4029625 ease expand animation a little 2025-12-03 15:53:41 -05:00
miloschwartz
5afff3c662 add extra org policy checks to middlewares 2025-12-03 15:50:24 -05:00
miloschwartz
9be5a01173 add niceId col back to table but hide by default 2025-12-03 15:27:58 -05:00
miloschwartz
357f297a3e remove enable_clients flag from config 2025-12-03 15:02:39 -05:00
miloschwartz
e1edbe6067 remove double clients permissions check boxes 2025-12-03 14:56:10 -05:00
miloschwartz
5a859aad29 update create client description 2025-12-03 14:52:57 -05:00
miloschwartz
a28b15a81d update descriptions 2025-12-03 14:47:59 -05:00
miloschwartz
e62186f395 change olm creds text 2025-12-03 14:46:57 -05:00
miloschwartz
11c1efc19c refactor to use DataTable component 2025-12-03 14:45:21 -05:00
Milo Schwartz
8b0491eb52 Merge pull request #1960 from Fredkiss3/refactor/separate-tables
refactor: separate tables
2025-12-03 11:28:21 -08:00
miloschwartz
0032634004 add owner devices to org on create org 2025-12-03 14:19:18 -05:00
miloschwartz
4af10c8108 change to --disable-clients flag 2025-12-03 14:12:53 -05:00
miloschwartz
56cb685813 fix spinner 2025-12-03 14:05:02 -05:00
miloschwartz
ccfe1f7d0a update description text for subnets 2025-12-03 14:01:13 -05:00
Fred KISSIE
bf987d867c 🚧 WIP 2025-12-03 19:28:07 +01:00
Fred KISSIE
3870ced635 Merge branch 'clients-user' into refactor/separate-tables 2025-12-03 17:01:50 +01:00
Fred KISSIE
cb3861a5c8 🚚 rename react-query-provider to TanstackQueryProvider 2025-12-03 16:58:40 +01:00
Fred KISSIE
f5bfddd262 🚨 run eslint --fix 2025-12-03 16:58:12 +01:00
Fred KISSIE
f060063f53 💬 update text 2025-12-02 19:24:02 +01:00
Fred KISSIE
6eb6b44f41 💬 update some text labels 2025-12-02 19:22:43 +01:00
Fred KISSIE
c93ab34021 ♻️ some refactors 2025-12-02 19:08:35 +01:00
Fred KISSIE
06a31bb716 ♻️ separate machine client & user devices tables + move common functions into hooks 2025-12-02 18:58:51 +01:00
Owen
152fb47ca4 Handle unrelay and relaying better 2025-12-02 11:17:08 -05:00
Fred KISSIE
3d400b2321 ♻️ ignore hydrateSaas script and exit(0) on PG migrations 2025-12-02 16:06:10 +01:00
Fred KISSIE
45a82f3ecc 🚧WIP: Separate user & machine clients 2025-12-02 03:14:02 +01:00
Fred KISSIE
342bedc012 🎨 format with prettier 2025-12-02 02:40:50 +01:00
Fred KISSIE
18db4a11c8 ♻️ separate client & proxy resources tables 2025-12-02 02:33:43 +01:00
Owen
a7e32d4013 Fix bugs with updating a resource 2025-12-01 19:57:23 -05:00
Owen
beea28daf3 Handle hp oddities 2025-12-01 16:20:10 -05:00
Owen
b5e94d44ae Fix switching orgs having connections from other orgs 2025-12-01 15:44:25 -05:00
Owen
a623604e96 Improve holepunching 2025-12-01 13:54:30 -05:00
miloschwartz
8c62dfa706 respond with relative code expiration time 2025-12-01 12:36:13 -05:00
Fred KISSIE
610e46f2d5 🚧 WIP: separate proxy & client resources 2025-12-01 18:26:32 +01:00
Owen
92125611e9 Add validation and fix thrown error from updatePeerData 2025-11-30 17:49:55 -05:00
Owen
096da391e5 Add a utility subnet 2025-11-30 17:38:12 -05:00
Owen
dd6b1d88d3 Update peer data when HP changes 2025-11-30 11:39:40 -05:00
Owen
79f0d60533 Start working on HP IP changes 2025-11-30 11:39:40 -05:00
Owen
67665864c2 Clarify that PP is only for TCP 2025-11-29 22:58:09 -05:00
Owen Schwartz
c4de617751 Merge pull request #1940 from fosrl/dependabot/npm_and_yarn/multi-4aa959df0f
Bump dompurify and monaco-editor
2025-11-29 13:15:49 -05:00
Owen Schwartz
19e3c5045e Merge pull request #1942 from fosrl/dependabot/npm_and_yarn/multi-f170272c46
Bump glob and npm
2025-11-29 13:15:30 -05:00
Owen Schwartz
9f63d8bb5b Merge pull request #1941 from fosrl/dependabot/npm_and_yarn/multi-b50d6d7a59
Bump tar and npm
2025-11-29 13:15:09 -05:00
dependabot[bot]
49348c6ab7 Bump glob and npm
Bumps [glob](https://github.com/isaacs/node-glob) to 11.1.0 and updates ancestor dependencies [glob](https://github.com/isaacs/node-glob) and [npm](https://github.com/npm/cli). These dependencies need to be updated together.


Updates `glob` from 11.0.3 to 11.1.0
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v11.0.3...v11.1.0)

Updates `glob` from 10.4.5 to 10.5.0
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v11.0.3...v11.1.0)

Updates `npm` from 11.6.2 to 11.6.4
- [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.2...v11.6.4)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 11.1.0
  dependency-type: direct:production
- dependency-name: glob
  dependency-version: 10.5.0
  dependency-type: indirect
- dependency-name: npm
  dependency-version: 11.6.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-29 18:11:23 +00:00
dependabot[bot]
0961ac1da1 Bump tar and npm
Removes [tar](https://github.com/isaacs/node-tar). It's no longer used after updating ancestor dependency [npm](https://github.com/npm/cli). These dependencies need to be updated together.


Removes `tar`

Updates `npm` from 11.6.2 to 11.6.4
- [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.2...v11.6.4)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 
  dependency-type: indirect
- dependency-name: npm
  dependency-version: 11.6.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-29 18:11:22 +00:00
dependabot[bot]
6a79436516 Bump dompurify and monaco-editor
Bumps [dompurify](https://github.com/cure53/DOMPurify) and [monaco-editor](https://github.com/microsoft/monaco-editor). These dependencies needed to be updated together.

Updates `dompurify` from 3.1.7 to 3.2.7
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.1.7...3.2.7)

Updates `monaco-editor` from 0.54.0 to 0.55.1
- [Release notes](https://github.com/microsoft/monaco-editor/releases)
- [Changelog](https://github.com/microsoft/monaco-editor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/microsoft/monaco-editor/compare/v0.54.0...v0.55.1)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.2.7
  dependency-type: indirect
- dependency-name: monaco-editor
  dependency-version: 0.55.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-29 18:11:10 +00:00
Owen Schwartz
85b46392e1 Merge pull request #1922 from fosrl/dependabot/npm_and_yarn/body-parser-2.2.1
Bump body-parser from 2.2.0 to 2.2.1
2025-11-29 13:10:02 -05:00
Owen Schwartz
f721c983aa Merge pull request #1936 from PavanendraBaahubali/Pavan/fix-custom-header-reset
Fix: prevent custom headers from being cleared on save
2025-11-29 11:00:46 -05:00
Pavan Kumar
ff0b30fc2e Merge branch 'main' of https://github.com/fosrl/pangolin into fix-custom-header-reset 2025-11-28 19:06:42 +05:30
Pavan Kumar
18070a37a8 fix: keep custom header values when editing resource 2025-11-28 19:06:09 +05:30
miloschwartz
5bd31f87f0 only allow one device auth per session 2025-11-26 15:48:49 -05:00
Owen
de83cf9d8c Handle delete org and checking org policy 2025-11-26 15:35:33 -05:00
Owen
ceae787cf5 Attempt to handle creating/deleting clients and role 2025-11-25 18:20:02 -05:00
Owen
ce6afd0019 Merge branch 'clients-user' of github.com:fosrl/pangolin into clients-user 2025-11-25 15:47:19 -05:00
miloschwartz
d977d57b2a use border instead of bg 2025-11-25 15:45:32 -05:00
dependabot[bot]
7bcd6adf01 Bump body-parser from 2.2.0 to 2.2.1
Bumps [body-parser](https://github.com/expressjs/body-parser) from 2.2.0 to 2.2.1.
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/v2.2.0...v2.2.1)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-version: 2.2.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 18:31:04 +00:00
miloschwartz
ac68dbd545 add my-device and force login 2025-11-25 10:51:53 -05:00
Owen Schwartz
d450e2c3ab Merge pull request #1920 from ThanatosDi/feat/add-zh-tw-language
feat: Add zh-TW language
2025-11-25 10:17:46 -05:00
古丁丁
9440a4f879 feat: Add zh-TW language 2025-11-25 11:23:48 +08:00
Owen
73b0411e1c Add alias config 2025-11-24 20:43:26 -05:00
Fred KISSIE
6368b9d837 ♻️ use linechart 2025-11-21 06:33:47 +01:00
Fred KISSIE
1b643fb4b6 🐛 fix Dockerfile 2025-11-21 06:27:13 +01:00
Fred KISSIE
d118c6b666 ♻️also export build file 2025-11-21 06:18:32 +01:00
Fred KISSIE
380e062d25 ♻️export driver in Dockerfile 2025-11-21 06:17:14 +01:00
Fred KISSIE
261f0333b8 💄 remove chart animations 2025-11-21 06:15:05 +01:00
Fred KISSIE
24adca6108 ♻️add auto refetch every 30 seconds 2025-11-21 06:14:48 +01:00
Fred KISSIE
3f440f0f7a 🏷️ fix type for SQLite 2025-11-21 06:10:01 +01:00
Fred KISSIE
ba6defa87c Add request by day chart 2025-11-21 06:03:34 +01:00
Fred KISSIE
887a0ef574 💄 chart for analytics 2025-11-21 05:36:30 +01:00
Fred KISSIE
200743747d 🚧add css variables for chart 2025-11-21 04:51:10 +01:00
Fred KISSIE
2082c5eed2 🚧 Add shadCN chart 2025-11-21 04:50:06 +01:00
Fred KISSIE
a42d012788 load logs per day 2025-11-21 04:48:01 +01:00
Fred KISSIE
82cc51424b 🔨also export driver in the db driver generation script 2025-11-21 04:47:42 +01:00
Fred KISSIE
7924f195aa 💄handle empty data 2025-11-21 04:47:13 +01:00
Fred KISSIE
d41bd3023f 🐛 filter by resource UI 2025-11-21 03:05:40 +01:00
Fred KISSIE
87a0dd2d12 ♻️ remove click 2025-11-21 02:57:44 +01:00
Fred KISSIE
5fd64596eb add top countries list 2025-11-21 02:00:47 +01:00
Owen
d23f61d995 Take into account the existing associations
Use to filter adds and removes in the associations
2025-11-20 16:42:55 -05:00
Owen
7ac27b3883 Switch to update 2025-11-20 16:08:03 -05:00
Owen
9420b41e39 Update the remote subnets 2025-11-20 15:17:48 -05:00
Owen
2cfb0e05cf Lock working without redis? 2025-11-20 14:03:25 -05:00
Owen
5b9386b18a Add lock 2025-11-20 12:40:25 -05:00
Owen
f5c3dff43c Some small bug fixes 2025-11-20 12:24:24 -05:00
Owen
eeb82c8cfe Merge branch 'main' of github.com:fosrl/pangolin 2025-11-20 10:36:38 -05:00
Owen
3750c36aa7 Working on orchestration 2025-11-20 10:31:09 -05:00
Fred KISSIE
3801354ae6 🚧 add country code flag emoji function 2025-11-20 08:37:49 +01:00
Fred KISSIE
266fbb1762 💄nicer colors 2025-11-20 08:22:16 +01:00
Fred KISSIE
5d1f81a92c world map 2025-11-20 08:19:11 +01:00
Fred KISSIE
d6e8eb5307 🧑‍💻add tailwind indicator component 2025-11-20 05:23:16 +01:00
Fred KISSIE
2bc82f49ed add enpoint for getting all resource names 2025-11-20 04:20:31 +01:00
Fred KISSIE
487985558d add react compiler 2025-11-20 04:19:58 +01:00
Fred KISSIE
dc237b8052 💬 update text message from the API 2025-11-20 03:19:43 +01:00
Fred KISSIE
4ed4515262 🚧 starting request analytics page 2025-11-20 02:55:52 +01:00
Fred KISSIE
cd76fa0139 add analytics endpoint 2025-11-20 02:55:33 +01:00
Fred KISSIE
af4b9e83f7 ✏️ fix typos 2025-11-20 02:55:03 +01:00
Owen
fa5facdf33 Fix bugs 2025-11-19 20:03:57 -05:00
Owen
937b36e756 Build client site resource associations and send messages 2025-11-19 18:05:42 -05:00
Fred KISSIE
e90bdf8f97 ♻️ translate sidebar headings 2025-11-19 21:43:34 +01:00
Owen Schwartz
56491cc17b Merge pull request #1896 from fosrl/copilot/configure-auto-login-idp-blueprints
Add blueprint support for auto-login-idp configuration
2025-11-19 14:33:17 -05:00
copilot-swe-agent[bot]
6da531e99b Use IDP ID instead of IDP name for auto-login-idp
Co-authored-by: oschwartz10612 <4999704+oschwartz10612@users.noreply.github.com>
2025-11-19 19:29:52 +00:00
copilot-swe-agent[bot]
01b5158b73 Add auto-login-idp support to blueprints
Co-authored-by: oschwartz10612 <4999704+oschwartz10612@users.noreply.github.com>
2025-11-19 16:50:06 +00:00
copilot-swe-agent[bot]
8f9b665bef Initial plan 2025-11-19 16:43:50 +00:00
Owen
806949879a Merge branch 'dev' into clients-user 2025-11-18 13:53:12 -05:00
Owen
e72e2b53aa Working on targets 2025-11-18 13:53:04 -05:00
Owen Schwartz
10f42fe2e6 Merge pull request #1884 from v1rusnl/main
Bump Traefik to v3.6 due to Docker 29.X.X compatibility
2025-11-18 09:42:38 -05:00
v1rusnl
51b438117a Update Traefik image version to v3.6 2025-11-18 12:44:10 +01:00
v1rusnl
d73825dd24 Update Traefik image version to v3.6 2025-11-18 12:41:12 +01:00
miloschwartz
b5c6191c67 add email consent and update audience 2025-11-17 20:50:24 -05:00
Owen
97c707248e Working on updating targets 2025-11-17 20:44:39 -05:00
miloschwartz
02fbc279b5 add email consent and update audience 2025-11-17 20:37:24 -05:00
Owen Schwartz
80a68507cd Merge pull request #1876 from fosrl/crowdin_dev
New Crowdin updates
2025-11-17 11:48:54 -05:00
Owen
dbb1e37033 Update lock 2025-11-17 11:30:25 -05:00
Owen
364b84359e Merge branch 'dev' into clients-user 2025-11-17 11:30:12 -05:00
Owen
93d4a40977 Merge branch 'main' into dev 2025-11-17 11:30:05 -05:00
Owen
97312343e4 Merge branch 'dev' into clients-user 2025-11-17 11:28:47 -05:00
Owen Schwartz
1736ad486a New translations en-us.json (Norwegian Bokmal) 2025-11-17 11:03:26 -05:00
Owen Schwartz
a07ad843a2 New translations en-us.json (Chinese Simplified) 2025-11-17 11:03:24 -05:00
Owen Schwartz
fef9101058 New translations en-us.json (Turkish) 2025-11-17 11:03:23 -05:00
Owen Schwartz
2890ff2605 New translations en-us.json (Russian) 2025-11-17 11:03:21 -05:00
Owen Schwartz
026ad2ccb9 New translations en-us.json (Portuguese) 2025-11-17 11:03:19 -05:00
Owen Schwartz
a82969b778 New translations en-us.json (Polish) 2025-11-17 11:03:18 -05:00
Owen Schwartz
612b04c26f New translations en-us.json (Dutch) 2025-11-17 11:03:16 -05:00
Owen Schwartz
2162f5f76f New translations en-us.json (Korean) 2025-11-17 11:03:14 -05:00
Owen Schwartz
710f16ce68 New translations en-us.json (Italian) 2025-11-17 11:03:13 -05:00
Owen Schwartz
61a4f468ba New translations en-us.json (German) 2025-11-17 11:03:11 -05:00
Owen Schwartz
b00fea5656 New translations en-us.json (Czech) 2025-11-17 11:03:09 -05:00
Owen Schwartz
269ff630aa New translations en-us.json (Bulgarian) 2025-11-17 11:03:08 -05:00
Owen Schwartz
986f7121bd New translations en-us.json (Spanish) 2025-11-17 11:03:06 -05:00
Owen Schwartz
21f0501bc6 New translations en-us.json (French) 2025-11-17 11:03:04 -05:00
Owen Schwartz
2b31dd955c Merge pull request #1848 from fosrl/dependabot/npm_and_yarn/dev-minor-updates-040abfaff9
Bump the dev-minor-updates group across 1 directory with 3 updates
2025-11-17 10:54:38 -05:00
Owen Schwartz
e7aeb4ff89 Merge pull request #1849 from fosrl/dependabot/go_modules/install/prod-minor-updates-4e8dbec1a6
Bump golang.org/x/term from 0.36.0 to 0.37.0 in /install in the prod-minor-updates group
2025-11-17 10:54:23 -05:00
Owen Schwartz
9dd1192033 Merge pull request #1855 from fosrl/dependabot/npm_and_yarn/prod-patch-updates-6d8f9bd785
Bump the prod-patch-updates group across 1 directory with 12 updates
2025-11-17 10:54:15 -05:00
Owen Schwartz
e61da0958f Merge pull request #1841 from fosrl/dependabot/github_actions/docker/setup-qemu-action-3.7.0
Bump docker/setup-qemu-action from 3.6.0 to 3.7.0
2025-11-17 10:49:20 -05:00
Owen Schwartz
fce588057e Merge pull request #1870 from fosrl/dependabot/npm_and_yarn/js-yaml-4.1.1
Bump js-yaml from 4.1.0 to 4.1.1
2025-11-17 10:48:32 -05:00
Owen
33331fd3c8 Merge branch 'Lokowitz-fix-zod-new' into dev 2025-11-17 10:46:40 -05:00
Owen
1261ad3a00 Standardize remote subnets build 2025-11-17 10:22:22 -05:00
Owen
7dcf4d5192 Remove remote subnet 2025-11-17 10:22:22 -05:00
Lokowitz
dc87df5d38 remove temp test 2025-11-17 14:01:11 +00:00
Lokowitz
5d2f65daa9 fix for zod 2025-11-17 13:23:30 +00:00
Lokowitz
58cf471bc4 fix z.coerce.number 2025-11-16 14:29:19 +00:00
Lokowitz
7db99a7dd5 used zod codemod 2025-11-16 14:18:17 +00:00
Lokowitz
000904eb31 upgrade zod 2025-11-16 14:09:22 +00:00
dependabot[bot]
6d1713b6b9 Bump js-yaml from 4.1.0 to 4.1.1
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-16 06:21:01 +00:00
Owen
de8262d7b9 Batch deletes 2025-11-15 11:51:52 -05:00
miloschwartz
4f026acad8 adjust icon in product update 2025-11-14 17:35:51 -05:00
miloschwartz
5b31bbce8d remove frontend env parsing 2025-11-14 12:25:32 -05:00
Milo Schwartz
e6e80f6fc7 Merge pull request #1814 from Fredkiss3/feat/update-popup
Feat: version updates & product updates popup
2025-11-14 09:13:15 -08:00
Milo Schwartz
bde4492d49 Merge branch 'dev' into feat/update-popup 2025-11-14 09:12:11 -08:00
miloschwartz
7c728c144c fix broken inputs in health check form 2025-11-14 12:00:15 -05:00
Owen
8ad7bcc0d6 Adjust rate limiting position 2025-11-14 11:33:52 -05:00
Owen
e62806d6fb Clean up old timestamps 2025-11-14 11:33:51 -05:00
miloschwartz
4e0a2e441b hide domain status info if not flags.use_pangolin_dns 2025-11-14 11:31:44 -05:00
Owen Schwartz
aabe39137b Merge pull request #1856 from LaurenceJJones/fix-remove-return-before-showing-token
fix: Remove return in installer which prevents showing token
2025-11-14 10:23:21 -05:00
miloschwartz
d9564ed6fe improve spacing and colors 2025-11-13 22:04:29 -05:00
miloschwartz
0798a0c6c2 clean up info box 2025-11-13 21:48:37 -05:00
dependabot[bot]
c9786946b7 Bump the prod-patch-updates group across 1 directory with 12 updates
Bumps the prod-patch-updates group with 12 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-avatar](https://github.com/radix-ui/primitives) | `1.1.10` | `1.1.11` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives) | `2.1.7` | `2.1.8` |
| [@radix-ui/react-progress](https://github.com/radix-ui/primitives) | `1.1.7` | `1.1.8` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives) | `1.1.7` | `1.1.8` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives) | `1.2.3` | `1.2.4` |
| [axios](https://github.com/axios/axios) | `1.13.1` | `1.13.2` |
| [eslint](https://github.com/eslint/eslint) | `9.39.0` | `9.39.1` |
| [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `16.0.1` | `16.0.2` |
| [js-yaml](https://github.com/nodeca/js-yaml) | `4.1.0` | `4.1.1` |
| [maxmind](https://github.com/runk/node-maxmind) | `5.0.0` | `5.0.1` |
| [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.11.0` | `5.11.2` |
| [resend](https://github.com/resend/resend-node) | `6.4.0` | `6.4.2` |



Updates `@radix-ui/react-avatar` from 1.1.10 to 1.1.11
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-label` from 2.1.7 to 2.1.8
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-progress` from 1.1.7 to 1.1.8
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-separator` from 1.1.7 to 1.1.8
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-slot` from 1.2.3 to 1.2.4
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `axios` from 1.13.1 to 1.13.2
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.1...v1.13.2)

Updates `eslint` from 9.39.0 to 9.39.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.0...v9.39.1)

Updates `eslint-config-next` from 16.0.1 to 16.0.2
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v16.0.2/packages/eslint-config-next)

Updates `js-yaml` from 4.1.0 to 4.1.1
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

Updates `maxmind` from 5.0.0 to 5.0.1
- [Release notes](https://github.com/runk/node-maxmind/releases)
- [Commits](https://github.com/runk/node-maxmind/compare/v5.0.0...v5.0.1)

Updates `posthog-node` from 5.11.0 to 5.11.2
- [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.11.2/packages/node)

Updates `resend` from 6.4.0 to 6.4.2
- [Release notes](https://github.com/resend/resend-node/releases)
- [Commits](https://github.com/resend/resend-node/compare/v6.4.0...v6.4.2)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-avatar"
  dependency-version: 1.1.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@radix-ui/react-progress"
  dependency-version: 1.1.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: axios
  dependency-version: 1.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: eslint
  dependency-version: 9.39.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: eslint-config-next
  dependency-version: 16.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: maxmind
  dependency-version: 5.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: posthog-node
  dependency-version: 5.11.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: resend
  dependency-version: 6.4.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-11-14 01:24:49 +00:00
dependabot[bot]
9344ab3546 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.36.0 to 0.37.0
- [Commits](https://github.com/golang/term/compare/v0.36.0...v0.37.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.37.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-11-14 01:23:32 +00:00
dependabot[bot]
1a4078b8a1 Bump the dev-minor-updates group across 1 directory with 3 updates
Bumps the dev-minor-updates group with 3 updates in the / directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [esbuild](https://github.com/evanw/esbuild) and [esbuild-node-externals](https://github.com/pradel/esbuild-node-externals).


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

Updates `esbuild` from 0.25.12 to 0.27.0
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.12...v0.27.0)

Updates `esbuild-node-externals` from 1.18.0 to 1.19.1
- [Release notes](https://github.com/pradel/esbuild-node-externals/releases)
- [Commits](https://github.com/pradel/esbuild-node-externals/compare/v1.18.0...esbuild-node-externals-v1.19.1)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.10.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: esbuild
  dependency-version: 0.27.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: esbuild-node-externals
  dependency-version: 1.19.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-14 01:21:19 +00:00
miloschwartz
ca66637270 remove from address in saas suppport email 2025-11-13 17:37:27 -05:00
miloschwartz
8674ca931b remove from address in saas suppport email 2025-11-13 17:34:49 -05:00
miloschwartz
08c82e072e Merge branch 'clients-user' of https://github.com/fosrl/pangolin into clients-user 2025-11-13 17:33:37 -05:00
miloschwartz
23c9827e4c remove create user client route 2025-11-13 17:32:35 -05:00
Owen Schwartz
864b587b89 Merge pull request #1858 from Pallavikumarimdb/role-in-headers
Role in headers
2025-11-13 17:16:31 -05:00
Owen Schwartz
ca89aa7ce8 Merge pull request #1847 from Pallavikumarimdb/fix/ipv6-validation
Fix: Improve IPv6 and IPV4 validation to support all variants using ipaddr.js
2025-11-13 17:10:47 -05:00
Pallavi Kumari
63a1ecfb86 role in header 2025-11-13 23:31:29 +05:30
Laurence Jones
fbce392137 Remove unnecessary return after success message
Remove redundant return statement after success message.
2025-11-13 12:52:21 +00:00
Pallavi Kumari
c004e969cb improve IPv6 validation to support all variants using ipaddr.js 2025-11-12 00:30:08 +05:30
dependabot[bot]
c6611471b1 Bump docker/setup-qemu-action from 3.6.0 to 3.7.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.6.0 to 3.7.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](29109295f8...c7c5346462)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: 3.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 01:37:59 +00:00
Owen
bdf1625976 Add headers 2025-11-09 10:46:46 -08:00
Owen
0a5dc17800 Merge branch 'dev' into feat/option-to-regenerate-keys 2025-11-09 10:43:26 -08:00
Owen
fa7aa508ea Merge branch 'dev' into pallavi/feat/make-niceId-editable 2025-11-09 10:39:30 -08:00
Owen
2973b61676 Fix merge confilct 2025-11-08 18:01:42 -08:00
Owen
2428413442 Dont create client 2025-11-08 17:57:54 -08:00
miloschwartz
5602d8ee64 sync user clients to org on add/remove user org 2025-11-08 17:52:05 -08:00
Owen
a70799c8c0 Merge branch 'dev' into clients-user 2025-11-08 16:51:45 -08:00
Owen
d38b321f85 Add missing header 2025-11-08 16:47:03 -08:00
Owen Schwartz
b0ff50a76f Merge pull request #1834 from fosrl/dev
Small Bug Fixes
2025-11-08 16:35:50 -08:00
Owen
37acdc2796 Revert transaction 2025-11-08 16:33:48 -08:00
Owen Schwartz
f3d31cb6de Merge pull request #1833 from fosrl/crowdin_dev
New Crowdin updates
2025-11-08 16:23:11 -08:00
Owen Schwartz
a336955066 New translations en-us.json (Norwegian Bokmal) 2025-11-08 16:22:42 -08:00
Owen Schwartz
a229fc1c61 New translations en-us.json (Chinese Simplified) 2025-11-08 16:22:40 -08:00
Owen Schwartz
7995fd364e New translations en-us.json (Turkish) 2025-11-08 16:22:39 -08:00
Owen Schwartz
5e0d822d45 New translations en-us.json (Russian) 2025-11-08 16:22:38 -08:00
Owen Schwartz
4fddaa8f11 New translations en-us.json (Portuguese) 2025-11-08 16:22:36 -08:00
Owen Schwartz
4a87cecf89 New translations en-us.json (Polish) 2025-11-08 16:22:35 -08:00
Owen Schwartz
ac5ee5c7ca New translations en-us.json (Dutch) 2025-11-08 16:22:34 -08:00
Owen Schwartz
8a8c357563 New translations en-us.json (Korean) 2025-11-08 16:22:32 -08:00
Owen Schwartz
263fd80c18 New translations en-us.json (Italian) 2025-11-08 16:22:31 -08:00
Owen Schwartz
7bdf05bdf5 New translations en-us.json (German) 2025-11-08 16:22:30 -08:00
Owen Schwartz
d00f12967d New translations en-us.json (Czech) 2025-11-08 16:22:28 -08:00
Owen Schwartz
d9991a18e2 New translations en-us.json (Bulgarian) 2025-11-08 16:22:27 -08:00
Owen Schwartz
a51c21cdd2 New translations en-us.json (Spanish) 2025-11-08 16:22:26 -08:00
Owen Schwartz
265cab5b64 New translations en-us.json (French) 2025-11-08 16:22:24 -08:00
Owen
da15e5e77b Remove software-properties-common
Fixes #1828
2025-11-08 16:13:42 -08:00
Owen
a717ca2675 Only uppercase the value if its a country
Fixes #1813
2025-11-08 15:42:46 -08:00
miloschwartz
693c9fbe0f make actions sticky in targets and rules input 2025-11-08 14:39:14 -08:00
Owen
564b290244 Fix #1830 2025-11-08 14:24:28 -08:00
Owen
84d78df67e Merge branch 'main' into dev 2025-11-08 14:20:40 -08:00
Owen
107053a98f Merge branch 'main' of github.com:fosrl/pangolin 2025-11-08 14:20:35 -08:00
Owen Schwartz
6422a78e6f Merge pull request #1830 from hetlelid/patch-2
Update resourceRawSettingsDescription with details
2025-11-08 14:20:21 -08:00
miloschwartz
10f8298161 reset nav logo size 2025-11-08 14:18:43 -08:00
miloschwartz
5f11630e27 minor adjustments to blueprints screens 2025-11-08 14:15:47 -08:00
Owen
a776b2ea94 Fix: qiery perferWildcardCert from db
Fixes #1816
Fixes #1829
2025-11-08 14:14:17 -08:00
miloschwartz
b83ec1b503 remove target unique check 2025-11-08 13:57:00 -08:00
Owen
83bd5957cd Dont allow editing a config managed domain
Ref #1816
2025-11-08 12:18:36 -08:00
Owen
f98b4baa73 Add remote subnets back based on resources 2025-11-08 12:17:33 -08:00
Pallavi Kumari
0af51cebbe scope niceid to the orgId 2025-11-08 19:44:23 +05:30
Pallavi Kumari
abc5f8ec68 show the identifier in the info box 2025-11-08 19:44:23 +05:30
Owen
ddc14d164e Rename nice id to Identifier in the ui 2025-11-08 19:44:23 +05:30
Pallavi Kumari
aeda85fcfb move resource niceid update to general page 2025-11-08 19:44:23 +05:30
Pallavi Kumari
66124f09c4 move site niceId details to general setting page 2025-11-08 19:44:23 +05:30
Pallavi Kumari
ac5fe1486a update url to prevent page redirect 2025-11-08 19:44:23 +05:30
Pallavi Kumari
50ac52d316 fix lint 2025-11-08 19:44:22 +05:30
Pallavi Kumari
f85d9f8b6e fix col 2025-11-08 19:44:22 +05:30
Pallavi Kumari
feb0bd58c8 make resource niceid editable 2025-11-08 19:44:22 +05:30
Pallavi Kumari
32949127d2 Make site niceId editable 2025-11-08 19:44:22 +05:30
Pallavi Kumari
84d24d9bf5 niceId inside resource info 2025-11-08 19:44:22 +05:30
Pallavi Kumari
8e1bb6a6fd add niceId inside info box 2025-11-08 19:44:22 +05:30
hetlelid
66c14c2d09 Update resourceRawSettingsDescription with details
Expanded the description for resourceRawSettings to include mapping details and a documentation link.
2025-11-08 13:24:51 +01:00
miloschwartz
cad4d97fb3 update works 2025-11-07 22:26:28 -08:00
Owen
de53cfb912 Update package lock 2025-11-07 21:57:31 -08:00
miloschwartz
55fd276773 update to node 25? 2025-11-07 21:55:09 -08:00
miloschwartz
7125b49024 add fade 2025-11-07 20:38:36 -08:00
miloschwartz
fb9ed8f592 dont auto close hide col popover on click 2025-11-07 18:22:13 -08:00
miloschwartz
020cb2d794 add friendly col names 2025-11-07 18:16:14 -08:00
miloschwartz
9b2c0d0b67 make org selector sticky top 2025-11-07 18:05:34 -08:00
miloschwartz
3993e5b705 add sitcky table cols for left and right cols 2025-11-07 18:03:44 -08:00
Owen
47bcadb329 Also include direct associations 2025-11-07 16:55:32 -08:00
Owen
00df2c876f Fix delete issue 2025-11-07 16:44:31 -08:00
Fred KISSIE
b4535f3dc4 ✏️ typo fix 2025-11-08 01:34:08 +01:00
miloschwartz
e51fca1f61 add clients to resource 2025-11-07 16:33:17 -08:00
Fred KISSIE
0e7f5b1aef 🌐 localize product update empty text 2025-11-08 00:57:07 +01:00
Fred KISSIE
579a4e1021 add flags for enabling notifications for product updates & new releases 2025-11-08 00:51:56 +01:00
Owen
c813202f92 Add DoNotCreateNewClient 2025-11-07 15:24:32 -08:00
Fred KISSIE
94e1c534ca 💄 add link to read more 2025-11-08 00:19:30 +01:00
Owen
41e21acf42 Fix error related to user id col 2025-11-07 14:59:45 -08:00
Pallavi Kumari
b6e98632b5 move re-key API routes to private api 2025-11-08 02:43:47 +05:30
Owen Schwartz
51db267a4a Merge pull request #1779 from fosrl/dependabot/npm_and_yarn/eslint-config-next-16.0.1
Bump eslint-config-next from 15.5.6 to 16.0.1
2025-11-07 12:15:19 -08:00
Pallavi Kumari
8a5f59cb9f disable re-key button for non licensed 2025-11-08 01:38:47 +05:30
dependabot[bot]
669817818a Bump eslint-config-next from 15.5.6 to 16.0.1
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 15.5.6 to 16.0.1.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v16.0.1/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 16.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-07 20:07:29 +00:00
Owen Schwartz
b84453bfbe Merge pull request #1825 from fosrl/dependabot/npm_and_yarn/dev-patch-updates-282bba5f0a
Bump the dev-patch-updates group across 1 directory with 5 updates
2025-11-07 12:06:08 -08:00
Owen Schwartz
15d561f59f Merge pull request #1824 from robtec/patch-1
Fix typo in shareSeeOnce message
2025-11-07 12:05:59 -08:00
Fred KISSIE
0745734273 ♻️ include build when getting product udpates 2025-11-07 20:05:51 +01:00
Fred KISSIE
aa3f07f1ba ♻️ make fossorial remote API only configurable on the frontend and only in DEV 2025-11-07 20:05:29 +01:00
Pallavi Kumari
2b8204fdc8 seperate credentials rekeying in modal for reuse 2025-11-07 23:30:24 +05:30
Pallavi Kumari
90e72c6aca hide credentials tab for local sites 2025-11-07 19:27:03 +05:30
Pallavi Kumari
62e2b7ca9e change alert text 2025-11-07 19:27:03 +05:30
Pallavi Kumari
f7e7993fd4 regenerate secret for wireguard 2025-11-07 19:27:03 +05:30
Pallavi Kumari
18cdf070c7 add view setting options 2025-11-07 19:27:03 +05:30
Pallavi Kumari
563a5b3e7e disable credential regenerate button for local and wireguard 2025-11-07 19:27:03 +05:30
Pallavi Kumari
3756aaecda change file naming structure to reGenerate exit node keys 2025-11-07 19:27:03 +05:30
Pallavi Kumari
58a13de0ff fix lint 2025-11-07 19:27:03 +05:30
Pallavi Kumari
d32505a833 Option to regenerate Newt keys 2025-11-07 19:27:03 +05:30
Pallavi Kumari
42091e88cb rename exit node tab to credentials 2025-11-07 19:27:03 +05:30
Pallavi Kumari
c2f607bb9a Option to regenerate olm keys inside client 2025-11-07 19:27:03 +05:30
Pallavi Kumari
3f38080b46 fix lint 2025-11-07 19:27:03 +05:30
Pallavi Kumari
9f9aa07c2d Option to regenerate remote-nodes keys 2025-11-07 19:27:03 +05:30
miloschwartz
76d54b2d0f add add/remove user/roles to siteResources/resources to integration api 2025-11-06 21:27:01 -08:00
Owen
bdb564823d Require valid user token 2025-11-06 21:19:37 -08:00
miloschwartz
b3a616c9f3 remove alerts from cleints and resources tables 2025-11-06 20:21:26 -08:00
Owen
ec1f94791a Remove siteIds and build associations from user role chnages 2025-11-06 20:19:15 -08:00
miloschwartz
bea1c65076 remove remote subnets from front end 2025-11-06 20:16:24 -08:00
miloschwartz
2274a3525b update olm and client routes 2025-11-06 20:12:54 -08:00
dependabot[bot]
749cea5a4d Bump the dev-patch-updates group across 1 directory with 5 updates
Bumps the dev-patch-updates group with 4 updates in the / directory: [@dotenvx/dotenvx](https://github.com/dotenvx/dotenvx), [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss), [esbuild](https://github.com/evanw/esbuild) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `@dotenvx/dotenvx` from 1.51.0 to 1.51.1
- [Release notes](https://github.com/dotenvx/dotenvx/releases)
- [Changelog](https://github.com/dotenvx/dotenvx/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dotenvx/dotenvx/compare/v1.51.0...v1.51.1)

Updates `@tailwindcss/postcss` from 4.1.16 to 4.1.17
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.17/packages/@tailwindcss-postcss)

Updates `esbuild` from 0.25.11 to 0.25.12
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.11...v0.25.12)

Updates `tailwindcss` from 4.1.16 to 4.1.17
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.17/packages/tailwindcss)

Updates `typescript-eslint` from 8.46.2 to 8.46.3
- [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.3/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@dotenvx/dotenvx"
  dependency-version: 1.51.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.1.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: esbuild
  dependency-version: 0.25.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: tailwindcss
  dependency-version: 4.1.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: typescript-eslint
  dependency-version: 8.46.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-07 01:23:55 +00:00
miloschwartz
999fb2fff1 Merge branch 'dev' into clients-user 2025-11-06 16:55:16 -08:00
miloschwartz
2a7529c39e don't delete user 2025-11-06 16:48:53 -08:00
Fred KISSIE
f27ae210ed Merge branch 'dev' into feat/update-popup 2025-11-07 01:30:18 +01:00
Fred KISSIE
ea744f8d28 💄 show update type 2025-11-07 01:14:05 +01:00
Fred KISSIE
0b70cbb1a3 💄 show update type in badge 2025-11-07 01:10:20 +01:00
miloschwartz
fce887436d fix bug causing auto provision to override manually created users 2025-11-06 15:46:54 -08:00
Fred KISSIE
f928708156 💄 animate exit and more 2025-11-07 00:27:57 +01:00
miloschwartz
fae899a8f1 remove dialog border 2025-11-06 15:17:19 -08:00
Rob
3489107a49 Fix typo in shareSeeOnce message 2025-11-06 23:09:52 +00:00
Fred KISSIE
45fb0a4156 💄 button for mark as read 2025-11-06 23:26:13 +01:00
Fred KISSIE
a62299c387 🎨 prettier format 2025-11-06 23:25:53 +01:00
Fred KISSIE
18757d7eb3 💄 show product updates list 2025-11-06 22:42:49 +01:00
Owen Schwartz
296b220bf3 Merge pull request #1819 from Pallavikumarimdb/fix/resourceTable-typeError
Fix/Revert column from Resource table to fix type error and match overall styling
2025-11-06 12:03:15 -08:00
Pallavi Kumari
0a9f37c44d revert column from resource table 2025-11-06 22:57:03 +05:30
miloschwartz
776c33d79d persist column filters 2025-11-05 17:34:50 -08:00
miloschwartz
9fd6af3a31 view devices for profile 2025-11-05 17:27:16 -08:00
miloschwartz
4ade878320 split clients table 2025-11-05 16:43:27 -08:00
miloschwartz
9e2477587c if one logs dont show nested 2025-11-05 16:13:51 -08:00
miloschwartz
c7787352c8 add sidebar groups 2025-11-05 16:09:12 -08:00
miloschwartz
85892c30b2 add site resource modes and alias 2025-11-05 15:24:07 -08:00
Fred KISSIE
7a2dd31019 🚧 use popup 2025-11-06 00:16:07 +01:00
Fred KISSIE
096ca379ce ♻️ refactor 2025-11-06 00:06:05 +01:00
Fred KISSIE
41601010f4 💡 comment 2025-11-05 23:58:56 +01:00
Fred KISSIE
64b87e203a 💄 animate product updates & new version 2025-11-05 23:57:43 +01:00
Fred KISSIE
c64b102aaa ♻️ refactor 2025-11-05 23:29:48 +01:00
Fred KISSIE
f371c7df81 add headless/ui for better enter/exit animations 2025-11-05 23:29:36 +01:00
Fred KISSIE
030f90db2e ♻️ validate env variables only in DEV 2025-11-05 21:41:29 +01:00
miloschwartz
e51b6b545e add users and roles to site resources 2025-11-05 12:24:50 -08:00
Owen Schwartz
ef5d72663f Merge pull request #1328 from Pallavikumarimdb/enhancement-#906/dashboard-enhancements
Enhancement #906/Resources Dashboard: Targets Column, Customizable Columns & Status Indicators
2025-11-05 11:41:43 -08:00
Owen
6ddfc9b8fe Revert columns 2025-11-05 11:41:07 -08:00
Owen
301654b63e Fix styling 2025-11-05 11:38:14 -08:00
miloschwartz
c73f8c88f7 hide sites inputs on clients 2025-11-05 10:37:52 -08:00
miloschwartz
2274404324 update tables 2025-11-05 10:29:29 -08:00
Fred KISSIE
6d349693a7 🚧 wip 2025-11-05 08:45:56 +01:00
Fred KISSIE
b9ce316574 🚧 wip 2025-11-05 08:38:23 +01:00
Fred KISSIE
a247ef7564 ♻️ import type 2025-11-05 07:33:25 +01:00
Fred KISSIE
18566c09dc add tanstack query 2025-11-05 07:32:28 +01:00
Fred KISSIE
1090dca634 Merge branch 'main' into feat/update-popup 2025-11-05 07:30:12 +01:00
Fred KISSIE
44f419d4f7 💄 animate popup 2025-11-05 07:30:01 +01:00
Fred KISSIE
162c6d567c revert package.json changes 2025-11-05 07:26:41 +01:00
Fred KISSIE
2f1abfbef8 🚧 New version popup 2025-11-05 06:55:08 +01:00
Fred KISSIE
a26a441d56 ♻️ validate env and add remote fossorial API as an env variable 2025-11-05 06:54:56 +01:00
miloschwartz
f628a76223 add them back 2025-11-04 16:56:56 -08:00
miloschwartz
8088e30e06 remove userClients and roleClients 2025-11-04 16:53:00 -08:00
miloschwartz
801cdec7f3 add deviceWebAuthCodes table to pg schema 2025-11-04 16:51:31 -08:00
Owen
3fd3f9871d Remove user check 2025-11-04 11:56:00 -08:00
miloschwartz
959a562e7c fix more shadows 2025-11-04 11:09:08 -08:00
Owen Schwartz
3b12a77cf0 Merge pull request #1809 from clemone210/patch-2
Update German translations for client and blueprint terms
2025-11-04 10:34:26 -08:00
Fred KISSIE
03e0e8d9c2 🚧 wip 2025-11-04 13:57:55 +01:00
Timo
7cd31313d8 Update German translations for client and blueprint terms
"Kunden" is generally used for "Customers", so in this case I suggest to stick with Client, as this is a widely used term in german tech sector. The same for "Bauplan" or "Blaupause". "Bauplan" is a "Construction plan" for building houses. "Blaupause" is pretty much the right translation for blueprints, but I would stick with Blueprint here as well.
2025-11-04 07:40:33 +01:00
miloschwartz
52a311bf36 fix colors and footer 2025-11-03 21:44:34 -08:00
Milo Schwartz
9822deb4bf Update README.md 2025-11-03 22:56:57 -05:00
Owen
83e0282212 Merge branch 'dev' into clients-user 2025-11-03 17:39:10 -08:00
Owen
8942cb7aa7 Update const 2025-11-03 17:38:50 -08:00
Owen
f0f219f293 Merge branch 'main' into dev 2025-11-03 17:38:43 -08:00
Owen
dc75d72522 Merge branch 'dev' into clients-user 2025-11-03 17:38:26 -08:00
Owen
6da81b3817 Fix bad request in non-enterprise 2025-11-03 17:33:50 -08:00
miloschwartz
847479b639 Merge branch 'cli-web-auth' into clients-user 2025-11-03 17:14:12 -08:00
miloschwartz
0790f37f5e hash device codes 2025-11-03 17:03:46 -08:00
Owen
9dd472c59b Creating olm working 2025-11-03 16:54:06 -08:00
miloschwartz
5746d69f98 reduce header padding 2025-11-03 16:22:40 -08:00
Owen
8356c5933f Small fixes around handling olm users 2025-11-03 16:22:13 -08:00
Owen
2c488baa80 Add name and lock client to specific olm 2025-11-03 16:16:19 -08:00
Owen
d30743a428 Update schmea; create client when registering 2025-11-03 15:42:22 -08:00
miloschwartz
009d84a3c6 remove shadows and outline ring 2025-11-03 11:22:00 -08:00
miloschwartz
e888b76747 complete web device auth flow 2025-11-03 11:10:17 -08:00
Owen
6174599754 Allow >30 days on oss 2025-11-03 09:54:41 -08:00
Owen Schwartz
8ba04aeb74 Merge pull request #1802 from fosrl/dependabot/npm_and_yarn/prod-minor-updates-700e856888
Bump the prod-minor-updates group across 1 directory with 9 updates
2025-11-03 09:49:01 -08:00
Owen
43590896e9 Add fosrl 2025-11-02 18:56:46 -08:00
Owen Schwartz
3547c4832b Revert "Refactor CI/CD workflow for improved release process" 2025-11-02 18:56:46 -08:00
Marc Schäfer
1cd098252e Refactor CI/CD workflow for improved release process
Updated CI/CD workflow to include new permissions, job definitions, and steps for version validation, tagging, and artifact management.
2025-11-02 18:56:46 -08:00
Owen
4adbc31dae Fix blueprints not applying
Fixes #1795
2025-11-02 18:56:46 -08:00
Owen
99031feb35 Fix camel case in health checks 2025-11-02 18:56:46 -08:00
Owen
d363b06d0e Fix rewritePath
Closes #1528
2025-11-02 18:56:46 -08:00
Owen
2af100cc86 Warning -> debug 2025-11-02 18:56:46 -08:00
dependabot[bot]
3e90211108 Bump the prod-minor-updates group across 1 directory with 9 updates
Bumps the prod-minor-updates group with 9 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.908.0` | `3.922.0` |
| [axios](https://github.com/axios/axios) | `1.12.2` | `1.13.1` |
| [eslint](https://github.com/eslint/eslint) | `9.37.0` | `9.39.0` |
| [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) | `8.1.0` | `8.2.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.545.0` | `0.552.0` |
| [next-intl](https://github.com/amannn/next-intl) | `4.3.12` | `4.4.0` |
| [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.10.4` | `5.11.0` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.65.0` | `7.66.0` |
| [resend](https://github.com/resend/resend-node) | `6.1.3` | `6.4.0` |



Updates `@aws-sdk/client-s3` from 3.908.0 to 3.922.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.922.0/clients/client-s3)

Updates `axios` from 1.12.2 to 1.13.1
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.12.2...v1.13.1)

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

Updates `express-rate-limit` from 8.1.0 to 8.2.1
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v8.1.0...v8.2.1)

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

Updates `next-intl` from 4.3.12 to 4.4.0
- [Release notes](https://github.com/amannn/next-intl/releases)
- [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-intl/compare/v4.3.12...v4.4.0)

Updates `posthog-node` from 5.10.4 to 5.11.0
- [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.11.0/packages/node)

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

Updates `resend` from 6.1.3 to 6.4.0
- [Release notes](https://github.com/resend/resend-node/releases)
- [Commits](https://github.com/resend/resend-node/compare/v6.1.3...v6.4.0)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.922.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: axios
  dependency-version: 1.13.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: eslint
  dependency-version: 9.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: express-rate-limit
  dependency-version: 8.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: lucide-react
  dependency-version: 0.552.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: next-intl
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: posthog-node
  dependency-version: 5.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react-hook-form
  dependency-version: 7.66.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: resend
  dependency-version: 6.4.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-11-03 01:34:13 +00:00
Owen
6dd161fe17 Add fosrl 2025-11-02 15:35:02 -08:00
Owen Schwartz
558bd040c6 Merge pull request #1801 from fosrl/revert-1792-main
Revert "Refactor CI/CD workflow for improved release process"
2025-11-02 15:22:12 -08:00
Owen Schwartz
f2c48975f6 Revert "Refactor CI/CD workflow for improved release process" 2025-11-02 15:22:03 -08:00
Owen Schwartz
fc43a56bb3 Merge pull request #1792 from marcschaeferger/main
Refactor CI/CD workflow for improved release process
2025-11-02 15:00:09 -08:00
Owen
ca7f557a3c Fix blueprints not applying
Fixes #1795
2025-11-02 14:56:19 -08:00
Owen
7477713eef Fix camel case in health checks 2025-11-02 14:17:38 -08:00
Owen
c16e762fa4 Fix rewritePath
Closes #1528
2025-11-02 14:05:41 -08:00
Owen Schwartz
41592133a6 Merge pull request #1788 from Pallavikumarimdb/fix/deleting-and-adding-back-a-target
Add transaction while deleting targets
2025-11-02 13:51:08 -08:00
Pallavi Kumari
54f7525f1b add status column in resource table 2025-11-02 13:55:17 +05:30
Pallavi Kumari
ad6bb3da9f fix type error 2025-11-02 13:55:17 +05:30
Pallavi Kumari
49bc2dc5da fix duplicate 2025-11-02 13:55:16 +05:30
Pallavi
cdf77087cd get niceid 2025-11-02 13:55:16 +05:30
Pallavi
8e5dde887c list targes in frontend 2025-11-02 13:55:16 +05:30
Pallavi
f21188000e remove status check and add column filtering on all of the tables 2025-11-02 13:55:16 +05:30
Pallavi
1b3eb32bf4 Show targets and status icons in the dashboard 2025-11-02 13:55:16 +05:30
Marc Schäfer
eec3f183e6 Refactor CI/CD workflow for improved release process
Updated CI/CD workflow to include new permissions, job definitions, and steps for version validation, tagging, and artifact management.
2025-11-02 00:44:03 +01:00
Owen
31b66cd911 Warning -> debug 2025-11-01 10:46:09 -07:00
Pallavi Kumari
ad425e8d9e add transaction while deleting targets 2025-11-01 11:58:09 +05:30
miloschwartz
da0196a308 no reset password for external users 2025-10-30 22:24:07 -07:00
miloschwartz
e585972b7b remove useSubscriptionStatusContext from HorizontalTabs 2025-10-30 21:31:48 -07:00
miloschwartz
cc62cd4add remove sqlite driver logger 2025-10-30 21:23:05 -07:00
Owen
25225a452c Return instead of throwing error 2025-10-30 21:18:26 -07:00
Owen
678644c7fb Fix empty blueprint 2025-10-30 21:09:20 -07:00
Owen
32f20ed984 Bugfixes for remote nodes 2025-10-30 21:01:45 -07:00
Owen
4eb5bf08d5 UI fixes 2025-10-30 17:44:22 -07:00
Owen
35c93f38e0 Fix small ui issues 2025-10-30 17:32:03 -07:00
Owen
f60c2f4fb9 Make refresh work 2025-10-30 17:25:49 -07:00
Owen
b2cf152b9e Add copy to clip 2025-10-30 16:17:20 -07:00
Owen
444928dffd Add wildcard 2025-10-30 15:27:24 -07:00
Owen
4d7e2d5840 Minor fixes to rc 2025-10-30 11:42:31 -07:00
509 changed files with 33477 additions and 12194 deletions

View File

@@ -31,7 +31,7 @@ jobs:
timeout-minutes: 120
env:
# Target images
DOCKERHUB_IMAGE: docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ github.event.repository.name }}
DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
steps:
@@ -39,7 +39,7 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1

3
.gitignore vendored
View File

@@ -49,4 +49,5 @@ postgres/
dynamic/
*.mmdb
scratch/
tsconfig.json
tsconfig.json
hydrateSaas.ts

2
.nvmrc
View File

@@ -1 +1 @@
22
25

View File

@@ -1,10 +1,12 @@
FROM node:22-alpine AS builder
FROM node:25-alpine AS builder
WORKDIR /app
ARG BUILD=oss
ARG DATABASE=sqlite
RUN apk add --no-cache curl tzdata python3 make g++
# COPY package.json package-lock.json ./
COPY package*.json ./
RUN npm ci
@@ -12,8 +14,9 @@ RUN npm ci
COPY . .
RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts
RUN echo "export const driver: \"pg\" | \"sqlite\" = \"$DATABASE\";" >> server/db/index.ts
RUN echo "export const build = \"$BUILD\" as any;" > server/build.ts
RUN echo "export const build = \"$BUILD\" as \"saas\" | \"enterprise\" | \"oss\";" > server/build.ts
# Copy the appropriate TypeScript configuration based on build type
RUN if [ "$BUILD" = "oss" ]; then cp tsconfig.oss.json tsconfig.json; \
@@ -30,9 +33,9 @@ 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; \
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; \
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
@@ -40,12 +43,13 @@ RUN test -f dist/server.mjs
RUN npm run build:cli
FROM node:22-alpine AS runner
FROM node:25-alpine AS runner
WORKDIR /app
# Curl used for the health checks
RUN apk add --no-cache curl tzdata
# Python and build tools needed for better-sqlite3 native compilation
RUN apk add --no-cache curl tzdata python3 make g++
# COPY package.json package-lock.json ./
COPY package*.json ./

View File

@@ -89,3 +89,7 @@ Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License
## Contributions
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
---
WireGuard® is a registered trademark of Jason A. Donenfeld.

View File

@@ -31,6 +31,7 @@ proxy-resources:
# - owen@pangolin.net
# whitelist-users:
# - owen@pangolin.net
# auto-login-idp: 1
headers:
- name: X-Example-Header
value: example-value

View File

@@ -5,14 +5,14 @@ meta {
}
post {
url: http://localhost:4000/api/v1/auth/login
url: http://localhost:3000/api/v1/auth/login
body: json
auth: none
}
body:json {
{
"email": "owen@pangolin.net",
"email": "admin@fosrl.io",
"password": "Password123!"
}
}

15
bruno/Olm/createOlm.bru Normal file
View File

@@ -0,0 +1,15 @@
meta {
name: createOlm
type: http
seq: 1
}
put {
url: http://localhost:3000/api/v1/olm
body: none
auth: inherit
}
settings {
encodeUrl: true
}

8
bruno/Olm/folder.bru Normal file
View File

@@ -0,0 +1,8 @@
meta {
name: Olm
seq: 15
}
auth {
mode: inherit
}

View File

@@ -1,6 +1,6 @@
{
"version": "1",
"name": "Pangolin Saas",
"name": "Pangolin",
"type": "collection",
"ignore": [
"node_modules",

View File

@@ -25,4 +25,3 @@ flags:
disable_user_create_org: true
allow_raw_resources: true
enable_integration_api: true
enable_clients: true

View File

@@ -35,7 +35,7 @@ services:
- 80:80 # Port for traefik because of the network_mode
traefik:
image: traefik:v3.5
image: traefik:v3.6
container_name: traefik
restart: unless-stopped
network_mode: service:gerbil # Ports appear on the gerbil service
@@ -52,4 +52,4 @@ networks:
default:
driver: bridge
name: pangolin
enable_ipv6: true
enable_ipv6: true

View File

@@ -35,7 +35,7 @@ services:
- 80:80
{{end}}
traefik:
image: docker.io/traefik:v3.5
image: docker.io/traefik:v3.6
container_name: traefik
restart: unless-stopped
{{if .InstallGerbil}}
@@ -59,4 +59,4 @@ networks:
default:
driver: bridge
name: pangolin
{{if .EnableIPv6}} enable_ipv6: true{{end}}
{{if .EnableIPv6}} enable_ipv6: true{{end}}

View File

@@ -73,7 +73,7 @@ func installDocker() error {
case strings.Contains(osRelease, "ID=ubuntu"):
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
apt-get update &&
apt-get install -y apt-transport-https ca-certificates curl software-properties-common &&
apt-get install -y apt-transport-https ca-certificates curl &&
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
apt-get update &&
@@ -82,7 +82,7 @@ func installDocker() error {
case strings.Contains(osRelease, "ID=debian"):
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
apt-get update &&
apt-get install -y apt-transport-https ca-certificates curl software-properties-common &&
apt-get install -y apt-transport-https ca-certificates curl &&
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
apt-get update &&

View File

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

View File

@@ -1,7 +1,7 @@
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -238,7 +238,6 @@ func main() {
}
fmt.Println("CrowdSec installed successfully!")
return
}
}
}

View File

@@ -131,7 +131,7 @@
"expireIn": "Изтече",
"neverExpire": "Никога не изтича",
"shareExpireDescription": "Времето на изтичане е колко дълго връзката ще бъде използваема и ще предоставя достъп до ресурса. След това време, връзката няма да работи и потребителите, които са я използвали, ще загубят достъп до ресурса.",
"shareSeeOnce": "Ще можете да видите тази връзка само веднъж. Уверете се да я копирате.",
"shareSeeOnce": "Ще можете да видите този линк само веднъж. Уверете се, че го копирате.",
"shareAccessHint": "Всеки с тази връзка може да има достъп до ресурса. Споделяйте я с внимание.",
"shareTokenUsage": "Вижте използването на токена за достъп",
"createLink": "Създаване на връзка",
@@ -179,7 +179,7 @@
"baseDomain": "Базов домейн",
"subdomnainDescription": "Субдомейнът, в който ще бъде достъпен вашият ресурс.",
"resourceRawSettings": "TCP/UDP настройки",
"resourceRawSettingsDescription": "Конфигурирайте как вашият ресурс ще бъде достъпен през TCP/UDP",
"resourceRawSettingsDescription": "Настройте как ресурсът ви ще бъде достъпен през TCP/UDP. Свързвате ресурса към порт на хост сървъра Pangolin, за да го достъпвате от server-public-ip:mapped-port.",
"protocol": "Протокол",
"protocolSelect": "Изберете протокол",
"resourcePortNumber": "Номер на порт",
@@ -1165,13 +1165,13 @@
"sidebarDomains": "Домейни",
"sidebarBluePrints": "Чертежи",
"blueprints": "Чертежи",
"blueprintsDescription": "Чертежите са декларативни YAML конфигурации, които определят вашите ресурси и техните настройки",
"blueprintsDescription": "Прилагайте декларативни конфигурации и преглеждайте предишни изпълнения",
"blueprintAdd": "Добави Чертеж",
"blueprintGoBack": "Виж всички Чертежи",
"blueprintCreate": "Създай Чертеж",
"blueprintCreateDescription2": "Следвайте стъпките по-долу, за да създадете и приложите нов чертеж",
"blueprintDetails": "Детайли за Чертежа",
"blueprintDetailsDescription": "Вижте детайлите за изпълнението на чертежа",
"blueprintDetails": "Детайли на чертежа",
"blueprintDetailsDescription": "Вижте резултата от приложените чертежи и всички възникнали грешки",
"blueprintInfo": "Информация за Чертежа",
"message": "Съобщение",
"blueprintContentsDescription": "Дефинирайте YAML съдържанието, описващо вашата инфраструктура",
@@ -1181,7 +1181,7 @@
"appliedAt": "Приложено във",
"source": "Източник",
"contents": "Съдържание",
"parsedContents": "Анализирано съдържание",
"parsedContents": "Парсирано съдържание (само за четене)",
"enableDockerSocket": "Активиране на Docker Чернова",
"enableDockerSocketDescription": "Активиране на Docker Socket маркировка за изтегляне на етикети на чернова. Пътят на гнездото трябва да бъде предоставен на Newt.",
"enableDockerSocketLink": "Научете повече",
@@ -1279,6 +1279,15 @@
"settingsErrorUpdateDescription": "Възникна грешка при обновяване на настройките",
"sidebarCollapse": "Свиване",
"sidebarExpand": "Разширяване",
"productUpdateMoreInfo": "{noOfUpdates} още актуализации",
"productUpdateInfo": "{noOfUpdates} актуализации",
"productUpdateWhatsNew": "Какво ново",
"productUpdateTitle": "Актуализации на продукта",
"productUpdateEmpty": "Няма актуализации",
"dismissAll": "Отхвърляне на всички",
"pangolinUpdateAvailable": "Налична е нова версия",
"pangolinUpdateAvailableInfo": "Версия {version} е готова за инсталиране",
"pangolinUpdateAvailableReleaseNotes": "Преглед на бележките за издание",
"newtUpdateAvailable": "Ново обновление",
"newtUpdateAvailableInfo": "Нова версия на Newt е налична. Моля, обновете до последната версия за най-добро изживяване.",
"domainPickerEnterDomain": "Домейн",
@@ -1525,6 +1534,12 @@
"resourcesTableTheseResourcesForUseWith": "Тези ресурси са за използване с",
"resourcesTableClients": "Клиенти",
"resourcesTableAndOnlyAccessibleInternally": са достъпни само вътрешно при свързване с клиент.",
"resourcesTableNoTargets": "Без цели",
"resourcesTableHealthy": "Здрав",
"resourcesTableDegraded": "Влошен",
"resourcesTableOffline": "Извън линия",
"resourcesTableUnknown": "Неизвестно",
"resourcesTableNotMonitored": "Не е наблюдавано",
"editInternalResourceDialogEditClientResource": "Редактиране на клиентски ресурс",
"editInternalResourceDialogUpdateResourceProperties": "Актуализирайте свойствата на ресурса и конфигурацията на целите за {resourceName}.",
"editInternalResourceDialogResourceProperties": "Свойствата на ресурса",
@@ -2080,5 +2095,46 @@
"supportSending": "Изпращане...",
"supportSend": "Изпрати",
"supportMessageSent": "Съобщението е изпратено!",
"supportWillContact": "Ще се свържем с вас скоро!"
"supportWillContact": "Ще се свържем с вас скоро!",
"selectLogRetention": "Изберете съхранение на логовете",
"showColumns": "Покажи колони",
"hideColumns": "Скрий колони",
"columnVisibility": "Видимост на колоните",
"toggleColumn": "Превключване на колоната {columnName}",
"allColumns": "Всички колони",
"defaultColumns": "По подразбиране колони",
"customizeView": "Персонализиране на изгледа",
"viewOptions": "Опции за изгледа",
"selectAll": "Избери всички",
"selectNone": "Избери нищо",
"selectedResources": "Избрани ресурси",
"enableSelected": "Разреши избраните",
"disableSelected": "Забрани избраните",
"checkSelectedStatus": "Проверете състоянието на избраните",
"credentials": "Удостоверения",
"savecredentials": "Запазване на удостоверения",
"regeneratecredentials": "Прегенериране",
"regenerateCredentials": "Прегенериране и запазване на удостоверенията ви",
"generatedcredentials": "Прегенерирани удостоверения",
"copyandsavethesecredentials": "Копирайте и запазете тези удостоверения",
"copyandsavethesecredentialsdescription": "Тези удостоверения няма да бъдат показани отново след като напуснете тази страница. Запазете ги сигурно сега.",
"credentialsSaved": "Удостоверенията са запазени",
"credentialsSavedDescription": "Удостоверенията бяха прегенерирани и успешно запазени.",
"credentialsSaveError": "Грешка при запазването на удостоверенията",
"credentialsSaveErrorDescription": "Възникна грешка при прегенерирането и запазването на удостоверенията.",
"regenerateCredentialsWarning": "Прегенерирането на удостоверения ще анулира предишните. Уверете се, че актуализирате всички конфигурации, които използват тези удостоверения.",
"confirm": "Потвърждаване",
"regenerateCredentialsConfirmation": "Сигурни ли сте, че искате да прегенерирате удостоверенията?",
"endpoint": "Крайна точка",
"Id": "Идентификатор",
"SecretKey": "Таен ключ",
"featureDisabledTooltip": "Тази функция е налична само в корпоративния пакет и изисква лиценз за използване.",
"niceId": "Красив ID",
"niceIdUpdated": "Красив ID е обновен",
"niceIdUpdatedSuccessfully": "Красив ID е успешно обновен",
"niceIdUpdateError": "Грешка при обновяването на Красив ID",
"niceIdUpdateErrorDescription": "Възникна грешка при обновяването на Красив ID.",
"niceIdCannotBeEmpty": "Красив ID не може да бъде празен",
"enterIdentifier": "Въведете идентификатор",
"identifier": "Идентификатор"
}

View File

@@ -131,7 +131,7 @@
"expireIn": "Platnost vyprší za",
"neverExpire": "Nikdy nevyprší",
"shareExpireDescription": "Doba platnosti určuje, jak dlouho bude odkaz použitelný a bude poskytovat přístup ke zdroji. Po této době odkaz již nebude fungovat a uživatelé kteří tento odkaz používali ztratí přístup ke zdroji.",
"shareSeeOnce": "Tento odkaz uvidíte pouze jednou. Ujistěte se, že jste jej zkopírovali.",
"shareSeeOnce": "Tento odkaz uvidíte pouze jednou. Nezapomeňte jej zkopírovat.",
"shareAccessHint": "Kdokoli s tímto odkazem může přistupovat ke zdroji. Sdílejte jej s rozvahou.",
"shareTokenUsage": "Zobrazit využití přístupového tokenu",
"createLink": "Vytvořit odkaz",
@@ -179,7 +179,7 @@
"baseDomain": "Základní doména",
"subdomnainDescription": "Subdoména, kde bude váš zdroj přístupný.",
"resourceRawSettings": "Nastavení TCP/UDP",
"resourceRawSettingsDescription": "Nakonfigurujte, jak bude váš dokument přístupný přes TCP/UDP",
"resourceRawSettingsDescription": "Nakonfigurujte, jak bude váš dokument přístupný přes TCP/UDP. Mapováte zdroj na port na serveru Pangolin, takže můžete přistupovat ke zdroji ze serveru-veřejné ip:mapped-port.",
"protocol": "Protokol",
"protocolSelect": "Vybrat protokol",
"resourcePortNumber": "Číslo portu",
@@ -1165,13 +1165,13 @@
"sidebarDomains": "Domény",
"sidebarBluePrints": "Plány",
"blueprints": "Plány",
"blueprintsDescription": "Plány jsou deklarativní YAML konfigurace, které definují vaše zdroje a jejich nastavení",
"blueprintsDescription": "Použít deklarativní konfigurace a zobrazit předchozí běhy",
"blueprintAdd": "Přidat plán",
"blueprintGoBack": "Zobrazit všechny plány",
"blueprintCreate": "Vytvořit plán",
"blueprintCreateDescription2": "Postupujte podle níže uvedených kroků pro vytvoření a použití nového plánu",
"blueprintDetails": "Podrobnosti plánu",
"blueprintDetailsDescription": "Podívejte se na detaily běhu plánu",
"blueprintDetailsDescription": "Podívejte se na výsledek použitého plánu a případné chyby, které se vyskytly",
"blueprintInfo": "Informace o plánu",
"message": "Zpráva",
"blueprintContentsDescription": "Definujte obsah YAML popisující vaši infrastrukturu",
@@ -1181,7 +1181,7 @@
"appliedAt": "Použito v",
"source": "Zdroj",
"contents": "Obsah",
"parsedContents": "Parsovaný obsah",
"parsedContents": "Parsed content (Pouze pro čtení)",
"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.",
"enableDockerSocketLink": "Zjistit více",
@@ -1279,6 +1279,15 @@
"settingsErrorUpdateDescription": "Došlo k chybě při aktualizaci nastavení",
"sidebarCollapse": "Sbalit",
"sidebarExpand": "Rozbalit",
"productUpdateMoreInfo": "{noOfUpdates} další aktualizace",
"productUpdateInfo": "Aktualizace {noOfUpdates}",
"productUpdateWhatsNew": "Co je nového",
"productUpdateTitle": "Aktualizace produktu",
"productUpdateEmpty": "Žádné aktualizace",
"dismissAll": "Odmítnout vše",
"pangolinUpdateAvailable": "K dispozici je nová verze",
"pangolinUpdateAvailableInfo": "Verze {version} je připravena k instalaci",
"pangolinUpdateAvailableReleaseNotes": "Zobrazit poznámky k vydání",
"newtUpdateAvailable": "Dostupná aktualizace",
"newtUpdateAvailableInfo": "Je k dispozici nová verze Newt. Pro nejlepší zážitek prosím aktualizujte na nejnovější verzi.",
"domainPickerEnterDomain": "Doména",
@@ -1525,6 +1534,12 @@
"resourcesTableTheseResourcesForUseWith": "Tyto zdroje jsou určeny pro použití s",
"resourcesTableClients": "Klienti",
"resourcesTableAndOnlyAccessibleInternally": "a jsou interně přístupné pouze v případě, že jsou propojeni s klientem.",
"resourcesTableNoTargets": "Žádné cíle",
"resourcesTableHealthy": "Zdravé",
"resourcesTableDegraded": "Rozklad",
"resourcesTableOffline": "Offline",
"resourcesTableUnknown": "Neznámý",
"resourcesTableNotMonitored": "Není sledováno",
"editInternalResourceDialogEditClientResource": "Upravit klientský dokument",
"editInternalResourceDialogUpdateResourceProperties": "Aktualizujte vlastnosti zdroje a cílovou konfiguraci pro {resourceName}.",
"editInternalResourceDialogResourceProperties": "Vlastnosti zdroje",
@@ -2080,5 +2095,46 @@
"supportSending": "Odesílání...",
"supportSend": "Poslat",
"supportMessageSent": "Zpráva odeslána!",
"supportWillContact": "Brzy budeme v kontaktu!"
"supportWillContact": "Brzy budeme v kontaktu!",
"selectLogRetention": "Vyberte záznam",
"showColumns": "Zobrazit sloupce",
"hideColumns": "Skrýt sloupce",
"columnVisibility": "Viditelnost sloupců",
"toggleColumn": "Přepnout sloupec {columnName}",
"allColumns": "Všechny sloupce",
"defaultColumns": "Výchozí sloupce",
"customizeView": "Přizpůsobit zobrazení",
"viewOptions": "Možnosti zobrazení",
"selectAll": "Vybrat vše",
"selectNone": "Nevybrat žádný",
"selectedResources": "Vybrané zdroje",
"enableSelected": "Povolit vybrané",
"disableSelected": "Zakázat vybrané",
"checkSelectedStatus": "Zkontrolovat stav vybraného",
"credentials": "Pověření",
"savecredentials": "Uložit přihlašovací údaje",
"regeneratecredentials": "Znovu klíče",
"regenerateCredentials": "Regenerovat a uložit vaše přihlašovací údaje",
"generatedcredentials": "Vygenerovaná pověření",
"copyandsavethesecredentials": "Zkopírovat a ukládat tato pověření",
"copyandsavethesecredentialsdescription": "Tyto přihlašovací údaje se znovu nezobrazí po opuštění této stránky. Uložte je bezpečně.",
"credentialsSaved": "Pověření uloženo",
"credentialsSavedDescription": "Pověření byla úspěšně obnovena a uložena.",
"credentialsSaveError": "Chyba při ukládání pověření",
"credentialsSaveErrorDescription": "Došlo k chybě při obnovování a ukládání přihlašovacích údajů.",
"regenerateCredentialsWarning": "Obnovení přihlašovacích údajů zneplatní ty předchozí. Ujistěte se, že aktualizujete všechny konfigurace, které tyto přihlašovací údaje používají.",
"confirm": "Potvrdit",
"regenerateCredentialsConfirmation": "Jste si jisti, že chcete obnovit přihlašovací údaje?",
"endpoint": "Endpoint",
"Id": "Id",
"SecretKey": "Tajný klíč",
"featureDisabledTooltip": "Tato funkce je dostupná pouze v podnikovém plánu a vyžaduje její používání.",
"niceId": "Pěkné ID",
"niceIdUpdated": "Nice ID aktualizováno",
"niceIdUpdatedSuccessfully": "Nice ID úspěšně aktualizováno",
"niceIdUpdateError": "Chyba při aktualizaci Nice ID",
"niceIdUpdateErrorDescription": "Došlo k chybě při aktualizaci identifikátoru Nice.",
"niceIdCannotBeEmpty": "Nice ID nemůže být prázdné",
"enterIdentifier": "Zadejte identifikátor",
"identifier": "Identifier"
}

View File

@@ -131,7 +131,7 @@
"expireIn": "Verfällt in",
"neverExpire": "Nie ablaufen",
"shareExpireDescription": "Ablaufzeit ist, wie lange der Link verwendet werden kann und bietet Zugriff auf die Ressource. Nach dieser Zeit wird der Link nicht mehr funktionieren und Benutzer, die diesen Link benutzt haben, verlieren den Zugriff auf die Ressource.",
"shareSeeOnce": "Sie können diesen Link nur ein einziges Mal sehen. Bitte kopieren Sie ihn.",
"shareSeeOnce": "Sie können diesen Link nur einmal sehen. Bitte kopieren Sie ihn.",
"shareAccessHint": "Jeder mit diesem Link kann auf die Ressource zugreifen. Teilen Sie sie mit Vorsicht.",
"shareTokenUsage": "Zugriffstoken-Nutzung anzeigen",
"createLink": "Link erstellen",
@@ -179,7 +179,7 @@
"baseDomain": "Basis-Domain",
"subdomnainDescription": "Die Subdomain, auf der Ihre Ressource erreichbar sein soll.",
"resourceRawSettings": "TCP/UDP Einstellungen",
"resourceRawSettingsDescription": "Konfigurieren Sie den Zugriff auf Ihre Ressource über TCP/UDP",
"resourceRawSettingsDescription": "Legen Sie fest, wie auf Ihre Ressource über TCP/UDP zugegriffen wird. Sie ordnen die Ressource einem Port auf dem Pangolin-Server zu, so dass Sie auf die Ressource von server-public-ip:mapped-port zugreifen können.",
"protocol": "Protokoll",
"protocolSelect": "Wählen Sie ein Protokoll",
"resourcePortNumber": "Portnummer",
@@ -1083,7 +1083,7 @@
"actionCreateClient": "Kunde erstellen",
"actionDeleteClient": "Kunde löschen",
"actionUpdateClient": "Kunde aktualisieren",
"actionListClients": "Kunden auflisten",
"actionListClients": "Clients auflisten",
"actionGetClient": "Kunde holen",
"actionCreateSiteResource": "Site-Ressource erstellen",
"actionDeleteSiteResource": "Site-Ressource löschen",
@@ -1161,17 +1161,17 @@
"sidebarAllUsers": "Alle Benutzer",
"sidebarIdentityProviders": "Identitätsanbieter",
"sidebarLicense": "Lizenz",
"sidebarClients": "Kunden",
"sidebarClients": "Clients",
"sidebarDomains": "Domänen",
"sidebarBluePrints": "Baupläne",
"blueprints": "Baupläne",
"blueprintsDescription": "Blaupausen sind deklarative YAML-Konfigurationen, die deine Ressourcen und deren Einstellungen definieren",
"blueprintsDescription": "Deklarative Konfigurationen anwenden und vorherige Abläufe anzeigen",
"blueprintAdd": "Blaupause hinzufügen",
"blueprintGoBack": "Alle Blaupausen ansehen",
"blueprintCreate": "Blaupause erstellen",
"blueprintCreateDescription2": "Folge den Schritten unten, um eine neue Blaupause zu erstellen und anzuwenden",
"blueprintDetails": "Blaupausendetails",
"blueprintDetailsDescription": "Siehe die Blaupausenlauf-Details",
"blueprintDetailsDescription": "Siehe das Ergebnis der angewendeten Blaupause und alle aufgetretenen Fehler",
"blueprintInfo": "Blaupauseninformation",
"message": "Nachricht",
"blueprintContentsDescription": "Definieren Sie den YAML-Inhalt, der Ihre Infrastruktur beschreibt",
@@ -1181,7 +1181,7 @@
"appliedAt": "Angewandt am",
"source": "Quelle",
"contents": "Inhalt",
"parsedContents": "Analysierte Inhalte",
"parsedContents": "Analysierte Inhalte (Nur lesen)",
"enableDockerSocket": "Docker Blaupause aktivieren",
"enableDockerSocketDescription": "Aktiviere Docker-Socket-Label-Scraping für Blaupausenbeschriftungen. Der Socket-Pfad muss neu angegeben werden.",
"enableDockerSocketLink": "Mehr erfahren",
@@ -1279,6 +1279,15 @@
"settingsErrorUpdateDescription": "Beim Aktualisieren der Einstellungen ist ein Fehler aufgetreten",
"sidebarCollapse": "Zusammenklappen",
"sidebarExpand": "Aufklappen",
"productUpdateMoreInfo": "{noOfUpdates} weitere Updates",
"productUpdateInfo": "{noOfUpdates} Updates",
"productUpdateWhatsNew": "Was ist neu",
"productUpdateTitle": "Produkt-Updates",
"productUpdateEmpty": "Keine Updates",
"dismissAll": "Alle verwerfen",
"pangolinUpdateAvailable": "Neue Version verfügbar",
"pangolinUpdateAvailableInfo": "Version {version} ist bereit zur Installation",
"pangolinUpdateAvailableReleaseNotes": "Versionshinweise anzeigen",
"newtUpdateAvailable": "Update verfügbar",
"newtUpdateAvailableInfo": "Eine neue Version von Newt ist verfügbar. Bitte aktualisieren Sie auf die neueste Version für das beste Erlebnis.",
"domainPickerEnterDomain": "Domäne",
@@ -1423,14 +1432,14 @@
},
"siteRequired": "Standort ist erforderlich.",
"olmTunnel": "Olm-Tunnel",
"olmTunnelDescription": "Nutzen Sie Olm für die Kundenverbindung",
"olmTunnelDescription": "Nutzen Sie Olm für die Client-Verbindung",
"errorCreatingClient": "Fehler beim Erstellen des Clients",
"clientDefaultsNotFound": "Standardeinstellungen des Clients nicht gefunden",
"createClient": "Client erstellen",
"createClientDescription": "Erstellen Sie einen neuen Client für die Verbindung zu Ihren Standorten.",
"seeAllClients": "Alle Clients anzeigen",
"clientInformation": "Kundeninformationen",
"clientNamePlaceholder": "Kundenname",
"clientInformation": "Client-Informationen",
"clientNamePlaceholder": "Client-Name",
"address": "Adresse",
"subnetPlaceholder": "Subnetz",
"addressDescription": "Die Adresse, die dieser Client für die Verbindung verwenden wird.",
@@ -1525,6 +1534,12 @@
"resourcesTableTheseResourcesForUseWith": "Diese Ressourcen sind zur Verwendung mit",
"resourcesTableClients": "Clients",
"resourcesTableAndOnlyAccessibleInternally": "und sind nur intern zugänglich, wenn mit einem Client verbunden.",
"resourcesTableNoTargets": "Keine Ziele",
"resourcesTableHealthy": "Gesund",
"resourcesTableDegraded": "Degradiert",
"resourcesTableOffline": "Offline",
"resourcesTableUnknown": "Unbekannt",
"resourcesTableNotMonitored": "Nicht überwacht",
"editInternalResourceDialogEditClientResource": "Client-Ressource bearbeiten",
"editInternalResourceDialogUpdateResourceProperties": "Aktualisieren Sie die Ressourceneigenschaften und die Zielkonfiguration für {resourceName}.",
"editInternalResourceDialogResourceProperties": "Ressourceneigenschaften",
@@ -2049,7 +2064,7 @@
"orgOrDomainIdMissing": "Organisation oder Domänen-ID fehlt",
"loadingDNSRecords": "Lade DNS-Einträge...",
"olmUpdateAvailableInfo": "Eine aktualisierte Version von Olm ist verfügbar. Bitte aktualisieren Sie auf die neueste Version für die beste Erfahrung.",
"client": "Kunde",
"client": "Client",
"proxyProtocol": "Proxy-Protokoll-Einstellungen",
"proxyProtocolDescription": "Konfigurieren Sie das Proxy-Protokoll, um die IP-Adressen des Clients für TCP/UDP-Dienste zu erhalten.",
"enableProxyProtocol": "Proxy-Protokoll aktivieren",
@@ -2080,5 +2095,46 @@
"supportSending": "Senden...",
"supportSend": "Senden",
"supportMessageSent": "Nachricht gesendet!",
"supportWillContact": "Wir werden in Kürze kontaktieren!"
"supportWillContact": "Wir werden in Kürze kontaktieren!",
"selectLogRetention": "Log-Speicherung auswählen",
"showColumns": "Spalten anzeigen",
"hideColumns": "Spalten ausblenden",
"columnVisibility": "Spaltensichtbarkeit",
"toggleColumn": "{columnName} Spalte umschalten",
"allColumns": "Alle Spalten",
"defaultColumns": "Standardspalten",
"customizeView": "Ansicht anpassen",
"viewOptions": "Optionen anzeigen",
"selectAll": "Alle auswählen",
"selectNone": "Nichts auswählen",
"selectedResources": "Ausgewählte Ressourcen",
"enableSelected": "Ausgewählte aktivieren",
"disableSelected": "Ausgewählte deaktivieren",
"credentials": "Zugangsdaten",
"savecredentials": "Zugangsdaten speichern",
"regeneratecredentials": "Re-Key",
"regenerateCredentials": "Regenerieren und speichern Sie Ihre Zugangsdaten",
"generatedcredentials": "Generierte Zugangsdaten",
"copyandsavethesecredentials": "Diese Zugangsdaten kopieren und speichern",
"copyandsavethesecredentialsdescription": "Diese Zugangsdaten werden nach dem Verlassen dieser Seite nicht mehr angezeigt. Speichern Sie sie jetzt sicher.",
"credentialsSaved": "Zugangsdaten gespeichert",
"credentialsSavedDescription": "Zugangsdaten wurden neu erstellt und erfolgreich gespeichert.",
"credentialsSaveError": "Fehler beim Speichern der Zugangsdaten",
"credentialsSaveErrorDescription": "Beim Erneuern und Speichern der Zugangsdaten ist ein Fehler aufgetreten.",
"regenerateCredentialsWarning": "Das Regenerieren von Zugangsdaten wird die vorhergehenden ungültig machen. Bitte aktualisieren die Konfigurationen, welche diese Zugangsdaten verwenden.",
"confirm": "Bestätigen",
"regenerateCredentialsConfirmation": "Sind Sie sicher, dass Sie die Zugangsdaten neu generieren möchten?",
"endpoint": "Endpunkt",
"Id": "ID",
"SecretKey": "Geheimer Schlüssel",
"featureDisabledTooltip": "Diese Funktion ist nur im Enterprise-Plan verfügbar und erfordert eine Lizenz, um sie zu nutzen.",
"niceId": "Schöne ID",
"niceIdUpdated": "Schöne ID aktualisiert",
"niceIdUpdatedSuccessfully": "Nice ID erfolgreich aktualisiert",
"niceIdUpdateError": "Fehler beim Aktualisieren der Nizza-ID",
"niceIdUpdateErrorDescription": "Beim Aktualisieren der Nizza-ID ist ein Fehler aufgetreten.",
"niceIdCannotBeEmpty": "Nizza-ID darf nicht leer sein",
"enterIdentifier": "Identifikator eingeben",
"identifier": "Identifier",
"checkSelectedStatus": "Status der Auswahl überprüfen"
}

View File

@@ -1,12 +1,12 @@
{
"setupCreate": "Create your organization, site, and resources",
"setupCreate": "Create the organization, site, and resources",
"setupNewOrg": "New Organization",
"setupCreateOrg": "Create Organization",
"setupCreateResources": "Create Resources",
"setupOrgName": "Organization Name",
"orgDisplayName": "This is the display name of your organization.",
"orgDisplayName": "This is the display name of the organization.",
"orgId": "Organization ID",
"setupIdentifierMessage": "This is the unique identifier for your organization. This is separate from the display name.",
"setupIdentifierMessage": "This is the unique identifier for the organization.",
"setupErrorIdentifier": "Organization ID is already taken. Please choose a different one.",
"componentsErrorNoMemberCreate": "You are not currently a member of any organizations. Create an organization to get started.",
"componentsErrorNoMember": "You are not currently a member of any organizations.",
@@ -50,10 +50,10 @@
"siteMessageRemove": "Once removed the site will no longer be accessible. All targets associated with the site will also be removed.",
"siteQuestionRemove": "Are you sure you want to remove the site from the organization?",
"siteManageSites": "Manage Sites",
"siteDescription": "Allow connectivity to your network through secure tunnels",
"siteDescription": "Create and manage sites to enable connectivity to private networks",
"siteCreate": "Create Site",
"siteCreateDescription2": "Follow the steps below to create and connect a new site",
"siteCreateDescription": "Create a new site to start connecting your resources",
"siteCreateDescription": "Create a new site to start connecting resources",
"close": "Close",
"siteErrorCreate": "Error creating site",
"siteErrorCreateKeyPair": "Key pair or site defaults not found",
@@ -74,7 +74,7 @@
"siteInstallNewt": "Install Newt",
"siteInstallNewtDescription": "Get Newt running on your system",
"WgConfiguration": "WireGuard Configuration",
"WgConfigurationDescription": "Use the following configuration to connect to your network",
"WgConfigurationDescription": "Use the following configuration to connect to the network",
"operatingSystem": "Operating System",
"commands": "Commands",
"recommended": "Recommended",
@@ -87,32 +87,32 @@
"siteUpdated": "Site updated",
"siteUpdatedDescription": "The site has been updated.",
"siteGeneralDescription": "Configure the general settings for this site",
"siteSettingDescription": "Configure the settings on your site",
"siteSettingDescription": "Configure the settings on the site",
"siteSetting": "{siteName} Settings",
"siteNewtTunnel": "Newt Tunnel (Recommended)",
"siteNewtTunnelDescription": "Easiest way to create an entrypoint into your network. No extra setup.",
"siteNewtTunnel": "Newt Site (Recommended)",
"siteNewtTunnelDescription": "Easiest way to create an entrypoint into any network. No extra setup.",
"siteWg": "Basic WireGuard",
"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.",
"siteLocalDescription": "Local resources only. No tunneling.",
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
"siteSeeAll": "See All Sites",
"siteTunnelDescription": "Determine how you want to connect to your site",
"siteNewtCredentials": "Newt Credentials",
"siteNewtCredentialsDescription": "This is how Newt will authenticate with the server",
"siteCredentialsSave": "Save Your Credentials",
"siteTunnelDescription": "Determine how you want to connect to the site",
"siteNewtCredentials": "Credentials",
"siteNewtCredentialsDescription": "This is how the site will authenticate with the server",
"siteCredentialsSave": "Save the Credentials",
"siteCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
"siteInfo": "Site Information",
"status": "Status",
"shareTitle": "Manage Share Links",
"shareDescription": "Create shareable links to grant temporary or permanent access to your resources",
"shareDescription": "Create shareable links to grant temporary or permanent access to proxy resources",
"shareSearch": "Search share links...",
"shareCreate": "Create Share Link",
"shareErrorDelete": "Failed to delete link",
"shareErrorDeleteMessage": "An error occurred deleting link",
"shareDeleted": "Link deleted",
"shareDeletedDescription": "The link has been deleted",
"shareTokenDescription": "Your access token can be passed in two ways: as a query parameter or in the request headers. These must be passed from the client on every request for authenticated access.",
"shareTokenDescription": "The access token can be passed in two ways: as a query parameter or in the request headers. These must be passed from the client on every request for authenticated access.",
"accessToken": "Access Token",
"usageExamples": "Usage Examples",
"tokenId": "Token ID",
@@ -121,7 +121,7 @@
"importantNote": "Important Note",
"shareImportantDescription": "For security reasons, using headers is recommended over query parameters when possible, as query parameters may be logged in server logs or browser history.",
"token": "Token",
"shareTokenSecurety": "Keep your access token secure. Do not share it in publicly accessible areas or client-side code.",
"shareTokenSecurety": "Keep the access token secure. Do not share it in publicly accessible areas or client-side code.",
"shareErrorFetchResource": "Failed to fetch resources",
"shareErrorFetchResourceDescription": "An error occurred while fetching the resources",
"shareErrorCreate": "Failed to create share link",
@@ -131,7 +131,7 @@
"expireIn": "Expire In",
"neverExpire": "Never expire",
"shareExpireDescription": "Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource.",
"shareSeeOnce": "You will only be able to see this linkonce. Make sure to copy it.",
"shareSeeOnce": "You will only be able to see this link once. Make sure to copy it.",
"shareAccessHint": "Anyone with this link can access the resource. Share it with care.",
"shareTokenUsage": "See Access Token Usage",
"createLink": "Create Link",
@@ -144,8 +144,10 @@
"expires": "Expires",
"never": "Never",
"shareErrorSelectResource": "Please select a resource",
"resourceTitle": "Manage Resources",
"resourceDescription": "Create secure proxies to your private applications",
"proxyResourceTitle": "Manage Proxy Resources",
"proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser",
"clientResourceTitle": "Manage Client Resources",
"clientResourceDescription": "Create and manage resources that are only accessible through a connected client",
"resourcesSearch": "Search resources...",
"resourceAdd": "Add Resource",
"resourceErrorDelte": "Error deleting resource",
@@ -155,9 +157,9 @@
"resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.",
"resourceQuestionRemove": "Are you sure you want to remove the resource from the organization?",
"resourceHTTP": "HTTPS Resource",
"resourceHTTPDescription": "Proxy requests to your app over HTTPS using a subdomain or base domain.",
"resourceHTTPDescription": "Proxy requests to the app over HTTPS using a subdomain or base domain.",
"resourceRaw": "Raw TCP/UDP Resource",
"resourceRawDescription": "Proxy requests to your app over TCP/UDP using a port number. This only works when sites are connected to nodes.",
"resourceRawDescription": "Proxy requests to the app over TCP/UDP using a port number. This only works when sites are connected to nodes.",
"resourceCreate": "Create Resource",
"resourceCreateDescription": "Follow the steps below to create a new resource",
"resourceSeeAll": "See All Resources",
@@ -171,22 +173,22 @@
"noCountryFound": "No country found.",
"siteSelectionDescription": "This site will provide connectivity to the target.",
"resourceType": "Resource Type",
"resourceTypeDescription": "Determine how you want to access your resource",
"resourceTypeDescription": "Determine how to access the resource",
"resourceHTTPSSettings": "HTTPS Settings",
"resourceHTTPSSettingsDescription": "Configure how your resource will be accessed over HTTPS",
"resourceHTTPSSettingsDescription": "Configure how the resource will be accessed over HTTPS",
"domainType": "Domain Type",
"subdomain": "Subdomain",
"baseDomain": "Base Domain",
"subdomnainDescription": "The subdomain where your resource will be accessible.",
"subdomnainDescription": "The subdomain where the resource will be accessible.",
"resourceRawSettings": "TCP/UDP Settings",
"resourceRawSettingsDescription": "Configure how your resource will be accessed over TCP/UDP",
"resourceRawSettingsDescription": "Configure how the resource will be accessed over TCP/UDP. You map the resource to a port on the host Pangolin server, so you can access the resource from server-public-ip:mapped-port.",
"protocol": "Protocol",
"protocolSelect": "Select a protocol",
"resourcePortNumber": "Port Number",
"resourcePortNumberDescription": "The external port number to proxy requests.",
"cancel": "Cancel",
"resourceConfig": "Configuration Snippets",
"resourceConfigDescription": "Copy and paste these configuration snippets to set up your TCP/UDP resource",
"resourceConfigDescription": "Copy and paste these configuration snippets to set up the TCP/UDP resource",
"resourceAddEntrypoints": "Traefik: Add Entrypoints",
"resourceExposePorts": "Gerbil: Expose Ports in Docker Compose",
"resourceLearnRaw": "Learn how to configure TCP/UDP resources",
@@ -202,14 +204,14 @@
"proxy": "Proxy",
"internal": "Internal",
"rules": "Rules",
"resourceSettingDescription": "Configure the settings on your resource",
"resourceSettingDescription": "Configure the settings on the resource",
"resourceSetting": "{resourceName} Settings",
"alwaysAllow": "Always Allow",
"alwaysDeny": "Always Deny",
"alwaysAllow": "Bypass Auth",
"alwaysDeny": "Block Access",
"passToAuth": "Pass to Auth",
"orgSettingsDescription": "Configure your organization's general settings",
"orgSettingsDescription": "Configure the organization's settings",
"orgGeneralSettings": "Organization Settings",
"orgGeneralSettingsDescription": "Manage your organization details and configuration",
"orgGeneralSettingsDescription": "Manage the organization's details and configuration",
"saveGeneralSettings": "Save General Settings",
"saveSettings": "Save Settings",
"orgDangerZone": "Danger Zone",
@@ -232,7 +234,7 @@
"orgMissing": "Organization ID Missing",
"orgMissingMessage": "Unable to regenerate invitation without an organization ID.",
"accessUsersManage": "Manage Users",
"accessUsersDescription": "Invite users and add them to roles to manage access to your organization",
"accessUsersDescription": "Invite and manage users with access to this organization",
"accessUsersSearch": "Search users...",
"accessUserCreate": "Create User",
"accessUserRemove": "Remove User",
@@ -241,13 +243,13 @@
"role": "Role",
"nameRequired": "Name is required",
"accessRolesManage": "Manage Roles",
"accessRolesDescription": "Configure roles to manage access to your organization",
"accessRolesDescription": "Create and manage roles for users in the organization",
"accessRolesSearch": "Search roles...",
"accessRolesAdd": "Add Role",
"accessRoleDelete": "Delete Role",
"description": "Description",
"inviteTitle": "Open Invitations",
"inviteDescription": "Manage your invitations to other users",
"inviteDescription": "Manage invitations for other users to join the organization",
"inviteSearch": "Search invitations...",
"minutes": "Minutes",
"hours": "Hours",
@@ -261,13 +263,13 @@
"apiKeysErrorCreate": "Error creating API key",
"apiKeysErrorSetPermission": "Error setting permissions",
"apiKeysCreate": "Generate API Key",
"apiKeysCreateDescription": "Generate a new API key for your organization",
"apiKeysCreateDescription": "Generate a new API key for the organization",
"apiKeysGeneralSettings": "Permissions",
"apiKeysGeneralSettingsDescription": "Determine what this API key can do",
"apiKeysList": "Your API Key",
"apiKeysSave": "Save Your API Key",
"apiKeysList": "New API Key",
"apiKeysSave": "Save the API Key",
"apiKeysSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
"apiKeysInfo": "Your API key is:",
"apiKeysInfo": "The API key is:",
"apiKeysConfirmCopy": "I have copied the API key",
"generate": "Generate",
"done": "Done",
@@ -424,7 +426,7 @@
"userCreated": "User created",
"userCreatedDescription": "The user has been successfully created.",
"userTypeInternal": "Internal User",
"userTypeInternalDescription": "Invite a user to join your organization directly.",
"userTypeInternalDescription": "Invite a user to join the organization directly.",
"userTypeExternal": "External User",
"userTypeExternalDescription": "Create a user with an external identity provider.",
"accessUserCreateDescription": "Follow the steps below to create a new user",
@@ -436,6 +438,16 @@
"inviteEmailSent": "Send invite email to user",
"inviteValid": "Valid For",
"selectDuration": "Select duration",
"selectResource": "Select Resource",
"filterByResource": "Filter By Resource",
"resetFilters": "Reset Filters",
"totalBlocked": "Requests Blocked By Pangolin",
"totalRequests": "Total Requests",
"requestsByCountry": "Requests By Country",
"requestsByDay": "Requests By Day",
"blocked": "Blocked",
"allowed": "Allowed",
"topCountries": "Top Countries",
"accessRoleSelect": "Select role",
"inviteEmailSentDescription": "An email has been sent to the user with the access link below. They must access the link to accept the invitation.",
"inviteSentDescription": "The user has been invited. They must access the link below to accept the invitation.",
@@ -458,13 +470,13 @@
"accessControlsSubmit": "Save Access Controls",
"roles": "Roles",
"accessUsersRoles": "Manage Users & Roles",
"accessUsersRolesDescription": "Invite users and add them to roles to manage access to your organization",
"accessUsersRolesDescription": "Invite users and add them to roles to manage access to the organization",
"key": "Key",
"createdAt": "Created At",
"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.",
"proxyEnableSSL": "Enable SSL",
"proxyEnableSSLDescription": "Enable SSL/TLS encryption for secure HTTPS connections to your targets.",
"proxyEnableSSLDescription": "Enable SSL/TLS encryption for secure HTTPS connections to the targets.",
"target": "Target",
"configureTarget": "Configure Targets",
"targetErrorFetch": "Failed to fetch targets",
@@ -480,29 +492,29 @@
"targetsErrorUpdate": "Failed to update targets",
"targetsErrorUpdateDescription": "An error occurred while updating targets",
"targetTlsUpdate": "TLS settings updated",
"targetTlsUpdateDescription": "Your TLS settings have been updated successfully",
"targetTlsUpdateDescription": "TLS settings have been updated successfully",
"targetErrorTlsUpdate": "Failed to update TLS settings",
"targetErrorTlsUpdateDescription": "An error occurred while updating TLS settings",
"proxyUpdated": "Proxy settings updated",
"proxyUpdatedDescription": "Your proxy settings have been updated successfully",
"proxyUpdatedDescription": "Proxy settings have been updated successfully",
"proxyErrorUpdate": "Failed to update proxy settings",
"proxyErrorUpdateDescription": "An error occurred while updating proxy settings",
"targetAddr": "IP / Hostname",
"targetPort": "Port",
"targetProtocol": "Protocol",
"targetTlsSettings": "Secure Connection Configuration",
"targetTlsSettingsDescription": "Configure SSL/TLS settings for your resource",
"targetTlsSettingsDescription": "Configure SSL/TLS settings for the resource",
"targetTlsSettingsAdvanced": "Advanced TLS Settings",
"targetTlsSni": "TLS Server Name",
"targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.",
"targetTlsSubmit": "Save Settings",
"targets": "Targets Configuration",
"targetsDescription": "Set up targets to route traffic to your backend services",
"targetsDescription": "Set up targets to route traffic to backend services",
"targetStickySessions": "Enable Sticky Sessions",
"targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.",
"methodSelect": "Select method",
"targetSubmit": "Add Target",
"targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to your backend.",
"targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to the backend.",
"targetNoOneDescription": "Adding more than one target above will enable load balancing.",
"targetsSubmit": "Save Targets",
"addTarget": "Add Target",
@@ -516,9 +528,11 @@
"targetCreatedDescription": "Target has been created successfully",
"targetErrorCreate": "Failed to create target",
"targetErrorCreateDescription": "An error occurred while creating the target",
"tlsServerName": "TLS Server Name",
"tlsServerNameDescription": "The TLS server name to use for SNI",
"save": "Save",
"proxyAdditional": "Additional Proxy Settings",
"proxyAdditionalDescription": "Configure how your resource handles proxy settings",
"proxyAdditionalDescription": "Configure how the resource handles proxy settings",
"proxyCustomHeader": "Custom Host Header",
"proxyCustomHeaderDescription": "The host header to set when proxying requests. Leave empty to use the default.",
"proxyAdditionalSubmit": "Save Proxy Settings",
@@ -558,7 +572,7 @@
"rulesMatchType": "Match Type",
"value": "Value",
"rulesAbout": "About Rules",
"rulesAboutDescription": "Rules allow you to control access to your resource based on a set of criteria. You can create rules to allow or deny access based on IP address or URL path.",
"rulesAboutDescription": "Rules allow you to control access to the resource based on a set of criteria. You can create rules to allow or deny access based on IP address or URL path.",
"rulesActions": "Actions",
"rulesActionAlwaysAllow": "Always Allow: Bypass all authentication methods",
"rulesActionAlwaysDeny": "Always Deny: Block all requests; no authentication can be attempted",
@@ -570,7 +584,7 @@
"rulesEnable": "Enable Rules",
"rulesEnableDescription": "Enable or disable rule evaluation for this resource",
"rulesResource": "Resource Rules Configuration",
"rulesResourceDescription": "Configure rules to control access to your resource",
"rulesResourceDescription": "Configure rules to control access to the resource",
"ruleSubmit": "Add Rule",
"rulesNoOne": "No rules. Add a rule using the form.",
"rulesOrder": "Rules are evaluated by priority in ascending order.",
@@ -586,7 +600,7 @@
"none": "None",
"unknown": "Unknown",
"resources": "Resources",
"resourcesDescription": "Resources are proxies to applications running on your private network. Create a resource for any HTTP/HTTPS or raw TCP/UDP service on your private network. Each resource must be connected to a site to enable private, secure connectivity through an encrypted WireGuard tunnel.",
"resourcesDescription": "Resources are proxies to applications running on the private network. Create a resource for any HTTP/HTTPS or raw TCP/UDP service on your private network. Each resource must be connected to a site to enable private, secure connectivity through an encrypted WireGuard tunnel.",
"resourcesWireGuardConnect": "Secure connectivity with WireGuard encryption",
"resourcesMultipleAuthenticationMethods": "Configure multiple authentication methods",
"resourcesUsersRolesAccess": "User and role-based access control",
@@ -597,7 +611,7 @@
"resourceSelect": "Select resource",
"shareLinks": "Share Links",
"share": "Shareable Links",
"shareDescription2": "Create shareable links to your resources. Links provide temporary or unlimited access to your resource. You can configure the expiration duration of the link when you create one.",
"shareDescription2": "Create shareable links to resources. Links provide temporary or unlimited access to your resource. You can configure the expiration duration of the link when you create one.",
"shareEasyCreate": "Easy to create and share",
"shareConfigurableExpirationDuration": "Configurable expiration duration",
"shareSecureAndRevocable": "Secure and revocable",
@@ -607,19 +621,19 @@
"unknownCommand": "Unknown command",
"newtErrorFetchReleases": "Failed to fetch release info: {err}",
"newtErrorFetchLatest": "Error fetching latest release: {err}",
"newtEndpoint": "Newt Endpoint",
"newtId": "Newt ID",
"newtSecretKey": "Newt Secret Key",
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Secret",
"architecture": "Architecture",
"sites": "Sites",
"siteWgAnyClients": "Use any WireGuard client to connect. You will have to address your internal resources using the peer IP.",
"siteWgAnyClients": "Use any WireGuard client to connect. You will have to address internal resources using the peer IP.",
"siteWgCompatibleAllClients": "Compatible with all WireGuard clients",
"siteWgManualConfigurationRequired": "Manual configuration required",
"userErrorNotAdminOrOwner": "User is not an admin or owner",
"pangolinSettings": "Settings - Pangolin",
"accessRoleYour": "Your role:",
"accessRoleSelect2": "Select a role",
"accessUserSelect": "Select a user",
"accessRoleSelect2": "Select roles",
"accessUserSelect": "Select users",
"otpEmailEnter": "Enter an email",
"otpEmailEnterDescription": "Press enter to add an email after typing it in the input field.",
"otpEmailErrorInvalid": "Invalid email address. Wildcard (*) must be the entire local part.",
@@ -671,7 +685,7 @@
"resourcePincodeSetupTitle": "Set Pincode",
"resourcePincodeSetupTitleDescription": "Set a pincode to protect this resource",
"resourceRoleDescription": "Admins can always access this resource.",
"resourceUsersRoles": "Users & Roles",
"resourceUsersRoles": "Access Controls",
"resourceUsersRolesDescription": "Configure which users and roles can visit this resource",
"resourceUsersRolesSubmit": "Save Users & Roles",
"resourceWhitelistSave": "Saved successfully",
@@ -702,6 +716,7 @@
"resourceTransferSubmit": "Transfer Resource",
"siteDestination": "Destination Site",
"searchSites": "Search sites",
"countries": "Countries",
"accessRoleCreate": "Create Role",
"accessRoleCreateDescription": "Create a new role to group users and manage their permissions.",
"accessRoleCreateSubmit": "Create Role",
@@ -766,15 +781,15 @@
"idpOidcConfigure": "OAuth2/OIDC Configuration",
"idpOidcConfigureDescription": "Configure the OAuth2/OIDC provider endpoints and credentials",
"idpClientId": "Client ID",
"idpClientIdDescription": "The OAuth2 client ID from your identity provider",
"idpClientIdDescription": "The OAuth2 client ID from the identity provider",
"idpClientSecret": "Client Secret",
"idpClientSecretDescription": "The OAuth2 client secret from your identity provider",
"idpClientSecretDescription": "The OAuth2 client secret from the identity provider",
"idpAuthUrl": "Authorization URL",
"idpAuthUrlDescription": "The OAuth2 authorization endpoint URL",
"idpTokenUrl": "Token URL",
"idpTokenUrlDescription": "The OAuth2 token endpoint URL",
"idpOidcConfigureAlert": "Important Information",
"idpOidcConfigureAlertDescription": "After creating the identity provider, you will need to configure the callback URL in your identity provider's settings. The callback URL will be provided after successful creation.",
"idpOidcConfigureAlertDescription": "After creating the identity provider, you will need to configure the callback URL in the identity provider's settings. The callback URL will be provided after successful creation.",
"idpToken": "Token Configuration",
"idpTokenDescription": "Configure how to extract user information from the ID token",
"idpJmespathAbout": "About JMESPath",
@@ -791,7 +806,7 @@
"idpSubmit": "Create Identity Provider",
"orgPolicies": "Organization Policies",
"idpSettings": "{idpName} Settings",
"idpCreateSettingsDescription": "Configure the settings for your identity provider",
"idpCreateSettingsDescription": "Configure the settings for the identity provider",
"roleMapping": "Role Mapping",
"orgMapping": "Organization Mapping",
"orgPoliciesSearch": "Search organization policies...",
@@ -826,7 +841,7 @@
"idpUpdatedDescription": "Identity provider updated successfully",
"redirectUrl": "Redirect URL",
"redirectUrlAbout": "About Redirect URL",
"redirectUrlAboutDescription": "This is the URL to which users will be redirected after authentication. You need to configure this URL in your identity provider settings.",
"redirectUrlAboutDescription": "This is the URL to which users will be redirected after authentication. You need to configure this URL in the identity provider's settings.",
"pangolinAuth": "Auth - Pangolin",
"verificationCodeLengthRequirements": "Your verification code must be 8 characters.",
"errorOccurred": "An error occurred",
@@ -1091,12 +1106,15 @@
"actionListSiteResources": "List Site Resources",
"actionUpdateSiteResource": "Update Site Resource",
"actionListInvitations": "List Invitations",
"actionExportLogs": "Export Logs",
"actionViewLogs": "View Logs",
"noneSelected": "None selected",
"orgNotFound2": "No organizations found.",
"searchProgress": "Search...",
"create": "Create",
"orgs": "Organizations",
"loginError": "An error occurred while logging in",
"loginRequiredForDevice": "Login is required to authenticate your device.",
"passwordForgot": "Forgot your password?",
"otpAuth": "Two-Factor Authentication",
"otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.",
@@ -1151,37 +1169,47 @@
"sidebarHome": "Home",
"sidebarSites": "Sites",
"sidebarResources": "Resources",
"sidebarProxyResources": "Proxy Resources",
"sidebarClientResources": "Client Resources",
"sidebarAccessControl": "Access Control",
"sidebarLogsAndAnalytics": "Logs & Analytics",
"sidebarUsers": "Users",
"sidebarAdmin": "Admin",
"sidebarInvitations": "Invitations",
"sidebarRoles": "Roles",
"sidebarShareableLinks": "Shareable Links",
"sidebarShareableLinks": "Links",
"sidebarApiKeys": "API Keys",
"sidebarSettings": "Settings",
"sidebarAllUsers": "All Users",
"sidebarIdentityProviders": "Identity Providers",
"sidebarLicense": "License",
"sidebarClients": "Clients",
"sidebarUserDevices": "User Devices",
"sidebarMachineClients": "Machine Clients",
"sidebarDomains": "Domains",
"sidebarGeneral": "General",
"sidebarLogAndAnalytics": "Log & Analytics",
"sidebarBluePrints": "Blueprints",
"sidebarOrganization": "Organization",
"sidebarLogsAnalytics": "Request Analytics",
"blueprints": "Blueprints",
"blueprintsDescription": "Blueprints are declarative YAML configurations that define your resources and their settings",
"blueprintsDescription": "Apply declarative configurations and view previous runs",
"blueprintAdd": "Add Blueprint",
"blueprintGoBack": "See all Blueprints",
"blueprintCreate": "Create Blueprint",
"blueprintCreateDescription2": "Follow the steps below to create and apply a new blueprint",
"blueprintDetails": "Blueprint details",
"blueprintDetailsDescription": "See the blueprint run details",
"blueprintDetails": "Blueprint Details",
"blueprintDetailsDescription": "See the result of the applied blueprint and any errors that occurred",
"blueprintInfo": "Blueprint Information",
"message": "Message",
"blueprintContentsDescription": "Define the YAML content describing your infrastructure",
"blueprintContentsDescription": "Define the YAML content describing the infrastructure",
"blueprintErrorCreateDescription": "An error occurred when applying the blueprint",
"blueprintErrorCreate": "Error creating blueprint",
"searchBlueprintProgress": "Search blueprints...",
"appliedAt": "Applied At",
"source": "Source",
"contents": "Contents",
"parsedContents": "Parsed Contents",
"parsedContents": "Parsed Contents (Read Only)",
"enableDockerSocket": "Enable Docker Blueprint",
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
"enableDockerSocketLink": "Learn More",
@@ -1230,15 +1258,15 @@
"loading": "Loading",
"restart": "Restart",
"domains": "Domains",
"domainsDescription": "Manage domains for your organization",
"domainsDescription": "Create and manage domains available in the organization",
"domainsSearch": "Search domains...",
"domainAdd": "Add Domain",
"domainAddDescription": "Register a new domain with your organization",
"domainAddDescription": "Register a new domain with to the organization",
"domainCreate": "Create Domain",
"domainCreatedDescription": "Domain created successfully",
"domainDeletedDescription": "Domain deleted successfully",
"domainQuestionRemove": "Are you sure you want to remove the domain from your account?",
"domainMessageRemove": "Once removed, the domain will no longer be associated with your account.",
"domainQuestionRemove": "Are you sure you want to remove the domain?",
"domainMessageRemove": "Once removed, the domain will no longer be associated with the organization.",
"domainConfirmDelete": "Confirm Delete Domain",
"domainDelete": "Delete Domain",
"domain": "Domain",
@@ -1257,7 +1285,7 @@
"pending": "Pending",
"sidebarBilling": "Billing",
"billing": "Billing",
"orgBillingDescription": "Manage your billing information and subscriptions",
"orgBillingDescription": "Manage billing information and subscriptions",
"github": "GitHub",
"pangolinHosted": "Pangolin Hosted",
"fossorial": "Fossorial",
@@ -1279,6 +1307,15 @@
"settingsErrorUpdateDescription": "An error occurred while updating settings",
"sidebarCollapse": "Collapse",
"sidebarExpand": "Expand",
"productUpdateMoreInfo": "{noOfUpdates} more updates",
"productUpdateInfo": "{noOfUpdates} updates",
"productUpdateWhatsNew": "What's New",
"productUpdateTitle": "Product Updates",
"productUpdateEmpty": "No updates",
"dismissAll": "Dismiss all",
"pangolinUpdateAvailable": "New version available",
"pangolinUpdateAvailableInfo": "Version {version} is ready to install",
"pangolinUpdateAvailableReleaseNotes": "View release notes",
"newtUpdateAvailable": "Update Available",
"newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.",
"domainPickerEnterDomain": "Domain",
@@ -1291,7 +1328,7 @@
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Checking availability...",
"domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.",
"domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check the organization's domain settings.",
"domainPickerOrganizationDomains": "Organization Domains",
"domainPickerProvidedDomains": "Provided Domains",
"domainPickerSubdomain": "Subdomain: {subdomain}",
@@ -1325,7 +1362,7 @@
"billingModifySubscription": "Modify Subscription",
"billingStartSubscription": "Start Subscription",
"billingRecurringCharge": "Recurring Charge",
"billingManageSubscriptionSettings": "Manage your subscription settings and preferences",
"billingManageSubscriptionSettings": "Manage subscription settings and preferences",
"billingNoActiveSubscription": "You don't have an active subscription. Start your subscription to increase usage limits.",
"billingFailedToLoadSubscription": "Failed to load subscription",
"billingFailedToLoadUsage": "Failed to load usage",
@@ -1336,9 +1373,9 @@
"billingPortalError": "Portal Error",
"billingDataUsageInfo": "You're charged for all data transferred through your secure tunnels when connected to the cloud. This includes both incoming and outgoing traffic across all your sites. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Data is not charged when using nodes.",
"billingOnlineTimeInfo": "You're charged based on how long your sites stay connected to the cloud. For example, 44,640 minutes equals one site running 24/7 for a full month. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Time is not charged when using nodes.",
"billingUsersInfo": "You're charged for each user in your organization. Billing is calculated daily based on the number of active user accounts in your org.",
"billingDomainInfo": "You're charged for each domain in your organization. Billing is calculated daily based on the number of active domain accounts in your org.",
"billingRemoteExitNodesInfo": "You're charged for each managed Node in your organization. Billing is calculated daily based on the number of active managed Nodes in your org.",
"billingUsersInfo": "You're charged for each user in the organization. Billing is calculated daily based on the number of active user accounts in your org.",
"billingDomainInfo": "You're charged for each domain in the organization. Billing is calculated daily based on the number of active domain accounts in your org.",
"billingRemoteExitNodesInfo": "You're charged for each managed Node in the organization. Billing is calculated daily based on the number of active managed Nodes in your org.",
"domainNotFound": "Domain Not Found",
"domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.",
"failed": "Failed",
@@ -1421,29 +1458,32 @@
"and": "and",
"privacyPolicy": "privacy policy"
},
"signUpMarketing": {
"keepMeInTheLoop": "Keep me in the loop with news, updates, and new features by email."
},
"siteRequired": "Site is required.",
"olmTunnel": "Olm Tunnel",
"olmTunnelDescription": "Use Olm for client connectivity",
"errorCreatingClient": "Error creating client",
"clientDefaultsNotFound": "Client defaults not found",
"createClient": "Create Client",
"createClientDescription": "Create a new client for connecting to your sites",
"createClientDescription": "Create a new client to access private resources",
"seeAllClients": "See All Clients",
"clientInformation": "Client Information",
"clientNamePlaceholder": "Client name",
"address": "Address",
"subnetPlaceholder": "Subnet",
"addressDescription": "The address that this client will use for connectivity",
"addressDescription": "The internal address of the client. Must fall within the organization's subnet.",
"selectSites": "Select sites",
"sitesDescription": "The client will have connectivity to the selected sites",
"clientInstallOlm": "Install Olm",
"clientInstallOlmDescription": "Get Olm running on your system",
"clientOlmCredentials": "Olm Credentials",
"clientOlmCredentialsDescription": "This is how Olm will authenticate with the server",
"olmEndpoint": "Olm Endpoint",
"olmId": "Olm ID",
"olmSecretKey": "Olm Secret Key",
"clientCredentialsSave": "Save Your Credentials",
"clientOlmCredentials": "Credentials",
"clientOlmCredentialsDescription": "This is how the client will authenticate with the server",
"olmEndpoint": "Endpoint",
"olmId": "ID",
"olmSecretKey": "Secret",
"clientCredentialsSave": "Save the Credentials",
"clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
"generalSettingsDescription": "Configure the general settings for this client",
"clientUpdated": "Client updated",
@@ -1454,9 +1494,7 @@
"sitesFetchError": "An error occurred while fetching sites.",
"olmErrorFetchReleases": "An error occurred while fetching Olm releases.",
"olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.",
"remoteSubnets": "Remote Subnets",
"enterCidrRange": "Enter CIDR range",
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
"resourceEnableProxy": "Enable Public Proxy",
"resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.",
"externalProxyEnabled": "External Proxy Enabled",
@@ -1474,14 +1512,15 @@
"enableHealthChecksDescription": "Monitor the health of this target. You can monitor a different endpoint than the target if required.",
"healthScheme": "Method",
"healthSelectScheme": "Select Method",
"healthCheckPortInvalid": "Health check port must be between 1 and 65535",
"healthCheckPath": "Path",
"healthHostname": "IP / Host",
"healthPort": "Port",
"healthCheckPathDescription": "The path to check for health status.",
"healthyIntervalSeconds": "Healthy Interval",
"unhealthyIntervalSeconds": "Unhealthy Interval",
"healthyIntervalSeconds": "Healthy Interval (sec)",
"unhealthyIntervalSeconds": "Unhealthy Interval (sec)",
"IntervalSeconds": "Healthy Interval",
"timeoutSeconds": "Timeout",
"timeoutSeconds": "Timeout (sec)",
"timeIsInSeconds": "Time is in seconds",
"retryAttempts": "Retry Attempts",
"expectedResponseCodes": "Expected Response Codes",
@@ -1517,16 +1556,22 @@
"resourceEditDomain": "Edit Domain",
"siteName": "Site Name",
"proxyPort": "Port",
"resourcesTableProxyResources": "Proxy Resources",
"resourcesTableClientResources": "Client Resources",
"resourcesTableProxyResources": "Public",
"resourcesTableClientResources": "Private",
"resourcesTableNoProxyResourcesFound": "No proxy resources found.",
"resourcesTableNoInternalResourcesFound": "No internal resources found.",
"resourcesTableDestination": "Destination",
"resourcesTableTheseResourcesForUseWith": "These resources are for use with",
"resourcesTableAlias": "Alias",
"resourcesTableClients": "Clients",
"resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.",
"resourcesTableNoTargets": "No targets",
"resourcesTableHealthy": "Healthy",
"resourcesTableDegraded": "Degraded",
"resourcesTableOffline": "Offline",
"resourcesTableUnknown": "Unknown",
"resourcesTableNotMonitored": "Not monitored",
"editInternalResourceDialogEditClientResource": "Edit Client Resource",
"editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.",
"editInternalResourceDialogUpdateResourceProperties": "Update the resource configuration and access controls for {resourceName}",
"editInternalResourceDialogResourceProperties": "Resource Properties",
"editInternalResourceDialogName": "Name",
"editInternalResourceDialogProtocol": "Protocol",
@@ -1545,11 +1590,22 @@
"editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format",
"editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1",
"editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536",
"editInternalResourceDialogPortModeRequired": "Protocol, proxy port, and destination port are required for port mode",
"editInternalResourceDialogMode": "Mode",
"editInternalResourceDialogModePort": "Port",
"editInternalResourceDialogModeHost": "Host",
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogDestination": "Destination",
"editInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.",
"editInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.",
"editInternalResourceDialogDestinationCidrDescription": "The CIDR range of the resource on the site's network.",
"editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAliasDescription": "An optional internal DNS alias for this resource.",
"createInternalResourceDialogNoSitesAvailable": "No Sites Available",
"createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.",
"createInternalResourceDialogClose": "Close",
"createInternalResourceDialogCreateClientResource": "Create Client Resource",
"createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.",
"createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will only be accessible to clients connected to the organization",
"createInternalResourceDialogResourceProperties": "Resource Properties",
"createInternalResourceDialogName": "Name",
"createInternalResourceDialogSite": "Site",
@@ -1578,11 +1634,22 @@
"createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format",
"createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1",
"createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536",
"createInternalResourceDialogPortModeRequired": "Protocol, proxy port, and destination port are required for port mode",
"createInternalResourceDialogMode": "Mode",
"createInternalResourceDialogModePort": "Port",
"createInternalResourceDialogModeHost": "Host",
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogDestination": "Destination",
"createInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.",
"createInternalResourceDialogDestinationCidrDescription": "The CIDR range of the resource on the site's network.",
"createInternalResourceDialogAlias": "Alias",
"createInternalResourceDialogAliasDescription": "An optional internal DNS alias for this resource.",
"siteConfiguration": "Configuration",
"siteAcceptClientConnections": "Accept Client Connections",
"siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.",
"siteAddress": "Site Address",
"siteAddressDescription": "Specify the IP address of the host for clients to connect to. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.",
"siteAcceptClientConnectionsDescription": "Allow user devices and clients to access resources on this site. This can be changed later.",
"siteAddress": "Site Address (Advanced)",
"siteAddressDescription": "The internal address of the site. Must fall within the organization's subnet.",
"siteNameDescription": "The display name of the site that can be changed later.",
"autoLoginExternalIdp": "Auto Login with External IDP",
"autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.",
"selectIdp": "Select IDP",
@@ -1596,7 +1663,7 @@
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
"remoteExitNodeManageRemoteExitNodes": "Remote Nodes",
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend network connectivity and reduce reliance on the cloud",
"remoteExitNodes": "Nodes",
"searchRemoteExitNodes": "Search nodes...",
"remoteExitNodeAdd": "Add Node",
@@ -1608,11 +1675,11 @@
"sidebarRemoteExitNodes": "Remote Nodes",
"remoteExitNodeCreate": {
"title": "Create Node",
"description": "Create a new node to extend your network connectivity",
"description": "Create a new node to extend network connectivity",
"viewAllButton": "View All Nodes",
"strategy": {
"title": "Creation Strategy",
"description": "Choose this to manually configure your node or generate new credentials.",
"description": "Choose this to manually configure the node or generate new credentials.",
"adopt": {
"title": "Adopt Node",
"description": "Choose this if you already have the credentials for the node."
@@ -1633,7 +1700,7 @@
},
"generate": {
"title": "Generated Credentials",
"description": "Use these generated credentials to configure your node",
"description": "Use these generated credentials to configure the node",
"nodeIdTitle": "Node ID",
"secretTitle": "Secret",
"saveCredentialsTitle": "Add Credentials to Config",
@@ -1709,16 +1776,16 @@
"idpTypeLabel": "Identity Provider Type",
"roleMappingExpressionPlaceholder": "e.g., contains(groups, 'admin') && 'Admin' || 'Member'",
"idpGoogleConfiguration": "Google Configuration",
"idpGoogleConfigurationDescription": "Configure your Google OAuth2 credentials",
"idpGoogleClientIdDescription": "Your Google OAuth2 Client ID",
"idpGoogleClientSecretDescription": "Your Google OAuth2 Client Secret",
"idpGoogleConfigurationDescription": "Configure the Google OAuth2 credentials",
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
"idpGoogleClientSecretDescription": "Google OAuth2 Client Secret",
"idpAzureConfiguration": "Azure Entra ID Configuration",
"idpAzureConfigurationDescription": "Configure your Azure Entra ID OAuth2 credentials",
"idpAzureConfigurationDescription": "Configure Azure Entra ID OAuth2 credentials",
"idpTenantId": "Tenant ID",
"idpTenantIdPlaceholder": "your-tenant-id",
"idpAzureTenantIdDescription": "Your Azure tenant ID (found in Azure Active Directory overview)",
"idpAzureClientIdDescription": "Your Azure App Registration Client ID",
"idpAzureClientSecretDescription": "Your Azure App Registration Client Secret",
"idpTenantIdPlaceholder": "tenant-id",
"idpAzureTenantIdDescription": "Azure tenant ID (found in Azure Active Directory overview)",
"idpAzureClientIdDescription": "Azure App Registration Client ID",
"idpAzureClientSecretDescription": "Azure App Registration Client Secret",
"idpGoogleTitle": "Google",
"idpGoogleAlt": "Google",
"idpAzureTitle": "Azure Entra ID",
@@ -1726,14 +1793,14 @@
"idpGoogleConfigurationTitle": "Google Configuration",
"idpAzureConfigurationTitle": "Azure Entra ID Configuration",
"idpTenantIdLabel": "Tenant ID",
"idpAzureClientIdDescription2": "Your Azure App Registration Client ID",
"idpAzureClientSecretDescription2": "Your Azure App Registration Client Secret",
"idpAzureClientIdDescription2": "Azure App Registration Client ID",
"idpAzureClientSecretDescription2": "Azure App Registration Client Secret",
"idpGoogleDescription": "Google OAuth2/OIDC provider",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
"subnet": "Subnet",
"subnetDescription": "The subnet for this organization's network configuration.",
"authPage": "Auth Page",
"authPageDescription": "Configure the auth page for your organization",
"authPageDescription": "Configure the auth page for the organization",
"authPageDomain": "Auth Page Domain",
"noDomainSet": "No domain set",
"changeDomain": "Change Domain",
@@ -1743,7 +1810,7 @@
"setAuthPageDomain": "Set Auth Page Domain",
"failedToFetchCertificate": "Failed to fetch certificate",
"failedToRestartCertificate": "Failed to restart certificate",
"addDomainToEnableCustomAuthPages": "Add a domain to enable custom authentication pages for your organization",
"addDomainToEnableCustomAuthPages": "Add a domain to enable custom authentication pages for the organization",
"selectDomainForOrgAuthPage": "Select a domain for the organization's authentication page",
"domainPickerProvidedDomain": "Provided Domain",
"domainPickerFreeProvidedDomain": "Free Provided Domain",
@@ -1758,7 +1825,7 @@
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" could not be made valid for {domain}.",
"domainPickerSubdomainSanitized": "Subdomain sanitized",
"domainPickerSubdomainCorrected": "\"{sub}\" was corrected to \"{sanitized}\"",
"orgAuthSignInTitle": "Sign in to your organization",
"orgAuthSignInTitle": "Sign in to the organization",
"orgAuthChooseIdpDescription": "Choose your identity provider to continue",
"orgAuthNoIdpConfigured": "This organization doesn't have any identity providers configured. You can log in with your Pangolin identity instead.",
"orgAuthSignInWithPangolin": "Sign in with Pangolin",
@@ -1776,7 +1843,7 @@
"enableTwoFactorAuthentication": "Enable two-factor authentication",
"completeSecuritySteps": "Complete Security Steps",
"securitySettings": "Security Settings",
"securitySettingsDescription": "Configure security policies for your organization",
"securitySettingsDescription": "Configure security policies for the organization",
"requireTwoFactorForAllUsers": "Require Two-Factor Authentication for All Users",
"requireTwoFactorDescription": "When enabled, all internal users in this organization must have two-factor authentication enabled to access the organization.",
"requireTwoFactorDisabledDescription": "This feature requires a valid license (Enterprise) or active subscription (SaaS)",
@@ -1839,8 +1906,12 @@
"enterpriseEdition": "Enterprise Edition",
"unlicensed": "Unlicensed",
"beta": "Beta",
"manageClients": "Manage Clients",
"manageClientsDescription": "Clients are devices that can connect to your sites",
"manageUserDevices": "User Devices",
"manageUserDevicesDescription": "View and manage devices that users use to privately connect to resources",
"manageMachineClients": "Manage Machine Clients",
"manageMachineClientsDescription": "Create and manage clients that servers and systems use to privately connect to resources",
"clientsTableUserClients": "User",
"clientsTableMachineClients": "Machine",
"licenseTableValidUntil": "Valid Until",
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
@@ -1980,6 +2051,7 @@
"clientMessageRemove": "Once removed, the client will no longer be able to connect to the site.",
"sidebarLogs": "Logs",
"request": "Request",
"requests": "Requests",
"logs": "Logs",
"logsSettingsDescription": "Monitor logs collected from this orginization",
"searchLogs": "Search logs...",
@@ -2005,6 +2077,7 @@
"ip": "IP",
"reason": "Reason",
"requestLogs": "Request Logs",
"requestAnalytics": "Request Analytics",
"host": "Host",
"location": "Location",
"actionLogs": "Action Logs",
@@ -2014,6 +2087,7 @@
"logRetention": "Log Retention",
"logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them",
"requestLogsDescription": "View detailed request logs for resources in this organization",
"requestAnalyticsDescription": "View detailed request analytics for resources in this organization",
"logRetentionRequestLabel": "Request Log Retention",
"logRetentionRequestDescription": "How long to retain request logs",
"logRetentionAccessLabel": "Access Log Retention",
@@ -2037,7 +2111,7 @@
"preferWildcardCert": "Prefer Wildcard Certificate",
"unverified": "Unverified",
"domainSetting": "Domain Settings",
"domainSettingDescription": "Configure settings for your domain",
"domainSettingDescription": "Configure settings for the domain",
"preferWildcardCertDescription": "Attempt to generate a wildcard certificate (require a properly configured certificate resolver).",
"recordName": "Record Name",
"auto": "Auto",
@@ -2051,15 +2125,15 @@
"olmUpdateAvailableInfo": "An updated version of Olm is available. Please update to the latest version for the best experience.",
"client": "Client",
"proxyProtocol": "Proxy Protocol Settings",
"proxyProtocolDescription": "Configure Proxy Protocol to preserve client IP addresses for TCP/UDP services.",
"proxyProtocolDescription": "Configure Proxy Protocol to preserve client IP addresses for TCP services.",
"enableProxyProtocol": "Enable Proxy Protocol",
"proxyProtocolInfo": "Preserve client IP addresses for TCP/UDP backends",
"proxyProtocolInfo": "Preserve client IP addresses for TCP backends",
"proxyProtocolVersion": "Proxy Protocol Version",
"version1": " Version 1 (Recommended)",
"version2": "Version 2",
"versionDescription": "Version 1 is text-based and widely supported. Version 2 is binary and more efficient but less compatible.",
"versionDescription": "Version 1 is text-based and widely supported. Version 2 is binary and more efficient but less compatible. Make sure servers transport is added to dynamic config.",
"warning": "Warning",
"proxyProtocolWarning": "Your backend application must be configured to accept Proxy Protocol connections. If your backend doesn't support Proxy Protocol, enabling this will break all connections. Make sure to configure your backend to trust Proxy Protocol headers from Traefik.",
"proxyProtocolWarning": "The backend application must be configured to accept Proxy Protocol connections. If your backend doesn't support Proxy Protocol, enabling this will break all connections so only enable this if you know what you're doing. Make sure to configure your backend to trust Proxy Protocol headers from Traefik.",
"restarting": "Restarting...",
"manual": "Manual",
"messageSupport": "Message Support",
@@ -2080,5 +2154,98 @@
"supportSending": "Sending...",
"supportSend": "Send",
"supportMessageSent": "Message Sent!",
"supportWillContact": "We'll be in touch shortly!"
"supportWillContact": "We'll be in touch shortly!",
"selectLogRetention": "Select log retention",
"terms": "Terms",
"privacy": "Privacy",
"security": "Security",
"docs": "Docs",
"deviceActivation": "Device activation",
"deviceCodeInvalidFormat": "Code must be 9 characters (e.g., A1AJ-N5JD)",
"deviceCodeInvalidOrExpired": "Invalid or expired code",
"deviceCodeVerifyFailed": "Failed to verify device code",
"signedInAs": "Signed in as",
"deviceCodeEnterPrompt": "Enter the code displayed on the device",
"continue": "Continue",
"deviceUnknownLocation": "Unknown location",
"deviceAuthorizationRequested": "This authorization was requested from {location} on {date}. Make sure you trust this device as it will get access to the account.",
"deviceLabel": "Device: {deviceName}",
"deviceWantsAccess": "wants to access your account",
"deviceExistingAccess": "Existing access:",
"deviceFullAccess": "Full access to your account",
"deviceOrganizationsAccess": "Access to all organizations your account has access to",
"deviceAuthorize": "Authorize {applicationName}",
"deviceConnected": "Device Connected!",
"deviceAuthorizedMessage": "Device is authorized to access your account.",
"pangolinCloud": "Pangolin Cloud",
"viewDevices": "View Devices",
"viewDevicesDescription": "Manage your connected devices",
"noDevices": "No devices found",
"dateCreated": "Date Created",
"unnamedDevice": "Unnamed Device",
"deviceQuestionRemove": "Are you sure you want to delete this device?",
"deviceMessageRemove": "This action cannot be undone.",
"deviceDeleteConfirm": "Delete Device",
"deleteDevice": "Delete Device",
"errorLoadingDevices": "Error loading devices",
"failedToLoadDevices": "Failed to load devices",
"deviceDeleted": "Device deleted",
"deviceDeletedDescription": "The device has been successfully deleted.",
"errorDeletingDevice": "Error deleting device",
"failedToDeleteDevice": "Failed to delete device",
"showColumns": "Show Columns",
"hideColumns": "Hide Columns",
"columnVisibility": "Column Visibility",
"toggleColumn": "Toggle {columnName} column",
"allColumns": "All Columns",
"defaultColumns": "Default Columns",
"customizeView": "Customize View",
"viewOptions": "View Options",
"selectAll": "Select All",
"selectNone": "Select None",
"selectedResources": "Selected Resources",
"enableSelected": "Enable Selected",
"disableSelected": "Disable Selected",
"checkSelectedStatus": "Check Status of Selected",
"clients": "Clients",
"accessClientSelect": "Select machine clients",
"resourceClientDescription": "Machine clients that can access this resource",
"regenerate": "Regenerate",
"credentials": "Credentials",
"savecredentials": "Save Credentials",
"regeneratecredentials": "Re-key",
"regenerateCredentials": "Regenerate and save your credentials",
"generatedcredentials": "Generated Credentials",
"copyandsavethesecredentials": "Copy and save these credentials",
"copyandsavethesecredentialsdescription": "These credentials will not be shown again after you leave this page. Save them securely now.",
"credentialsSaved": "Credentials Saved",
"credentialsSavedDescription": "Credentials have been regenerated and saved successfully.",
"credentialsSaveError": "Credentials Save Error",
"credentialsSaveErrorDescription": "An error occurred while regenerating and saving the credentials.",
"regenerateCredentialsWarning": "Regenerating credentials will invalidate the previous ones. Make sure to update any configurations that use these credentials.",
"confirm": "Confirm",
"regenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials?",
"endpoint": "Endpoint",
"Id": "Id",
"SecretKey": "Secret Key",
"featureDisabledTooltip": "This feature is only available in the enterprise plan and require a license to use it.",
"niceId": "Nice ID",
"niceIdUpdated": "Nice ID Updated",
"niceIdUpdatedSuccessfully": "Nice ID Updated Successfully",
"niceIdUpdateError": "Error updating Nice ID",
"niceIdUpdateErrorDescription": "An error occurred while updating the Nice ID.",
"niceIdCannotBeEmpty": "Nice ID cannot be empty",
"enterIdentifier": "Enter identifier",
"identifier": "Identifier",
"deviceLoginUseDifferentAccount": "Not you? Use a different account.",
"deviceLoginDeviceRequestingAccessToAccount": "A device is requesting access to this account.",
"noData": "No Data",
"machineClients": "Machine Clients",
"install": "Install",
"run": "Run",
"clientNameDescription": "The display name of the client that can be changed later.",
"clientAddress": "Client Address (Advanced)",
"setupFailedToFetchSubnet": "Failed to fetch default subnet",
"setupSubnetAdvanced": "Subnet (Advanced)",
"setupSubnetDescription": "The subnet for this organization's internal network."
}

View File

@@ -131,7 +131,7 @@
"expireIn": "Caduca en",
"neverExpire": "Nunca expirar",
"shareExpireDescription": "El tiempo de caducidad es cuánto tiempo el enlace será utilizable y proporcionará acceso al recurso. Después de este tiempo, el enlace ya no funcionará, y los usuarios que usaron este enlace perderán el acceso al recurso.",
"shareSeeOnce": "Sólo podrá ver este enlace una vez. Asegúrese de copiarlo.",
"shareSeeOnce": "Sólo podrás ver este enlace una vez. Asegúrate de copiarlo.",
"shareAccessHint": "Cualquiera con este enlace puede acceder al recurso. Compártelo con cuidado.",
"shareTokenUsage": "Ver Uso de Token de Acceso",
"createLink": "Crear enlace",
@@ -179,7 +179,7 @@
"baseDomain": "Dominio base",
"subdomnainDescription": "El subdominio al que su recurso será accesible.",
"resourceRawSettings": "Configuración TCP/UDP",
"resourceRawSettingsDescription": "Configurar cómo se accederá a su recurso a través de TCP/UDP",
"resourceRawSettingsDescription": "Configure cómo se accederá a su recurso a través de TCP/UDP. Mapeas el recurso a un puerto en el servidor Pangolin host, así puedes acceder al recurso desde el servidor-public-ip:mapped-port.",
"protocol": "Protocolo",
"protocolSelect": "Seleccionar un protocolo",
"resourcePortNumber": "Número de puerto",
@@ -1165,13 +1165,13 @@
"sidebarDomains": "Dominios",
"sidebarBluePrints": "Planos",
"blueprints": "Planos",
"blueprintsDescription": "Los planos son configuraciones YAML declarativas que definen sus recursos y sus configuraciones",
"blueprintsDescription": "Aplicar configuraciones declarativas y ver ejecuciones anteriores",
"blueprintAdd": "Añadir plano",
"blueprintGoBack": "Ver todos los Planos",
"blueprintCreate": "Crear Plano",
"blueprintCreateDescription2": "Siga los siguientes pasos para crear y aplicar un nuevo plano",
"blueprintDetails": "Detalles del plano",
"blueprintDetailsDescription": "Ver los detalles de la ejecución del plano",
"blueprintDetailsDescription": "Ver el resultado del plano aplicado y cualquier error que haya ocurrido",
"blueprintInfo": "Información del plano",
"message": "Mensaje",
"blueprintContentsDescription": "Defina el contenido YAML describiendo su infraestructura",
@@ -1181,7 +1181,7 @@
"appliedAt": "Aplicado en",
"source": "Fuente",
"contents": "Contenido",
"parsedContents": "Contenido analizado",
"parsedContents": "Contenido analizado (Sólo lectura)",
"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.",
"enableDockerSocketLink": "Saber más",
@@ -1279,6 +1279,15 @@
"settingsErrorUpdateDescription": "Ocurrió un error al actualizar ajustes",
"sidebarCollapse": "Colapsar",
"sidebarExpand": "Expandir",
"productUpdateMoreInfo": "{noOfUpdates} actualizaciones más",
"productUpdateInfo": "{noOfUpdates} actualizaciones",
"productUpdateWhatsNew": "Novedades",
"productUpdateTitle": "Actualizaciones de producto",
"productUpdateEmpty": "Sin actualizaciones",
"dismissAll": "Descartar todo",
"pangolinUpdateAvailable": "Nueva versión disponible",
"pangolinUpdateAvailableInfo": "La versión {version} está lista para instalar",
"pangolinUpdateAvailableReleaseNotes": "Ver notas del lanzamiento",
"newtUpdateAvailable": "Nueva actualización disponible",
"newtUpdateAvailableInfo": "Hay una nueva versión de Newt disponible. Actualice a la última versión para la mejor experiencia.",
"domainPickerEnterDomain": "Dominio",
@@ -1525,6 +1534,12 @@
"resourcesTableTheseResourcesForUseWith": "Estos recursos son para uso con",
"resourcesTableClients": "Clientes",
"resourcesTableAndOnlyAccessibleInternally": "y solo son accesibles internamente cuando se conectan con un cliente.",
"resourcesTableNoTargets": "Sin objetivos",
"resourcesTableHealthy": "Saludable",
"resourcesTableDegraded": "Degrado",
"resourcesTableOffline": "Desconectado",
"resourcesTableUnknown": "Desconocido",
"resourcesTableNotMonitored": "No supervisado",
"editInternalResourceDialogEditClientResource": "Editar recurso del cliente",
"editInternalResourceDialogUpdateResourceProperties": "Actualizar las propiedades del recurso y la configuración del objetivo para {resourceName}.",
"editInternalResourceDialogResourceProperties": "Propiedades del recurso",
@@ -2080,5 +2095,46 @@
"supportSending": "Enviando...",
"supportSend": "Enviar",
"supportMessageSent": "¡Mensaje enviado!",
"supportWillContact": "¡Estaremos en contacto en breve!"
"supportWillContact": "¡Estaremos en contacto en breve!",
"selectLogRetention": "Seleccionar retención de registro",
"showColumns": "Mostrar columnas",
"hideColumns": "Ocultar columnas",
"columnVisibility": "Visibilidad de la columna",
"toggleColumn": "Cambiar columna {columnName}",
"allColumns": "Todas las columnas",
"defaultColumns": "Columnas por defecto",
"customizeView": "Personalizar vista",
"viewOptions": "Ver opciones",
"selectAll": "Seleccionar todo",
"selectNone": "No seleccionar",
"selectedResources": "Recursos seleccionados",
"enableSelected": "Habilitar seleccionados",
"disableSelected": "Desactivar Seleccionado",
"checkSelectedStatus": "Comprobar el estado de selección",
"credentials": "Credenciales",
"savecredentials": "Guardar credenciales",
"regeneratecredentials": "Re-clave",
"regenerateCredentials": "Regenerar y guardar tus credenciales",
"generatedcredentials": "Credenciales generadas",
"copyandsavethesecredentials": "Copiar y guardar estas credenciales",
"copyandsavethesecredentialsdescription": "Estas credenciales no se mostrarán de nuevo después de salir de esta página. Guárdelas de forma segura ahora.",
"credentialsSaved": "Credenciales guardadas",
"credentialsSavedDescription": "Las credenciales se han regenerado y guardado correctamente.",
"credentialsSaveError": "Error al guardar las credenciales",
"credentialsSaveErrorDescription": "Se ha producido un error al regenerar y guardar las credenciales.",
"regenerateCredentialsWarning": "Regenerar las credenciales invalidará las anteriores. Asegúrese de actualizar cualquier configuración que use estas credenciales.",
"confirm": "Confirmar",
"regenerateCredentialsConfirmation": "¿Está seguro que desea regenerar las credenciales?",
"endpoint": "Endpoint",
"Id": "Id",
"SecretKey": "Clave secreta",
"featureDisabledTooltip": "Esta característica sólo está disponible en el plan empresarial y requiere una licencia para usarla.",
"niceId": "ID bonita",
"niceIdUpdated": "Bonito ID actualizado",
"niceIdUpdatedSuccessfully": "Bonito ID actualizado correctamente",
"niceIdUpdateError": "Error al actualizar Nice ID",
"niceIdUpdateErrorDescription": "Se ha producido un error al actualizar el ID de Niza.",
"niceIdCannotBeEmpty": "El ID de Niza no puede estar vacío",
"enterIdentifier": "Introducir identificador",
"identifier": "Identifier"
}

View File

@@ -1,76 +1,76 @@
{
"setupCreate": "Créez votre organisation, votre site et vos ressources",
"setupCreate": "Créez votre organisation, vos nœuds et vos ressources",
"setupNewOrg": "Nouvelle organisation",
"setupCreateOrg": "Créer une organisation",
"setupCreateResources": "Créer des ressources",
"setupOrgName": "Nom de l'organisation",
"orgDisplayName": "Ceci est le nom d'affichage de votre organisation.",
"orgDisplayName": "Ceci est le nom affiché de votre organisation.",
"orgId": "ID de l'organisation",
"setupIdentifierMessage": "Ceci est l'identifiant unique pour votre organisation. Il est séparé du nom affiché.",
"setupErrorIdentifier": "L'ID de l'organisation est déjà pris. Veuillez en choisir un autre.",
"setupIdentifierMessage": "Ceci est l'identifiant unique de votre organisation. Il est différent du nom.",
"setupErrorIdentifier": "Cet ID est déjà utilisé. Veuillez en choisir un autre.",
"componentsErrorNoMemberCreate": "Vous n'êtes actuellement membre d'aucune organisation. Créez une organisation pour commencer.",
"componentsErrorNoMember": "Vous n'êtes actuellement membre d'aucune organisation.",
"welcome": "Bienvenue à Pangolin",
"welcome": "Bienvenue sur Pangolin !",
"welcomeTo": "Bienvenue chez",
"componentsCreateOrg": "Créer une organisation",
"componentsMember": "Vous êtes membre de {count, plural, =0 {aucune organisation} one {une organisation} other {# organisations}}.",
"componentsInvalidKey": "Clés de licence invalides ou expirées détectées. Suivez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
"dismiss": "Refuser",
"componentsLicenseViolation": "Violation de licence : Ce serveur utilise des sites {usedSites} qui dépassent la limite autorisée des sites {maxSites} . Suivez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
"componentsMember": "Vous {count, plural, =0 {n'} other {} }êtes membre {count, plural, =0 {d'aucune organisation} one {d'une organisation} other {de # organisations}}.",
"componentsInvalidKey": "Clés de licence invalides ou expirées détectées. Veuillez respecter les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
"dismiss": "Rejeter",
"componentsLicenseViolation": "Violation de licence : ce serveur utilise {usedSites} nœuds, ce qui dépasse la limite autorisée de {maxSites} nœuds. Respectez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
"componentsSupporterMessage": "Merci de soutenir Pangolin en tant que {tier}!",
"inviteErrorNotValid": "Nous sommes désolés, mais il semble que l'invitation que vous essayez d'accéder n'ait pas été acceptée ou n'est plus valide.",
"inviteErrorUser": "Nous sommes désolés, mais il semble que l'invitation que vous essayez d'accéder ne soit pas pour cet utilisateur.",
"inviteLoginUser": "Assurez-vous que vous êtes bien connecté en tant qu'utilisateur correct.",
"inviteErrorNoUser": "Nous sommes désolés, mais il semble que l'invitation que vous essayez d'accéder ne soit pas pour un utilisateur qui existe.",
"inviteErrorNotValid": "Nous sommes désolés, mais il semble que l'invitation à laquelle vous essayez d'accéder n'ait pas été acceptée ou ne soit plus valide.",
"inviteErrorUser": "Nous sommes désolés, mais il semble que l'invitation à laquelle vous essayez d'accéder ne soit pas pour cet utilisateur.",
"inviteLoginUser": "Veuillez vous assurer que vous êtes connecté avec le bon compte.",
"inviteErrorNoUser": "Nous sommes désolés, mais il semble que l'invitation à laquelle vous essayez d'accéder ne concerne pas un utilisateur existant.",
"inviteCreateUser": "Veuillez d'abord créer un compte.",
"goHome": "Retour à la maison",
"goHome": "Retour à l'accueil",
"inviteLogInOtherUser": "Se connecter en tant qu'utilisateur différent",
"createAnAccount": "Créer un compte",
"inviteNotAccepted": "Invitation non acceptée",
"authCreateAccount": "Créez un compte pour commencer",
"authNoAccount": "Vous n'avez pas de compte ?",
"email": "Courriel",
"email": "Adresse mail",
"password": "Mot de passe",
"confirmPassword": "Confirmer le mot de passe",
"createAccount": "Créer un compte",
"viewSettings": "Afficher les paramètres",
"delete": "Supprimez",
"delete": "Supprimer",
"name": "Nom",
"online": "En ligne",
"offline": "Hors ligne",
"site": "Site",
"dataIn": "Données dans",
"dataOut": "Données épuisées",
"site": "Nœud",
"dataIn": "Données reçues",
"dataOut": "Données émises",
"connectionType": "Type de connexion",
"tunnelType": "Type de tunnel",
"local": "Locale",
"edit": "Editer",
"siteConfirmDelete": "Confirmer la suppression du site",
"siteDelete": "Supprimer le site",
"siteMessageRemove": "Une fois supprimé, le site ne sera plus accessible. Toutes les cibles associées au site seront également supprimées.",
"siteQuestionRemove": "Êtes-vous sûr de vouloir supprimer le site de l'organisation ?",
"siteManageSites": "Gérer les sites",
"siteDescription": "Autoriser la connectivité à votre réseau via des tunnels sécurisés",
"siteCreate": "Créer un site",
"siteCreateDescription2": "Suivez les étapes ci-dessous pour créer et connecter un nouveau site",
"siteCreateDescription": "Créez un nouveau site pour commencer à connecter vos ressources",
"edit": "Modifier",
"siteConfirmDelete": "Confirmer la suppression du nœud",
"siteDelete": "Supprimer le nœud",
"siteMessageRemove": "Une fois supprimé, le nœud ne sera plus accessible. Toutes les cibles associées au nœud seront également supprimées.",
"siteQuestionRemove": "Êtes-vous sûr de vouloir supprimer ce nœud de l'organisation ?",
"siteManageSites": "Gérer les nœuds",
"siteDescription": "Autoriser la connexion à votre réseau via des tunnels sécurisés",
"siteCreate": "Créer un nœud",
"siteCreateDescription2": "Suivez les étapes ci-dessous pour créer et connecter un nouveau nœud",
"siteCreateDescription": "Créez un nouveau nœud pour commencer à connecter vos ressources",
"close": "Fermer",
"siteErrorCreate": "Erreur lors de la création du site",
"siteErrorCreateKeyPair": "Paire de clés ou site par défaut introuvable",
"siteErrorCreateDefaults": "Les valeurs par défaut du site sont introuvables",
"siteErrorCreate": "Erreur lors de la création du nœud",
"siteErrorCreateKeyPair": "Clés ou nœud par défaut introuvable",
"siteErrorCreateDefaults": "Les valeurs par défaut du nœud sont introuvables",
"method": "Méthode",
"siteMethodDescription": "C'est ainsi que vous exposerez les connexions.",
"siteLearnNewt": "Apprenez à installer Newt sur votre système",
"siteSeeConfigOnce": "Vous ne pourrez voir la configuration qu'une seule fois.",
"siteLoadWGConfig": "Chargement de la configuration WireGuard...",
"siteDocker": "Développer les détails du déploiement Docker",
"siteDocker": "Développer pour obtenir plus de détails sur le déploiement Docker",
"toggle": "Activer/désactiver",
"dockerCompose": "Composition Docker",
"dockerRun": "Exécution Docker",
"siteLearnLocal": "Les sites locaux ne tunnel, en savoir plus",
"dockerCompose": "Docker Compose",
"dockerRun": "Docker Run",
"siteLearnLocal": "Les nœuds locaux ne font pas de tunnel, en savoir plus",
"siteConfirmCopy": "J'ai copié la configuration",
"searchSitesProgress": "Rechercher des sites...",
"siteAdd": "Ajouter un site",
"searchSitesProgress": "Rechercher des nœuds...",
"siteAdd": "Ajouter un nœud",
"siteInstallNewt": "Installer Newt",
"siteInstallNewtDescription": "Faites fonctionner Newt sur votre système",
"WgConfiguration": "Configuration WireGuard",
@@ -78,41 +78,41 @@
"operatingSystem": "Système d'exploitation",
"commands": "Commandes",
"recommended": "Recommandé",
"siteNewtDescription": "Pour une meilleure expérience d'utilisateur, utilisez Newt. Il utilise WireGuard sous le capot et vous permet d'adresser vos ressources privées par leur adresse LAN sur votre réseau privé à partir du tableau de bord Pangolin.",
"siteNewtDescription": "Pour une meilleure expérience d'utilisateur, utilisez Newt. Newt se base sur le protocole WireGuard et vous permet de vous connecter à vos ressources privées, par leur adresse LAN sur votre réseau privé, à partir de Pangolin.",
"siteRunsInDocker": "Exécute dans Docker",
"siteRunsInShell": "Exécute en shell sur macOS, Linux et Windows",
"siteErrorDelete": "Erreur lors de la suppression du site",
"siteErrorUpdate": "Impossible de mettre à jour le site",
"siteErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour du site.",
"siteUpdated": "Site mis à jour",
"siteUpdatedDescription": "Le site a été mis à jour.",
"siteGeneralDescription": "Configurer les paramètres généraux de ce site",
"siteSettingDescription": "Configurer les paramètres de votre site",
"siteSetting": "Réglages {siteName}",
"siteRunsInShell": "Fonctionne depuis le shell sur macOS, Linux et Windows",
"siteErrorDelete": "Erreur lors de la suppression du nœud",
"siteErrorUpdate": "Impossible de mettre à jour le nœud",
"siteErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour du nœud.",
"siteUpdated": "Nœud mis à jour",
"siteUpdatedDescription": "Le nœud a été mis à jour.",
"siteGeneralDescription": "Configurer les paramètres par défaut de ce nœud",
"siteSettingDescription": "Configurer les paramètres de votre nœud",
"siteSetting": "Paramètres de {siteName}",
"siteNewtTunnel": "Tunnel Newt (Recommandé)",
"siteNewtTunnelDescription": "La façon la plus simple de créer un point d'entrée dans votre réseau. Pas de configuration supplémentaire.",
"siteWg": "WireGuard basique",
"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.",
"siteLocalDescription": "Ressources locales seulement. Pas de tunneling.",
"siteLocalDescriptionSaas": "Ressources locales uniquement. Pas de tunneling. Disponible uniquement sur les nœuds distants.",
"siteSeeAll": "Voir tous les sites",
"siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre site",
"siteSeeAll": "Voir tous les nœuds",
"siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre nœud",
"siteNewtCredentials": "Identifiants Newt",
"siteNewtCredentialsDescription": "C'est ainsi que Newt s'authentifiera avec le serveur",
"siteNewtCredentialsDescription": "C'est comme ça que Newt s'authentifiera avec le serveur",
"siteCredentialsSave": "Enregistrez vos identifiants",
"siteCredentialsSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Assurez-vous de le copier dans un endroit sécurisé.",
"siteInfo": "Informations sur le site",
"siteCredentialsSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Assurez-vous de l'enregistrer dans un endroit sécurisé.",
"siteInfo": "Informations du nœud",
"status": "Statut",
"shareTitle": "Gérer les liens de partage",
"shareTitle": "Gérer les liens partageables",
"shareDescription": "Créez des liens partageables pour accorder un accès temporaire ou permanent à vos ressources",
"shareSearch": "Rechercher des liens de partage...",
"shareCreate": "Créer un lien de partage",
"shareSearch": "Rechercher des liens partageables...",
"shareCreate": "Créer un lien partageable",
"shareErrorDelete": "Impossible de supprimer le lien",
"shareErrorDeleteMessage": "Une erreur s'est produite lors de la suppression du lien",
"shareDeleted": "Lien supprimé",
"shareDeletedDescription": "Le lien a été supprimé",
"shareTokenDescription": "Votre jeton d'accès peut être passé de deux façons : en tant que paramètre de requête ou dans les en-têtes de la requête. Elles doivent être transmises par le client à chaque demande d'accès authentifié.",
"shareTokenDescription": "Votre jeton d'accès peut être fourni de deux façons : en tant que paramètre de requête ou dans les en-têtes de la requête. Il doit être transmis par le client à chaque demande d'accès authentifié.",
"accessToken": "Jeton d'accès",
"usageExamples": "Exemples d'utilisation",
"tokenId": "ID du jeton",
@@ -124,16 +124,16 @@
"shareTokenSecurety": "Gardez votre jeton d'accès sécurisé. Ne le partagez pas dans des zones accessibles au public ou dans du code côté client.",
"shareErrorFetchResource": "Impossible de récupérer les ressources",
"shareErrorFetchResourceDescription": "Une erreur est survenue lors de la récupération des ressources",
"shareErrorCreate": "Impossible de créer le lien de partage",
"shareErrorCreateDescription": "Une erreur s'est produite lors de la création du lien de partage",
"shareErrorCreate": "Impossible de créer le lien partageable",
"shareErrorCreateDescription": "Une erreur s'est produite lors de la création du lien partageable",
"shareCreateDescription": "N'importe qui avec ce lien peut accéder à la ressource",
"shareTitleOptional": "Titre (facultatif)",
"expireIn": "Expire dans",
"neverExpire": "N'expire jamais",
"shareExpireDescription": "Le temps d'expiration est combien de temps le lien sera utilisable et fournira un accès à la ressource. Après cette période, le lien ne fonctionnera plus et les utilisateurs qui ont utilisé ce lien perdront l'accès à la ressource.",
"shareSeeOnce": "Vous ne pourrez voir ce lien. Assurez-vous de le copier.",
"shareAccessHint": "N'importe qui avec ce lien peut accéder à la ressource. Partagez-le avec soin.",
"shareTokenUsage": "Voir Utilisation du jeton d'accès",
"shareExpireDescription": "Le délai d'expiration correspond à la période pendant laquelle le lien sera utilisable et permettra d'accéder à la ressource. Passé ce délai, le lien ne fonctionnera plus et les utilisateurs qui l'ont utilisé perdront l'accès à la ressource.",
"shareSeeOnce": "Vous ne pourrez voir ce lien qu'une seule fois. N'oubliez pas de le copier.",
"shareAccessHint": "N'importe qui avec ce lien peut accéder à la ressource. Partagez-le avec précaution.",
"shareTokenUsage": "Voir l'utilisation du jeton d'accès",
"createLink": "Créer un lien",
"resourcesNotFound": "Aucune ressource trouvée",
"resourceSearch": "Rechercher des ressources",
@@ -145,43 +145,43 @@
"never": "Jamais",
"shareErrorSelectResource": "Veuillez sélectionner une ressource",
"resourceTitle": "Gérer les ressources",
"resourceDescription": "Créez des proxy sécurisés pour vos applications privées",
"resourcesSearch": "Rechercher des ressources...",
"resourceDescription": "Créez des proxys sécurisés pour vos applications privées",
"resourcesSearch": "Chercher des ressources...",
"resourceAdd": "Ajouter une ressource",
"resourceErrorDelte": "Erreur de suppression de la ressource",
"resourceErrorDelte": "Erreur lors de la de suppression de la ressource",
"authentication": "Authentification",
"protected": "Protégé",
"notProtected": "Non Protégé",
"resourceMessageRemove": "Une fois supprimée, la ressource ne sera plus accessible. Toutes les cibles associées à la ressource seront également supprimées.",
"resourceQuestionRemove": "Êtes-vous sûr de vouloir supprimer la ressource de l'organisation ?",
"resourceQuestionRemove": "Êtes-vous sûr de vouloir retirer la ressource de l'organisation ?",
"resourceHTTP": "Ressource HTTPS",
"resourceHTTPDescription": "Requêtes de proxy à votre application via HTTPS en utilisant un sous-domaine ou un domaine de base.",
"resourceHTTPDescription": "Requêtes de proxy vers votre application via HTTPS en utilisant un sous-domaine ou un domaine racine.",
"resourceRaw": "Ressource TCP/UDP brute",
"resourceRawDescription": "Demandes de proxy à votre application via TCP/UDP en utilisant un numéro de port.",
"resourceRawDescription": "Demandes de proxy vers votre application via TCP/UDP en utilisant un port.",
"resourceCreate": "Créer une ressource",
"resourceCreateDescription": "Suivez les étapes ci-dessous pour créer une nouvelle ressource",
"resourceSeeAll": "Voir toutes les ressources",
"resourceInfo": "Informations sur la ressource",
"resourceNameDescription": "Ceci est le nom d'affichage de la ressource.",
"siteSelect": "Sélectionner un site",
"siteSearch": "Chercher un site",
"siteNotFound": "Aucun site trouvé.",
"siteSelect": "Sélectionnez un nœud",
"siteSearch": "Chercher un nœud",
"siteNotFound": "Aucun nœud trouvé.",
"selectCountry": "Sélectionnez un pays",
"searchCountries": "Recherchez des pays...",
"noCountryFound": "Aucun pays trouvé.",
"siteSelectionDescription": "Ce site fournira la connectivité à la cible.",
"resourceType": "Type de ressource",
"resourceTypeDescription": "Déterminer comment vous voulez accéder à votre ressource",
"resourceTypeDescription": "Détermine comment vous voulez accéder à votre ressource",
"resourceHTTPSSettings": "Paramètres HTTPS",
"resourceHTTPSSettingsDescription": "Configurer comment votre ressource sera accédée via HTTPS",
"resourceHTTPSSettingsDescription": "Configure comment votre ressource sera accédée via HTTPS",
"domainType": "Type de domaine",
"subdomain": "Sous-domaine",
"baseDomain": "Domaine de base",
"subdomnainDescription": "Le sous-domaine où votre ressource sera accessible.",
"baseDomain": "Domaine racine",
"subdomnainDescription": "Le sous-domaine depuis lequel cette ressource sera accessible.",
"resourceRawSettings": "Paramètres TCP/UDP",
"resourceRawSettingsDescription": "Configurer comment votre ressource sera accédée via TCP/UDP",
"resourceRawSettingsDescription": "Configurer comment votre ressource sera accédée via TCP/UDP. Vous mappez la ressource à un port sur le serveur Pangolin, de sorte que vous puissiez accéder à la ressource depuis ip-publique-du-serveur:port-mappé.",
"protocol": "Protocole",
"protocolSelect": "Sélectionner un protocole",
"protocolSelect": "Choisir un protocole",
"resourcePortNumber": "Numéro de port",
"resourcePortNumberDescription": "Le numéro de port externe pour les requêtes de proxy.",
"cancel": "Abandonner",
@@ -203,17 +203,17 @@
"internal": "Interne",
"rules": "Règles",
"resourceSettingDescription": "Configurer les paramètres de votre ressource",
"resourceSetting": "Réglages {resourceName}",
"resourceSetting": "Réglages de {resourceName}",
"alwaysAllow": "Toujours autoriser",
"alwaysDeny": "Toujours refuser",
"passToAuth": "Paser à l'authentification",
"orgSettingsDescription": "Configurer les paramètres généraux de votre organisation",
"passToAuth": "Passer à l'authentification",
"orgSettingsDescription": "Configurer les paramètres de votre organisation",
"orgGeneralSettings": "Paramètres de l'organisation",
"orgGeneralSettingsDescription": "Gérer les détails et la configuration de votre organisation",
"saveGeneralSettings": "Enregistrer les paramètres généraux",
"saveSettings": "Enregistrer les paramètres",
"orgDangerZone": "Zone de danger",
"orgDangerZoneDescription": "Une fois que vous supprimez cette organisation, il n'y a pas de retour en arrière. Soyez certain.",
"orgDangerZone": "Zone dangereuse",
"orgDangerZoneDescription": "Une fois cette organisation supprimée, elle ne pourra plus être restaurée. Faites attention.",
"orgDelete": "Supprimer l'organisation",
"orgDeleteConfirm": "Confirmer la suppression de l'organisation",
"orgMessageRemove": "Cette action est irréversible et supprimera toutes les données associées.",
@@ -224,7 +224,7 @@
"orgErrorUpdate": "Échec de la mise à jour de l'organisation",
"orgErrorUpdateMessage": "Une erreur s'est produite lors de la mise à jour de l'organisation.",
"orgErrorFetch": "Impossible de récupérer les organisations",
"orgErrorFetchMessage": "Une erreur s'est produite lors de la liste de vos organisations",
"orgErrorFetchMessage": "Une erreur s'est produite lors de la récupération des organisations",
"orgErrorDelete": "Échec de la suppression de l'organisation",
"orgErrorDeleteMessage": "Une erreur s'est produite lors de la suppression de l'organisation.",
"orgDeleted": "Organisation supprimée",
@@ -233,21 +233,21 @@
"orgMissingMessage": "Impossible de régénérer l'invitation sans un ID d'organisation.",
"accessUsersManage": "Gérer les utilisateurs",
"accessUsersDescription": "Invitez des utilisateurs et ajoutez-les aux rôles pour gérer l'accès à votre organisation",
"accessUsersSearch": "Rechercher des utilisateurs...",
"accessUsersSearch": "Chercher des utilisateurs...",
"accessUserCreate": "Créer un utilisateur",
"accessUserRemove": "Supprimer l'utilisateur",
"accessUserRemove": "Supprimer un utilisateur",
"username": "Nom d'utilisateur",
"identityProvider": "Fournisseur d'identité",
"role": "Rôle",
"nameRequired": "Le nom est requis",
"accessRolesManage": "Gérer les rôles",
"accessRolesDescription": "Configurer les rôles pour gérer l'accès à votre organisation",
"accessRolesSearch": "Rechercher des rôles...",
"accessRolesSearch": "Chercher des rôles...",
"accessRolesAdd": "Ajouter un rôle",
"accessRoleDelete": "Supprimer le rôle",
"description": "Libellé",
"inviteTitle": "Invitations ouvertes",
"inviteDescription": "Gérer vos invitations à d'autres utilisateurs",
"inviteTitle": "Invitations actives",
"inviteDescription": "Gérez les invitations des autres utilisateurs",
"inviteSearch": "Rechercher des invitations...",
"minutes": "Minutes",
"hours": "Heures",
@@ -256,41 +256,41 @@
"months": "Mois",
"years": "Années",
"day": "{count, plural, one {# jour} other {# jours}}",
"apiKeysTitle": "Informations sur la clé API",
"apiKeysConfirmCopy2": "Vous devez confirmer que vous avez copié la clé API.",
"apiKeysTitle": "Informations sur la clé d'API",
"apiKeysConfirmCopy2": "Vous devez confirmer que vous avez copié la clé d'API.",
"apiKeysErrorCreate": "Erreur lors de la création de la clé API",
"apiKeysErrorSetPermission": "Erreur lors de la définition des permissions",
"apiKeysCreate": "Générer une clé API",
"apiKeysCreateDescription": "Générer une nouvelle clé API pour votre organisation",
"apiKeysCreate": "Générer une clé d'API",
"apiKeysCreateDescription": "Générer une nouvelle clé d'API pour votre organisation",
"apiKeysGeneralSettings": "Permissions",
"apiKeysGeneralSettingsDescription": "Déterminez ce que cette clé API peut faire",
"apiKeysList": "Votre clé API",
"apiKeysGeneralSettingsDescription": "Déterminez ce que cette clé d\"API peut faire",
"apiKeysList": "Votre clé d\"API",
"apiKeysSave": "Enregistrer votre clé API",
"apiKeysSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Assurez-vous de la copier dans un endroit sécurisé.",
"apiKeysInfo": "Votre clé API est :",
"apiKeysConfirmCopy": "J'ai copié la clé API",
"apiKeysSaveDescription": "Vous ne pourrez la voir qu'une seule fois. Assurez-vous de la copier dans un endroit sécurisé.",
"apiKeysInfo": "Votre clé d'API est :",
"apiKeysConfirmCopy": "J'ai copié la clé d\"API",
"generate": "Générer",
"done": "Terminé",
"apiKeysSeeAll": "Voir toutes les clés API",
"apiKeysPermissionsErrorLoadingActions": "Erreur lors du chargement des actions de la clé API",
"apiKeysSeeAll": "Voir toutes les clés d\"API",
"apiKeysPermissionsErrorLoadingActions": "Erreur lors du chargement des actions de la clé d\"API",
"apiKeysPermissionsErrorUpdate": "Erreur lors de la définition des permissions",
"apiKeysPermissionsUpdated": "Permissions mises à jour",
"apiKeysPermissionsUpdatedDescription": "Les permissions ont été mises à jour.",
"apiKeysPermissionsGeneralSettings": "Permissions",
"apiKeysPermissionsGeneralSettingsDescription": "Déterminez ce que cette clé API peut faire",
"apiKeysPermissionsGeneralSettingsDescription": "Déterminez ce que cette clé d'API peut faire",
"apiKeysPermissionsSave": "Enregistrer les permissions",
"apiKeysPermissionsTitle": "Permissions",
"apiKeys": "Clés API",
"searchApiKeys": "Rechercher des clés API...",
"apiKeysAdd": "Générer une clé API",
"apiKeysErrorDelete": "Erreur lors de la suppression de la clé API",
"apiKeysErrorDeleteMessage": "Erreur lors de la suppression de la clé API",
"apiKeysQuestionRemove": "Êtes-vous sûr de vouloir supprimer la clé API de l'organisation ?",
"apiKeysMessageRemove": "Une fois supprimée, la clé API ne pourra plus être utilisée.",
"apiKeysDeleteConfirm": "Confirmer la suppression de la clé API",
"apiKeysDelete": "Supprimer la clé API",
"apiKeysManage": "Gérer les clés API",
"apiKeysDescription": "Les clés API sont utilisées pour s'authentifier avec l'API d'intégration",
"apiKeys": "Clés d'API",
"searchApiKeys": "Rechercher des clés d'API...",
"apiKeysAdd": "Générer une clé d'API",
"apiKeysErrorDelete": "Erreur lors de la suppression de la clé d'API",
"apiKeysErrorDeleteMessage": "Erreur lors de la suppression de la clé d'API",
"apiKeysQuestionRemove": "Êtes-vous sûr de vouloir supprimer la clé d'API de l'organisation ?",
"apiKeysMessageRemove": "Une fois supprimée, la clé d'API ne pourra plus être utilisée.",
"apiKeysDeleteConfirm": "Confirmer la suppression de la clé d'API",
"apiKeysDelete": "Supprimer la clé d'API",
"apiKeysManage": "Gérer les clés d'API",
"apiKeysDescription": "Les clés d'API sont utilisées pour s'authentifier avec l'API d'intégration",
"apiKeysSettings": "Paramètres de {apiKeyName}",
"userTitle": "Gérer tous les utilisateurs",
"userDescription": "Voir et gérer tous les utilisateurs du système",
@@ -305,10 +305,10 @@
"userQuestionRemove": "Êtes-vous sûr de vouloir supprimer définitivement l'utilisateur du serveur?",
"licenseKey": "Clé de licence",
"valid": "Valide",
"numberOfSites": "Nombre de sites",
"numberOfSites": "Nombre de nœuds",
"licenseKeySearch": "Rechercher des clés de licence...",
"licenseKeyAdd": "Ajouter une clé de licence",
"type": "Type de texte",
"type": "Type",
"licenseKeyRequired": "La clé de licence est requise",
"licenseTermsAgree": "Vous devez accepter les conditions de licence",
"licenseErrorKeyLoad": "Impossible de charger les clés de licence",
@@ -342,7 +342,7 @@
"licenseTitleDescription": "Voir et gérer les clés de licence dans le système",
"licenseHost": "Licence Hôte",
"licenseHostDescription": "Gérer la clé de licence principale de l'hôte.",
"licensedNot": "Non licenc",
"licensedNot": "Pas de licence",
"hostId": "ID de l'hôte",
"licenseReckeckAll": "Revérifier toutes les clés",
"licenseSiteUsage": "Utilisation des sites",
@@ -350,7 +350,7 @@
"licenseNoSiteLimit": "Il n'y a pas de limite sur le nombre de sites utilisant un hôte non autorisé.",
"licensePurchase": "Acheter une licence",
"licensePurchaseSites": "Acheter des sites supplémentaires",
"licenseSitesUsedMax": "{usedSites} des sites {maxSites} utilisés",
"licenseSitesUsedMax": "{usedSites} des {maxSites} sites utilisés",
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}} dans le système.",
"licensePurchaseDescription": "Choisissez le nombre de sites que vous voulez {selectedMode, select, license {achetez une licence. Vous pouvez toujours ajouter plus de sites plus tard.} other {ajouter à votre licence existante.}}",
"licenseFee": "Frais de licence",
@@ -371,7 +371,7 @@
"inviteQuestionRemove": "Êtes-vous sûr de vouloir supprimer l'invitation?",
"inviteMessageRemove": "Une fois supprimée, cette invitation ne sera plus valide. Vous pourrez toujours réinviter l'utilisateur plus tard.",
"inviteMessageConfirm": "Pour confirmer, veuillez saisir l'adresse e-mail de l'invitation ci-dessous.",
"inviteQuestionRegenerate": "Êtes-vous sûr de vouloir régénérer l'invitation {email}? Cela révoquera l'invitation précédente.",
"inviteQuestionRegenerate": "Êtes-vous sûr de vouloir régénérer l'invitation pour {email}? Cela révoquera l'invitation précédente.",
"inviteRemoveConfirm": "Confirmer la suppression de l'invitation",
"inviteRegenerated": "Invitation régénérée",
"inviteSent": "Une nouvelle invitation a été envoyée à {email}.",
@@ -465,7 +465,7 @@
"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",
"proxyEnableSSLDescription": "Activez le cryptage SSL/TLS pour des connexions HTTPS sécurisées vers vos cibles.",
"target": "Target",
"target": "Cible",
"configureTarget": "Configurer les cibles",
"targetErrorFetch": "Échec de la récupération des cibles",
"targetErrorFetchDescription": "Une erreur s'est produite lors de la récupération des cibles",
@@ -611,7 +611,7 @@
"newtId": "ID Newt",
"newtSecretKey": "Clé secrète Newt",
"architecture": "Architecture",
"sites": "Espaces",
"sites": "Nœuds",
"siteWgAnyClients": "Utilisez n'importe quel client WireGuard pour vous connecter. Vous devrez adresser vos ressources internes en utilisant l'IP du pair.",
"siteWgCompatibleAllClients": "Compatible avec tous les clients WireGuard",
"siteWgManualConfigurationRequired": "Configuration manuelle requise",
@@ -1021,7 +1021,7 @@
"actionDeleteSite": "Supprimer un site",
"actionGetSite": "Obtenir un site",
"actionListSites": "Lister les sites",
"actionApplyBlueprint": "Appliquer le Plan",
"actionApplyBlueprint": "Appliquer la Config",
"setupToken": "Jeton de configuration",
"setupTokenDescription": "Entrez le jeton de configuration depuis la console du serveur.",
"setupTokenRequired": "Le jeton de configuration est requis.",
@@ -1149,7 +1149,7 @@
"apiKeysErrorNoUpdate": "Pas de clé API à mettre à jour",
"sidebarOverview": "Aperçu",
"sidebarHome": "Domicile",
"sidebarSites": "Espaces",
"sidebarSites": "Nœuds",
"sidebarResources": "Ressource",
"sidebarAccessControl": "Contrôle d'accès",
"sidebarUsers": "Utilisateurs",
@@ -1163,26 +1163,26 @@
"sidebarLicense": "Licence",
"sidebarClients": "Clients",
"sidebarDomains": "Domaines",
"sidebarBluePrints": "Plans",
"blueprints": "Plans",
"blueprintsDescription": "Les plans sont des configurations YAML déclaratives qui définissent vos ressources et leurs paramètres",
"blueprintAdd": "Ajouter un Plan",
"blueprintGoBack": "Voir tous les plans",
"blueprintCreate": "Créer un Plan",
"blueprintCreateDescription2": "Suivez les étapes ci-dessous pour créer et appliquer un nouveau plan",
"blueprintDetails": "Détails du Plan",
"blueprintDetailsDescription": "Voir les détails de l'exécution des plans",
"blueprintInfo": "Informations sur le Plan",
"sidebarBluePrints": "Configs",
"blueprints": "Configs",
"blueprintsDescription": "Appliquer les configurations déclaratives et afficher les exécutions précédentes",
"blueprintAdd": "Ajouter une Config",
"blueprintGoBack": "Voir toutes les Configs",
"blueprintCreate": "Créer une Config",
"blueprintCreateDescription2": "Suivez les étapes ci-dessous pour créer et appliquer une nouvelle config",
"blueprintDetails": "Détails de la Config",
"blueprintDetailsDescription": "Voir le résultat du plan appliqué et les erreurs qui se sont produites",
"blueprintInfo": "Informations sur la Config",
"message": "Message",
"blueprintContentsDescription": "Définissez le contenu YAML décrivant votre infrastructure",
"blueprintErrorCreateDescription": "Une erreur s'est produite lors de l'application du plan",
"blueprintErrorCreate": "Erreur lors de la création du plan",
"searchBlueprintProgress": "Rechercher des plans...",
"blueprintErrorCreateDescription": "Une erreur s'est produite lors de l'application de la config",
"blueprintErrorCreate": "Erreur lors de la création de la config",
"searchBlueprintProgress": "Rechercher des configs...",
"appliedAt": "Appliqué à",
"source": "Source",
"contents": "Contenus",
"parsedContents": "Contenu analysé",
"enableDockerSocket": "Activer le Plan Docker",
"parsedContents": "Contenu analysé (lecture seule)",
"enableDockerSocket": "Activer la Config Docker",
"enableDockerSocketDescription": "Activer le ramassage d'étiquettes de socket Docker pour les étiquettes de plan. Le chemin de socket doit être fourni à Newt.",
"enableDockerSocketLink": "En savoir plus",
"viewDockerContainers": "Voir les conteneurs Docker",
@@ -1279,6 +1279,15 @@
"settingsErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour des paramètres",
"sidebarCollapse": "Réduire",
"sidebarExpand": "Développer",
"productUpdateMoreInfo": "{noOfUpdates} mises à jour de plus",
"productUpdateInfo": "{noOfUpdates} mises à jour",
"productUpdateWhatsNew": "Quoi de neuf",
"productUpdateTitle": "Mises à jour",
"productUpdateEmpty": "Aucune mise à jour",
"dismissAll": "Tout cacher",
"pangolinUpdateAvailable": "Mise à jour disponible",
"pangolinUpdateAvailableInfo": "La version {version} est prête à être installée",
"pangolinUpdateAvailableReleaseNotes": "Voir les notes de version",
"newtUpdateAvailable": "Mise à jour disponible",
"newtUpdateAvailableInfo": "Une nouvelle version de Newt est disponible. Veuillez mettre à jour vers la dernière version pour une meilleure expérience.",
"domainPickerEnterDomain": "Domaine",
@@ -1525,6 +1534,12 @@
"resourcesTableTheseResourcesForUseWith": "Ces ressources sont à utiliser avec",
"resourcesTableClients": "Clients",
"resourcesTableAndOnlyAccessibleInternally": "et sont uniquement accessibles en interne lorsqu'elles sont connectées avec un client.",
"resourcesTableNoTargets": "Aucune cible",
"resourcesTableHealthy": "Sain",
"resourcesTableDegraded": "Dégradé",
"resourcesTableOffline": "Hors ligne",
"resourcesTableUnknown": "Inconnu",
"resourcesTableNotMonitored": "Non-monitoré",
"editInternalResourceDialogEditClientResource": "Modifier la ressource client",
"editInternalResourceDialogUpdateResourceProperties": "Mettez à jour les propriétés de la ressource et la configuration de la cible pour {resourceName}.",
"editInternalResourceDialogResourceProperties": "Propriétés de la ressource",
@@ -2041,7 +2056,7 @@
"preferWildcardCertDescription": "Tentative de génération d'un certificat générique (nécessite un résolveur de certificat correctement configuré).",
"recordName": "Nom de l'enregistrement",
"auto": "Automatique",
"TTL": "TTC",
"TTL": "TTL",
"howToAddRecords": "Comment ajouter des enregistrements",
"dnsRecord": "Enregistrements DNS",
"required": "Requis",
@@ -2061,7 +2076,7 @@
"warning": "Avertissement",
"proxyProtocolWarning": "Votre application backend doit être configurée pour accepter les connexions Proxy Protocol. Si votre backend ne prend pas en charge le protocole Proxy, activer ceci va casser toutes les connexions. Assurez-vous de configurer votre backend pour faire confiance aux en-têtes du protocole Proxy de Traefik.",
"restarting": "Redémarrage...",
"manual": "Manuelle",
"manual": "Manuel",
"messageSupport": "Soutien aux messages",
"supportNotAvailableTitle": "Support non disponible",
"supportNotAvailableDescription": "L'assistance n'est pas disponible pour le moment. Vous pouvez envoyer un e-mail à support@pangolin.net.",
@@ -2080,5 +2095,46 @@
"supportSending": "Envoi...",
"supportSend": "Envoyer",
"supportMessageSent": "Message envoyé !",
"supportWillContact": "Nous vous contacterons sous peu!"
"supportWillContact": "Nous vous contacterons sous peu!",
"selectLogRetention": "Sélectionner la durée de rétention des logs",
"showColumns": "Afficher les colonnes",
"hideColumns": "Cacher les colonnes",
"columnVisibility": "Visibilité des colonnes",
"toggleColumn": "Activer/désactiver la colonne {columnName}",
"allColumns": "Toutes les colonnes",
"defaultColumns": "Colonnes par défaut",
"customizeView": "Personnaliser l'apparence",
"viewOptions": "Voir les options",
"selectAll": "Tout sélectionner",
"selectNone": "Ne rien sélectionner",
"selectedResources": "Ressources sélectionnées",
"enableSelected": "Activer la sélection",
"disableSelected": "Désactiver la sélection",
"checkSelectedStatus": "Vérifier le statut de la sélection",
"credentials": "Identifiants",
"savecredentials": "Enregistrer les identifiants",
"regeneratecredentials": "Re-claver",
"regenerateCredentials": "Régénérer et enregistrer les identifiants",
"generatedcredentials": "Identifiants générés",
"copyandsavethesecredentials": "Copier et enregistrer ces identifiants",
"copyandsavethesecredentialsdescription": "Ces identifiants ne seront pas affichés à nouveaux une fois cette page fermée. Enregistrez-les maintenant.",
"credentialsSaved": "Identifiants enregistrés",
"credentialsSavedDescription": "Les identifiants ont été régénérés et enregistrés avec succès.",
"credentialsSaveError": "Erreur lors de l'enregistrement des identifiants",
"credentialsSaveErrorDescription": "Une erreur s'est produite lors de la régénération et l'enregistrement des identifiants.",
"regenerateCredentialsWarning": "La régénération de ces identifiants invalidera ceux actuellement utilisés. Assurez-vous de mettre à jour toutes les configurations qui les utilisent.",
"confirm": "Confirmer",
"regenerateCredentialsConfirmation": "Voulez-vous vraiment régénérer les identifiants ?",
"endpoint": "Endpoint",
"Id": "Id",
"SecretKey": "Clé privée",
"featureDisabledTooltip": "Cette fonctionnalité n'est disponible que dans la version entreprise et nécessite une licence pour être utilisée.",
"niceId": "Joli ID",
"niceIdUpdated": "Joli ID mis à jour",
"niceIdUpdatedSuccessfully": "Joli ID mis à jour avec succès",
"niceIdUpdateError": "Erreur lors de la mise à jour du joli ID",
"niceIdUpdateErrorDescription": "Erreur lors de la mise à jour du joli ID.",
"niceIdCannotBeEmpty": "Merci de renseigner un joli ID",
"enterIdentifier": "Entrez l'identifiant",
"identifier": "Identifiant"
}

View File

@@ -131,7 +131,7 @@
"expireIn": "Scadenza In",
"neverExpire": "Mai scadere",
"shareExpireDescription": "Il tempo di scadenza è per quanto tempo il link sarà utilizzabile e fornirà accesso alla risorsa. Dopo questo tempo, il link non funzionerà più e gli utenti che hanno utilizzato questo link perderanno l'accesso alla risorsa.",
"shareSeeOnce": "Potrai vedere solo questo linkonce. Assicurati di copiarlo.",
"shareSeeOnce": "Potrai vedere questo link solo una volta. Assicurati di copiarlo.",
"shareAccessHint": "Chiunque abbia questo link può accedere alla risorsa. Condividilo con cura.",
"shareTokenUsage": "Vedi Utilizzo Token Di Accesso",
"createLink": "Crea Collegamento",
@@ -179,7 +179,7 @@
"baseDomain": "Dominio Base",
"subdomnainDescription": "Il sottodominio in cui la tua risorsa sarà accessibile.",
"resourceRawSettings": "Impostazioni TCP/UDP",
"resourceRawSettingsDescription": "Configura come accedere alla tua risorsa tramite TCP/UDP",
"resourceRawSettingsDescription": "Configura come sarà possibile accedere alla tua risorsa tramite TCP/UDP. Mappare la risorsa a una porta sul server host Pangolin, in modo da poter accedere alla risorsa dal server-public ip:mapped-port.",
"protocol": "Protocollo",
"protocolSelect": "Seleziona un protocollo",
"resourcePortNumber": "Numero Porta",
@@ -1165,13 +1165,13 @@
"sidebarDomains": "Domini",
"sidebarBluePrints": "Progetti",
"blueprints": "Progetti",
"blueprintsDescription": "I progetti sono configurazioni YAML dichiarative che definiscono le tue risorse e le loro impostazioni",
"blueprintsDescription": "Applica le configurazioni dichiarative e visualizza le partite precedenti",
"blueprintAdd": "Aggiungi Progetto",
"blueprintGoBack": "Vedi tutti i progetti",
"blueprintCreate": "Crea Progetto",
"blueprintCreateDescription2": "Segui i passaggi qui sotto per creare e applicare un nuovo progetto",
"blueprintDetails": "Dettagli progetto",
"blueprintDetailsDescription": "Vedi i dettagli dell'esecuzione del progetto",
"blueprintDetails": "Dettagli Progetto",
"blueprintDetailsDescription": "Vedere il risultato del progetto applicato e gli eventuali errori verificatisi",
"blueprintInfo": "Informazioni Sul Progetto",
"message": "Messaggio",
"blueprintContentsDescription": "Definisci il contenuto di YAML che descrive la tua infrastruttura",
@@ -1181,7 +1181,7 @@
"appliedAt": "Applicato Il",
"source": "Fonte",
"contents": "Contenuti",
"parsedContents": "Sommario Analizzato",
"parsedContents": "Sommario Analizzato (Solo Lettura)",
"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.",
"enableDockerSocketLink": "Scopri di più",
@@ -1279,6 +1279,15 @@
"settingsErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento delle impostazioni",
"sidebarCollapse": "Comprimi",
"sidebarExpand": "Espandi",
"productUpdateMoreInfo": "{noOfUpdates} altri aggiornamenti",
"productUpdateInfo": "{noOfUpdates} aggiornamenti",
"productUpdateWhatsNew": "Novità",
"productUpdateTitle": "Aggiornamenti Prodotto",
"productUpdateEmpty": "Nessun aggiornamento",
"dismissAll": "Ignora tutto",
"pangolinUpdateAvailable": "Nuova versione disponibile",
"pangolinUpdateAvailableInfo": "La versione {version} è pronta per l'installazione",
"pangolinUpdateAvailableReleaseNotes": "Visualizza note di rilascio",
"newtUpdateAvailable": "Aggiornamento Disponibile",
"newtUpdateAvailableInfo": "È disponibile una nuova versione di Newt. Si prega di aggiornare all'ultima versione per la migliore esperienza.",
"domainPickerEnterDomain": "Dominio",
@@ -1525,6 +1534,12 @@
"resourcesTableTheseResourcesForUseWith": "Queste risorse sono per uso con",
"resourcesTableClients": "Client",
"resourcesTableAndOnlyAccessibleInternally": "e sono accessibili solo internamente quando connessi con un client.",
"resourcesTableNoTargets": "Nessun obiettivo",
"resourcesTableHealthy": "Sano",
"resourcesTableDegraded": "Degraded",
"resourcesTableOffline": "Offline",
"resourcesTableUnknown": "Sconosciuto",
"resourcesTableNotMonitored": "Non monitorato",
"editInternalResourceDialogEditClientResource": "Modifica Risorsa Client",
"editInternalResourceDialogUpdateResourceProperties": "Aggiorna le proprietà della risorsa e la configurazione del target per {resourceName}.",
"editInternalResourceDialogResourceProperties": "Proprietà della Risorsa",
@@ -2080,5 +2095,46 @@
"supportSending": "Invio...",
"supportSend": "Invia",
"supportMessageSent": "Messaggio Inviato!",
"supportWillContact": "Saremo in contatto a breve!"
"supportWillContact": "Saremo in contatto a breve!",
"selectLogRetention": "Seleziona ritenzione log",
"showColumns": "Mostra Colonne",
"hideColumns": "Nascondi Colonne",
"columnVisibility": "Visibilità Colonna",
"toggleColumn": "Attiva/disattiva colonna {columnName}",
"allColumns": "Tutte Le Colonne",
"defaultColumns": "Colonne Predefinite",
"customizeView": "Personalizza Vista",
"viewOptions": "Opzioni Visualizzazione",
"selectAll": "Seleziona Tutto",
"selectNone": "Seleziona Nessuno",
"selectedResources": "Risorse Selezionate",
"enableSelected": "Abilita Selezionati",
"disableSelected": "Disabilita Selezionati",
"checkSelectedStatus": "Controlla lo stato dei selezionati",
"credentials": "Credenziali",
"savecredentials": "Salva Credenziali",
"regeneratecredentials": "Ri-chiave",
"regenerateCredentials": "Rigenera e salva le tue credenziali",
"generatedcredentials": "Credenziali Generate",
"copyandsavethesecredentials": "Copia e salva queste credenziali",
"copyandsavethesecredentialsdescription": "Queste credenziali non verranno mostrate di nuovo dopo aver lasciato questa pagina. Salvarle in modo sicuro ora.",
"credentialsSaved": "Credenziali Salvate",
"credentialsSavedDescription": "Le credenziali sono state rigenerate e salvate con successo.",
"credentialsSaveError": "Errore Di Salvataggio Credenziali",
"credentialsSaveErrorDescription": "Errore durante la rigenerazione e il salvataggio delle credenziali.",
"regenerateCredentialsWarning": "Rigenerare le credenziali invaliderà quelle precedenti. Assicurarsi di aggiornare le configurazioni che utilizzano queste credenziali.",
"confirm": "Conferma",
"regenerateCredentialsConfirmation": "Sei sicuro di voler rigenerare le credenziali?",
"endpoint": "Endpoint",
"Id": "Id",
"SecretKey": "Chiave Segreta",
"featureDisabledTooltip": "Questa funzione è disponibile solo nel piano aziendale e richiede una licenza per utilizzarla.",
"niceId": "Simpatico ID",
"niceIdUpdated": "Nice ID Aggiornato",
"niceIdUpdatedSuccessfully": "Nizza Id Aggiornato Con Successo",
"niceIdUpdateError": "Errore nell'aggiornare Nice ID",
"niceIdUpdateErrorDescription": "Si è verificato un errore durante l'aggiornamento del Nice ID.",
"niceIdCannotBeEmpty": "Il Nice ID non può essere vuoto",
"enterIdentifier": "Inserisci identificatore",
"identifier": "Identifier"
}

View File

@@ -179,7 +179,7 @@
"baseDomain": "기본 도메인",
"subdomnainDescription": "리소스에 접근할 수 있는 하위 도메인입니다.",
"resourceRawSettings": "TCP/UDP 설정",
"resourceRawSettingsDescription": "TCP/UDP를 통해 리소스에 접근하는 방법을 구성하세요.",
"resourceRawSettingsDescription": "리소스를 TCP/UDP를 통해 액세스하는 방법을 구성합니다. 리소스를 호스트 Pangolin 서버의 포트에 매핑하여 서버-public-ip:매핑된 포트에서 리소스에 액세스할 수 있습니다.",
"protocol": "프로토콜",
"protocolSelect": "프로토콜 선택",
"resourcePortNumber": "포트 번호",
@@ -1165,13 +1165,13 @@
"sidebarDomains": "도메인",
"sidebarBluePrints": "청사진",
"blueprints": "청사진",
"blueprintsDescription": "청사진은 리소스와 그 설정을 정의하는 선언적인 YAML 구성입니다",
"blueprintsDescription": "선언적 구성을 적용하고 이전 실행을 봅니다",
"blueprintAdd": "청사진 추가",
"blueprintGoBack": "모든 청사진 보기",
"blueprintCreate": "청사진 생성",
"blueprintCreateDescription2": "새 청사진을 생성하고 적용하려면 아래 단계를 따르십시오",
"blueprintDetails": "청사진 세부 사항",
"blueprintDetailsDescription": "청사진 실행 세부 정보 보기",
"blueprintDetails": "청사진 세부사항",
"blueprintDetailsDescription": "적용된 청사진의 결과와 발생한 오류를 확인합니다",
"blueprintInfo": "청사진 정보",
"message": "메시지",
"blueprintContentsDescription": "인프라를 설명하는 YAML 콘텐츠를 정의하십시오",
@@ -1181,7 +1181,7 @@
"appliedAt": "적용 시점",
"source": "출처",
"contents": "콘텐츠",
"parsedContents": "구문 분석된 콘텐츠",
"parsedContents": "구문 분석된 콘텐츠 (읽기 전용)",
"enableDockerSocket": "Docker 청사진 활성화",
"enableDockerSocketDescription": "블루프린트 레이블을 위한 Docker 소켓 레이블 수집을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.",
"enableDockerSocketLink": "자세히 알아보기",
@@ -1279,6 +1279,15 @@
"settingsErrorUpdateDescription": "설정을 업데이트하는 동안 오류가 발생했습니다",
"sidebarCollapse": "줄이기",
"sidebarExpand": "확장하기",
"productUpdateMoreInfo": "{noOfUpdates}개의 더 많은 업데이트",
"productUpdateInfo": "{noOfUpdates}개 업데이트",
"productUpdateWhatsNew": "새로운 기능",
"productUpdateTitle": "제품 업데이트",
"productUpdateEmpty": "업데이트 없음",
"dismissAll": "모두 해제",
"pangolinUpdateAvailable": "새 버전 사용 가능",
"pangolinUpdateAvailableInfo": "버전 {version}을(를) 설치할 준비가 되었습니다",
"pangolinUpdateAvailableReleaseNotes": "릴리스 노트 보기",
"newtUpdateAvailable": "업데이트 가능",
"newtUpdateAvailableInfo": "뉴트의 새 버전이 출시되었습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
"domainPickerEnterDomain": "도메인",
@@ -1525,6 +1534,12 @@
"resourcesTableTheseResourcesForUseWith": "이 리소스는 다음과 함께 사용하기 위한 것입니다.",
"resourcesTableClients": "클라이언트",
"resourcesTableAndOnlyAccessibleInternally": "클라이언트와 연결되었을 때만 내부적으로 접근 가능합니다.",
"resourcesTableNoTargets": "대상 없음",
"resourcesTableHealthy": "정상",
"resourcesTableDegraded": "저하됨",
"resourcesTableOffline": "오프라인",
"resourcesTableUnknown": "알 수 없음",
"resourcesTableNotMonitored": "모니터링되지 않음",
"editInternalResourceDialogEditClientResource": "클라이언트 리소스 수정",
"editInternalResourceDialogUpdateResourceProperties": "{resourceName}의 리소스 속성과 대상 구성을 업데이트하세요.",
"editInternalResourceDialogResourceProperties": "리소스 속성",
@@ -2080,5 +2095,46 @@
"supportSending": "발송 중...",
"supportSend": "보내기",
"supportMessageSent": "메시지 전송 완료!",
"supportWillContact": "곧 연락드리겠습니다!"
"supportWillContact": "곧 연락드리겠습니다!",
"selectLogRetention": "로그 보존 선택",
"showColumns": "열 표시",
"hideColumns": "열 숨기기",
"columnVisibility": "열 가시성",
"toggleColumn": "{columnName} 열 토글",
"allColumns": "모든 열",
"defaultColumns": "기본 열",
"customizeView": "보기 사용자 지정",
"viewOptions": "보기 옵션",
"selectAll": "모두 선택",
"selectNone": "선택하지 않음",
"selectedResources": "선택된 리소스",
"enableSelected": "선택된 항목 활성화",
"disableSelected": "선택된 항목 비활성화",
"checkSelectedStatus": "선택된 항목 상태 확인",
"credentials": "자격 증명",
"savecredentials": "자격 증명 저장",
"regeneratecredentials": "재생성",
"regenerateCredentials": "자격 증명을 재생성하고 저장합니다",
"generatedcredentials": "생성된 자격 증명",
"copyandsavethesecredentials": "이 자격 증명을 복사하여 저장합니다",
"copyandsavethesecredentialsdescription": "이 페이지를 떠난 후에는 자격 증명이 다시 표시되지 않습니다. 지금 안전하게 저장하십시오.",
"credentialsSaved": "자격 증명 저장됨",
"credentialsSavedDescription": "자격 증명이 성공적으로 재생성 및 저장되었습니다.",
"credentialsSaveError": "자격 증명 저장 오류",
"credentialsSaveErrorDescription": "자격 증명을 재생성하고 저장하는 동안 오류가 발생했습니다.",
"regenerateCredentialsWarning": "자격 증명을 재생성하면 이전 자격 증명이 무효화됩니다. 이 자격 증명을 사용하는 모든 구성을 업데이트하십시오.",
"confirm": "확인",
"regenerateCredentialsConfirmation": "자격 증명을 재생성하시겠습니까?",
"endpoint": "엔드포인트",
"Id": "아이디",
"SecretKey": "비밀 키",
"featureDisabledTooltip": "이 기능은 엔터프라이즈 플랜에서만 사용할 수 있으며 라이센스가 필요합니다.",
"niceId": "예쁜 ID",
"niceIdUpdated": "예쁜 ID 업데이트됨",
"niceIdUpdatedSuccessfully": "예쁜 ID가 성공적으로 업데이트되었습니다",
"niceIdUpdateError": "예쁜 ID 업데이트 오류",
"niceIdUpdateErrorDescription": "예쁜 ID를 업데이트하는 동안 오류가 발생했습니다.",
"niceIdCannotBeEmpty": "예쁜 ID는 비워둘 수 없습니다",
"enterIdentifier": "식별자 입력",
"identifier": "식별자"
}

View File

@@ -131,7 +131,7 @@
"expireIn": "Utløper om",
"neverExpire": "Utløper aldri",
"shareExpireDescription": "Utløpstid er hvor lenge lenken vil være brukbar og gi tilgang til ressursen. Etter denne tiden vil lenken ikke lenger fungere, og brukere som brukte denne lenken vil miste tilgangen til ressursen.",
"shareSeeOnce": "Du får bare se denne lenken én gang. Pass på å kopiere den.",
"shareSeeOnce": "Du vil bare kunne se denne linken én gang. Pass på å kopiere den.",
"shareAccessHint": "Alle med denne lenken kan få tilgang til ressursen. Del forsiktig.",
"shareTokenUsage": "Se tilgangstokenbruk",
"createLink": "Opprett lenke",
@@ -179,7 +179,7 @@
"baseDomain": "Grunndomene",
"subdomnainDescription": "Underdomenet der ressursen din vil være tilgjengelig.",
"resourceRawSettings": "TCP/UDP-innstillinger",
"resourceRawSettingsDescription": "Konfigurer tilgang til ressursen din over TCP/UDP",
"resourceRawSettingsDescription": "Konfigurer hvordan din ressurs vil bli tilgjengelig over TCP/UDP. Du kartlegger ressursen til en port på vertsserveren Pangolin slik at du får tilgang til ressursene fra server-ip:mappet port.",
"protocol": "Protokoll",
"protocolSelect": "Velg en protokoll",
"resourcePortNumber": "Portnummer",
@@ -1165,13 +1165,13 @@
"sidebarDomains": "Domener",
"sidebarBluePrints": "Tegninger",
"blueprints": "Tegninger",
"blueprintsDescription": "Tegninger er deklarative YAML konfigurasjoner som definerer dine ressurser og deres innstillinger",
"blueprintsDescription": "Bruk deklarative konfigurasjoner og vis tidligere kjøringer",
"blueprintAdd": "Legg til blåkopi",
"blueprintGoBack": "Se alle blåkopier",
"blueprintCreate": "Opprette mal",
"blueprintCreateDescription2": "Følg trinnene nedenfor for å opprette og bruke en ny plantegning",
"blueprintDetails": "Blåkopi detaljer",
"blueprintDetailsDescription": "Se detaljer om plantegning",
"blueprintDetailsDescription": "Se resultatet av den påførte blåkopien og alle feil som oppstod",
"blueprintInfo": "Blåkopi informasjon",
"message": "Melding",
"blueprintContentsDescription": "Definer innhold av YAML som beskriver din infrastruktur",
@@ -1181,7 +1181,7 @@
"appliedAt": "Anvendt på",
"source": "Kilde",
"contents": "Innhold",
"parsedContents": "Parket innhold",
"parsedContents": "Parastinnhold (kun lese)",
"enableDockerSocket": "Aktiver Docker blåkopi",
"enableDockerSocketDescription": "Aktiver skraping av Docker Socket for blueprint Etiketter. Socket bane må brukes for nye.",
"enableDockerSocketLink": "Lær mer",
@@ -1279,6 +1279,15 @@
"settingsErrorUpdateDescription": "En feil oppstod under oppdatering av innstillinger",
"sidebarCollapse": "Skjul",
"sidebarExpand": "Utvid",
"productUpdateMoreInfo": "{noOfUpdates} flere oppdateringer",
"productUpdateInfo": "{noOfUpdates} oppdateringer",
"productUpdateWhatsNew": "Hva er nytt",
"productUpdateTitle": "Oppdateringer om produktet",
"productUpdateEmpty": "Ingen oppdateringer",
"dismissAll": "Avvis alle",
"pangolinUpdateAvailable": "Ny versjon tilgjengelig",
"pangolinUpdateAvailableInfo": "Versjon {version} er klar til å installere",
"pangolinUpdateAvailableReleaseNotes": "Se utgivelsnotater",
"newtUpdateAvailable": "Oppdatering tilgjengelig",
"newtUpdateAvailableInfo": "En ny versjon av Newt er tilgjengelig. Vennligst oppdater til den nyeste versjonen for den beste opplevelsen.",
"domainPickerEnterDomain": "Domene",
@@ -1525,6 +1534,12 @@
"resourcesTableTheseResourcesForUseWith": "Disse ressursene er til bruk med",
"resourcesTableClients": "Klienter",
"resourcesTableAndOnlyAccessibleInternally": "og er kun tilgjengelig internt når de er koblet til med en klient.",
"resourcesTableNoTargets": "Ingen mål",
"resourcesTableHealthy": "Frisk",
"resourcesTableDegraded": "Nedgradert",
"resourcesTableOffline": "Frakoblet",
"resourcesTableUnknown": "Ukjent",
"resourcesTableNotMonitored": "Ikke overvåket",
"editInternalResourceDialogEditClientResource": "Rediger klientressurs",
"editInternalResourceDialogUpdateResourceProperties": "Oppdater ressursens egenskaper og målkonfigurasjon for {resourceName}.",
"editInternalResourceDialogResourceProperties": "Ressursegenskaper",
@@ -2080,5 +2095,46 @@
"supportSending": "Sender...",
"supportSend": "Sende",
"supportMessageSent": "Melding sendt!",
"supportWillContact": "Vi kommer raskt til å ta kontakt!"
"supportWillContact": "Vi kommer raskt til å ta kontakt!",
"selectLogRetention": "Velg oppbevaring av logg",
"showColumns": "Vis kolonner",
"hideColumns": "Skjul kolonner",
"columnVisibility": "Kolonne Synlighet",
"toggleColumn": "Veksle {columnName} kolonne",
"allColumns": "Alle kolonner",
"defaultColumns": "Standard kolonner",
"customizeView": "Tilpass visning",
"viewOptions": "Vis alternativer",
"selectAll": "Velg alle",
"selectNone": "Velg ingen",
"selectedResources": "Valgte ressurser",
"enableSelected": "Aktiver valgte",
"disableSelected": "Deaktiver valgte",
"checkSelectedStatus": "Kontroller status for valgte",
"credentials": "Legitimasjon",
"savecredentials": "Lagre brukeropplysninger",
"regeneratecredentials": "Ny nøkkel",
"regenerateCredentials": "Regenerer og lagre opplysningene dine",
"generatedcredentials": "Genererte brukeropplysninger",
"copyandsavethesecredentials": "Kopier og lagre disse opplysningene",
"copyandsavethesecredentialsdescription": "Disse opplysningene vil ikke bli vist igjen etter at du forlater siden. Lagre dem trygt nå.",
"credentialsSaved": "Påloggingsinformasjon lagret",
"credentialsSavedDescription": "Påloggingsinformasjonen har blitt regenerert og lagret.",
"credentialsSaveError": "Påloggingsinformasjon lagre feil",
"credentialsSaveErrorDescription": "En feil oppstod under regenerering og lagring av legitimasjon.",
"regenerateCredentialsWarning": "Regenerering av legitimasjon vil ugyldiggjøre de forrige. Sørg for at alle konfigurasjoner som bruker disse legitimasjonene.",
"confirm": "Bekreft",
"regenerateCredentialsConfirmation": "Er du sikker på at du vil regenerere legetimasjonene?",
"endpoint": "Endpoint",
"Id": "Id",
"SecretKey": "Hemmelig nøkkel",
"featureDisabledTooltip": "Denne funksjonen er bare tilgjengelig i virksomhetsplanen og krever en lisens til å bruke den.",
"niceId": "God ID",
"niceIdUpdated": "Flott ID oppdatert",
"niceIdUpdatedSuccessfully": "Id-en ble oppdatert",
"niceIdUpdateError": "Feil under oppdatering av hyggelig ID",
"niceIdUpdateErrorDescription": "Det oppstod en feil under oppdatering av Nice ID.",
"niceIdCannotBeEmpty": "God ID kan ikke være tom",
"enterIdentifier": "Angi identifikator",
"identifier": "Identifier"
}

View File

@@ -131,7 +131,7 @@
"expireIn": "Vervalt in",
"neverExpire": "Nooit verlopen",
"shareExpireDescription": "Vervaltijd is hoe lang de link bruikbaar is en geeft toegang tot de bron. Na deze tijd zal de link niet meer werken en zullen gebruikers die deze link hebben gebruikt de toegang tot de pagina verliezen.",
"shareSeeOnce": "Je kunt deze koppeling alleen zien. Zorg ervoor dat je het kopieert.",
"shareSeeOnce": "U kunt deze link slechts één keer zien. Zorg ervoor dat u deze kopieert.",
"shareAccessHint": "Iedereen met deze link heeft toegang tot de bron. Deel deze met zorg.",
"shareTokenUsage": "Zie Toegangstoken Gebruik",
"createLink": "Koppeling aanmaken",
@@ -179,7 +179,7 @@
"baseDomain": "Basis domein",
"subdomnainDescription": "Het subdomein waar de bron toegankelijk is.",
"resourceRawSettings": "TCP/UDP instellingen",
"resourceRawSettingsDescription": "Stel in hoe je bron wordt benaderd via TCP/UDP",
"resourceRawSettingsDescription": "Stel in hoe uw bron wordt benaderd via TCP/UDP. Je gooit de bron toe aan een poort op de host-Pangolin server, zodat je de bron kan bereiken vanaf server-public-ip:mapped-port.",
"protocol": "Protocol",
"protocolSelect": "Selecteer een protocol",
"resourcePortNumber": "Nummer van poort",
@@ -1165,13 +1165,13 @@
"sidebarDomains": "Domeinen",
"sidebarBluePrints": "Blauwdrukken",
"blueprints": "Blauwdrukken",
"blueprintsDescription": "Blauwdrukken zijn declaratieve YAML-configuraties die je bronnen en hun instellingen bepalen",
"blueprintsDescription": "Gebruik declaratieve configuraties en bekijk vorige uitvoeringen.",
"blueprintAdd": "Blauwdruk toevoegen",
"blueprintGoBack": "Bekijk alle Blauwdrukken",
"blueprintCreate": "Creëer blauwdruk",
"blueprintCreateDescription2": "Volg de onderstaande stappen om een nieuwe blauwdruk te maken en toe te passen",
"blueprintDetails": "Blauwdruk details",
"blueprintDetailsDescription": "Bekijk de blauwdruk run details",
"blueprintDetails": "Blauwdruk Details",
"blueprintDetailsDescription": "Bekijk het resultaat van de toegepaste blauwdruk en eventuele fouten",
"blueprintInfo": "Blauwdruk Informatie",
"message": "bericht",
"blueprintContentsDescription": "Definieer de YAML content die je infrastructuur beschrijft",
@@ -1181,7 +1181,7 @@
"appliedAt": "Toegepast op",
"source": "Bron",
"contents": "Inhoud",
"parsedContents": "Geparseerde inhoud",
"parsedContents": "Geparseerde inhoud (alleen lezen)",
"enableDockerSocket": "Schakel Docker Blauwdruk in",
"enableDockerSocketDescription": "Schakel Docker Socket label in voor blauwdruk labels. Pad naar Nieuw.",
"enableDockerSocketLink": "Meer informatie",
@@ -1279,6 +1279,15 @@
"settingsErrorUpdateDescription": "Er is een fout opgetreden bij het bijwerken van instellingen",
"sidebarCollapse": "Inklappen",
"sidebarExpand": "Uitklappen",
"productUpdateMoreInfo": "Nog {noOfUpdates} updates",
"productUpdateInfo": "{noOfUpdates} updates",
"productUpdateWhatsNew": "Wat is nieuw",
"productUpdateTitle": "Update Producten",
"productUpdateEmpty": "Geen updates",
"dismissAll": "Alles afwijzen",
"pangolinUpdateAvailable": "Nieuwe versie beschikbaar",
"pangolinUpdateAvailableInfo": "Versie {version} is klaar om te installeren",
"pangolinUpdateAvailableReleaseNotes": "Bekijk release notities",
"newtUpdateAvailable": "Update beschikbaar",
"newtUpdateAvailableInfo": "Er is een nieuwe versie van Newt beschikbaar. Update naar de nieuwste versie voor de beste ervaring.",
"domainPickerEnterDomain": "Domein",
@@ -1525,6 +1534,12 @@
"resourcesTableTheseResourcesForUseWith": "Deze bronnen zijn bedoeld voor gebruik met",
"resourcesTableClients": "Clienten",
"resourcesTableAndOnlyAccessibleInternally": "en zijn alleen intern toegankelijk wanneer verbonden met een client.",
"resourcesTableNoTargets": "Geen doelen",
"resourcesTableHealthy": "Gezond",
"resourcesTableDegraded": "Verminderde",
"resourcesTableOffline": "Offline",
"resourcesTableUnknown": "onbekend",
"resourcesTableNotMonitored": "Niet gecontroleerd",
"editInternalResourceDialogEditClientResource": "Bewerk clientbron",
"editInternalResourceDialogUpdateResourceProperties": "Werk de eigenschapen van de bron en doelconfiguratie bij voor {resourceName}.",
"editInternalResourceDialogResourceProperties": "Bron eigenschappen",
@@ -2080,5 +2095,46 @@
"supportSending": "Verzenden...",
"supportSend": "Verzenden",
"supportMessageSent": "Bericht verzonden!",
"supportWillContact": "We nemen binnenkort contact met u op!"
"supportWillContact": "We nemen binnenkort contact met u op!",
"selectLogRetention": "Selecteer log retentie",
"showColumns": "Kolommen weergeven",
"hideColumns": "Kolommen verbergen",
"columnVisibility": "Zichtbaarheid kolommen",
"toggleColumn": "{columnName} kolom in-/uitschakelen",
"allColumns": "Alle kolommen",
"defaultColumns": "Standaard Kolommen",
"customizeView": "Weergave aanpassen",
"viewOptions": "Bekijk opties",
"selectAll": "Alles selecteren",
"selectNone": "Niets selecteren",
"selectedResources": "Geselecteerde bronnen",
"enableSelected": "Selectie inschakelen",
"disableSelected": "Selectie uitschakelen",
"checkSelectedStatus": "Controleer de status van de geselecteerde",
"credentials": "Aanmeldgegevens",
"savecredentials": "Referenties opslaan",
"regeneratecredentials": "Hersleutel",
"regenerateCredentials": "Opnieuw genereren en opslaan van uw referenties",
"generatedcredentials": "Gegenereerde referenties",
"copyandsavethesecredentials": "Kopieer en bewaar deze inloggegevens",
"copyandsavethesecredentialsdescription": "Deze referenties worden niet meer getoond nadat u deze pagina verlaat. Sla ze nu veilig op.",
"credentialsSaved": "Referenties opgeslagen",
"credentialsSavedDescription": "Referenties werden met succes opnieuw gegenereerd en opgeslagen.",
"credentialsSaveError": "Fout bij opslaan referenties",
"credentialsSaveErrorDescription": "Er is een fout opgetreden tijdens het opnieuw genereren en opslaan van de inloggegevens.",
"regenerateCredentialsWarning": "Het opnieuw genereren van inloggegevens zal de vorige ongeldig maken. Zorg ervoor dat alle configuraties die deze inloggegevens gebruiken bijgewerkt worden.",
"confirm": "Bevestigen",
"regenerateCredentialsConfirmation": "Weet u zeker dat u de inloggegevens opnieuw wilt genereren?",
"endpoint": "Endpoint",
"Id": "Id",
"SecretKey": "Geheime sleutel",
"featureDisabledTooltip": "Deze functie is alleen beschikbaar in het bedrijfsplan en vereist een licentie om deze te gebruiken.",
"niceId": "Leuk ID",
"niceIdUpdated": "Leuke ID bijgewerkt",
"niceIdUpdatedSuccessfully": "Nice ID Updated Successfully",
"niceIdUpdateError": "Fout bij bijwerken ID Nice",
"niceIdUpdateErrorDescription": "Fout opgetreden tijdens het bijwerken van de ID van Nice.",
"niceIdCannotBeEmpty": "Nice ID mag niet leeg zijn",
"enterIdentifier": "ID invoeren",
"identifier": "Identifier"
}

View File

@@ -131,7 +131,7 @@
"expireIn": "Wygasa za",
"neverExpire": "Nigdy nie wygasa",
"shareExpireDescription": "Czas wygaśnięcia to jak długo link będzie mógł być użyty i zapewni dostęp do zasobu. Po tym czasie link nie będzie już działał, a użytkownicy, którzy użyli tego linku, utracą dostęp do zasobu.",
"shareSeeOnce": "Możesz zobaczyć tylko ten link. Upewnij się, że go skopiowało.",
"shareSeeOnce": "Możesz zobaczyć ten link tylko raz. Pamiętaj, aby go skopiować.",
"shareAccessHint": "Każdy z tym linkiem może uzyskać dostęp do zasobu. Podziel się nim ostrożnie.",
"shareTokenUsage": "Zobacz użycie tokenu dostępu",
"createLink": "Utwórz link",
@@ -179,7 +179,7 @@
"baseDomain": "Bazowa domena",
"subdomnainDescription": "Poddomena, w której twój zasób będzie dostępny.",
"resourceRawSettings": "Ustawienia TCP/UDP",
"resourceRawSettingsDescription": "Skonfiguruj jak twój zasób będzie dostępny przez TCP/UDP",
"resourceRawSettingsDescription": "Skonfiguruj jak twój zasób będzie dostępny przez TCP/UDP. Zmapujesz zasób do portu na serwerze hosta Pangolin, dzięki czemu możesz uzyskać dostęp do zasobu z serwera-public ip:mapped-port.",
"protocol": "Protokół",
"protocolSelect": "Wybierz protokół",
"resourcePortNumber": "Numer portu",
@@ -1165,13 +1165,13 @@
"sidebarDomains": "Domeny",
"sidebarBluePrints": "Schematy",
"blueprints": "Schematy",
"blueprintsDescription": "Plany to deklaratywne konfiguracje YAML, które definiują twoje zasoby i ich ustawienia",
"blueprintsDescription": "Zastosuj konfiguracje deklaracyjne i wyświetl poprzednie operacje",
"blueprintAdd": "Dodaj schemat",
"blueprintGoBack": "Zobacz wszystkie schematy",
"blueprintCreate": "Utwórz schemat",
"blueprintCreateDescription2": "Wykonaj poniższe kroki, aby utworzyć i zastosować nowy schemat",
"blueprintDetails": "Szczegóły projektu",
"blueprintDetailsDescription": "Zobacz szczegóły uruchomienia schematu",
"blueprintDetails": "Szczegóły Projektu",
"blueprintDetailsDescription": "Zobacz wynik zastosowanego schematu i wszelkie błędy, które wystąpiły",
"blueprintInfo": "Informacje o projekcie",
"message": "Wiadomość",
"blueprintContentsDescription": "Zdefiniuj zawartość YAML opisującą Twoją infrastrukturę",
@@ -1181,7 +1181,7 @@
"appliedAt": "Zastosowano",
"source": "Źródło",
"contents": "Treść",
"parsedContents": "Przetworzona zawartość",
"parsedContents": "Przetworzona zawartość (tylko do odczytu)",
"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.",
"enableDockerSocketLink": "Dowiedz się więcej",
@@ -1279,6 +1279,15 @@
"settingsErrorUpdateDescription": "Wystąpił błąd podczas aktualizacji ustawień",
"sidebarCollapse": "Zwiń",
"sidebarExpand": "Rozwiń",
"productUpdateMoreInfo": "{noOfUpdates} więcej aktualizacji",
"productUpdateInfo": "Aktualizacje {noOfUpdates}",
"productUpdateWhatsNew": "Co nowego",
"productUpdateTitle": "Aktualizacje produktu",
"productUpdateEmpty": "Brak aktualizacji",
"dismissAll": "Zamknij wszystkie",
"pangolinUpdateAvailable": "Dostępna jest nowa wersja",
"pangolinUpdateAvailableInfo": "Wersja {version} jest gotowa do zainstalowania",
"pangolinUpdateAvailableReleaseNotes": "Zobacz notatki o wydaniu",
"newtUpdateAvailable": "Dostępna aktualizacja",
"newtUpdateAvailableInfo": "Nowa wersja Newt jest dostępna. Prosimy o aktualizację do najnowszej wersji dla najlepszej pracy.",
"domainPickerEnterDomain": "Domena",
@@ -1525,6 +1534,12 @@
"resourcesTableTheseResourcesForUseWith": "Te zasoby są do użytku z",
"resourcesTableClients": "Klientami",
"resourcesTableAndOnlyAccessibleInternally": "i są dostępne tylko wewnętrznie po połączeniu z klientem.",
"resourcesTableNoTargets": "Brak celów",
"resourcesTableHealthy": "Zdrowe",
"resourcesTableDegraded": "Degradacja",
"resourcesTableOffline": "Offline",
"resourcesTableUnknown": "Nieznane",
"resourcesTableNotMonitored": "Nie monitorowano",
"editInternalResourceDialogEditClientResource": "Edytuj zasób klienta",
"editInternalResourceDialogUpdateResourceProperties": "Zaktualizuj właściwości zasobu i konfigurację celu dla {resourceName}.",
"editInternalResourceDialogResourceProperties": "Właściwości zasobów",
@@ -2080,5 +2095,46 @@
"supportSending": "Wysyłanie...",
"supportSend": "Wyślij",
"supportMessageSent": "Wiadomość wysłana!",
"supportWillContact": "Wkrótce będziemy w kontakcie!"
"supportWillContact": "Wkrótce będziemy w kontakcie!",
"selectLogRetention": "Wybierz zatrzymanie dziennika",
"showColumns": "Pokaż kolumny",
"hideColumns": "Ukryj kolumny",
"columnVisibility": "Widoczność kolumn",
"toggleColumn": "Przełącz kolumnę {columnName}",
"allColumns": "Wszystkie kolumny",
"defaultColumns": "Kolumny domyślne",
"customizeView": "Dostosuj widok",
"viewOptions": "Opcje widoku",
"selectAll": "Zaznacz wszystko",
"selectNone": "Nie wybierz żadnego",
"selectedResources": "Wybrane Zasoby",
"enableSelected": "Włącz zaznaczone",
"disableSelected": "Wyłącz zaznaczone",
"checkSelectedStatus": "Sprawdź status zaznaczonych",
"credentials": "Dane logowania",
"savecredentials": "Zapisz dane logowania",
"regeneratecredentials": "Przycisk ponownie",
"regenerateCredentials": "Ponownie wygeneruj i zapisz swoje dane logowania",
"generatedcredentials": "Wygenerowane dane logowania",
"copyandsavethesecredentials": "Skopiuj i zapisz te dane logowania",
"copyandsavethesecredentialsdescription": "Te dane uwierzytelniające nie będą wyświetlane ponownie po opuszczeniu tej strony. Zapisz je teraz bezpiecznie.",
"credentialsSaved": "Zapisano dane logowania",
"credentialsSavedDescription": "Dane logowania zostały wygenerowane i zapisane pomyślnie.",
"credentialsSaveError": "Błąd zapisu danych logowania",
"credentialsSaveErrorDescription": "Wystąpił błąd podczas regeneracji i zapisywania poświadczeń.",
"regenerateCredentialsWarning": "Regeneracja poświadczeń spowoduje unieważnienie poprzednich poświadczeń. Upewnij się, że zaktualizowano wszystkie konfiguracje, które używają tych poświadczeń.",
"confirm": "Potwierdź",
"regenerateCredentialsConfirmation": "Czy na pewno chcesz wygenerować dane logowania?",
"endpoint": "Endpoint",
"Id": "Id",
"SecretKey": "Sekretny klucz",
"featureDisabledTooltip": "Ta funkcja jest dostępna tylko w planie przedsiębiorstwa i wymaga licencji, aby z niej korzystać.",
"niceId": "Niepoprawne ID",
"niceIdUpdated": "Zaktualizowano błędne ID",
"niceIdUpdatedSuccessfully": "Zaktualizowano błędne ID",
"niceIdUpdateError": "Błąd podczas aktualizacji Nice ID",
"niceIdUpdateErrorDescription": "Wystąpił błąd podczas aktualizowania Nicei ID.",
"niceIdCannotBeEmpty": "Niepoprawny identyfikator nie może być pusty",
"enterIdentifier": "Wprowadź identyfikator",
"identifier": "Identifier"
}

View File

@@ -179,7 +179,7 @@
"baseDomain": "Domínio Base",
"subdomnainDescription": "O subdomínio onde seu recurso estará acessível.",
"resourceRawSettings": "Configurações TCP/UDP",
"resourceRawSettingsDescription": "Configure como seu recurso será acessado sobre TCP/UDP",
"resourceRawSettingsDescription": "Configure como seu recurso será acessado sobre TCP/UDP. Você mapeia o recurso para uma porta no servidor Pangolin do hospedeiro, para que você possa acessar o recurso do server-public-ip:mapped-port.",
"protocol": "Protocolo",
"protocolSelect": "Selecione um protocolo",
"resourcePortNumber": "Número da Porta",
@@ -1165,13 +1165,13 @@
"sidebarDomains": "Domínios",
"sidebarBluePrints": "Diagramas",
"blueprints": "Diagramas",
"blueprintsDescription": "Diagramas são configurações declarativas YAML que definem seus recursos e suas configurações",
"blueprintsDescription": "Aplicar configurações declarativas e ver execuções anteriores",
"blueprintAdd": "Adicionar Diagrama",
"blueprintGoBack": "Ver todos os Diagramas",
"blueprintCreate": "Criar Diagrama",
"blueprintCreateDescription2": "Siga as etapas abaixo para criar e aplicar um novo diagrama",
"blueprintDetails": "Detalhes do Diagrama",
"blueprintDetailsDescription": "Veja os detalhes da execução do diagrama",
"blueprintDetailsDescription": "Veja o resultado do diagrama aplicado e todos os erros que ocorreram",
"blueprintInfo": "Informação do Diagrama",
"message": "mensagem",
"blueprintContentsDescription": "Defina o conteúdo YAML descrevendo a sua infraestrutura",
@@ -1181,7 +1181,7 @@
"appliedAt": "Aplicado em",
"source": "fonte",
"contents": "Conteúdo",
"parsedContents": "Conteúdo analisado",
"parsedContents": "Conteúdo analisado (Somente Leitura)",
"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.",
"enableDockerSocketLink": "Saiba mais",
@@ -1279,6 +1279,15 @@
"settingsErrorUpdateDescription": "Ocorreu um erro ao atualizar configurações",
"sidebarCollapse": "Recolher",
"sidebarExpand": "Expandir",
"productUpdateMoreInfo": "Mais {noOfUpdates} atualizações",
"productUpdateInfo": "Atualizações {noOfUpdates}",
"productUpdateWhatsNew": "Novidades",
"productUpdateTitle": "Atualizações de Produto",
"productUpdateEmpty": "Não há atualizações",
"dismissAll": "Recusar tudo",
"pangolinUpdateAvailable": "Nova versão disponível",
"pangolinUpdateAvailableInfo": "A versão {version} está pronta para ser instalada",
"pangolinUpdateAvailableReleaseNotes": "Ver notas de lançamento",
"newtUpdateAvailable": "Nova Atualização Disponível",
"newtUpdateAvailableInfo": "Uma nova versão do Newt está disponível. Atualize para a versão mais recente para uma melhor experiência.",
"domainPickerEnterDomain": "Domínio",
@@ -1525,6 +1534,12 @@
"resourcesTableTheseResourcesForUseWith": "Esses recursos são para uso com",
"resourcesTableClients": "Clientes",
"resourcesTableAndOnlyAccessibleInternally": "e são acessíveis apenas internamente quando conectados com um cliente.",
"resourcesTableNoTargets": "Nenhum alvo",
"resourcesTableHealthy": "Saudável",
"resourcesTableDegraded": "Degradado",
"resourcesTableOffline": "Desconectado",
"resourcesTableUnknown": "Desconhecido",
"resourcesTableNotMonitored": "Não monitorado",
"editInternalResourceDialogEditClientResource": "Editar Recurso do Cliente",
"editInternalResourceDialogUpdateResourceProperties": "Atualize as propriedades do recurso e a configuração do alvo para {resourceName}.",
"editInternalResourceDialogResourceProperties": "Propriedades do Recurso",
@@ -2080,5 +2095,46 @@
"supportSending": "Enviando...",
"supportSend": "Mandar",
"supportMessageSent": "Mensagem enviada!",
"supportWillContact": "Entraremos em contato em breve!"
"supportWillContact": "Entraremos em contato em breve!",
"selectLogRetention": "Selecionar retenção de log",
"showColumns": "Exibir Colunas",
"hideColumns": "Ocultar colunas",
"columnVisibility": "Visibilidade da Coluna",
"toggleColumn": "Alternar coluna {columnName}",
"allColumns": "Todas as colunas",
"defaultColumns": "Colunas padrão",
"customizeView": "Personalizar visualização",
"viewOptions": "Opções de visualização",
"selectAll": "Selecionar Todos",
"selectNone": "Não selecionar nada",
"selectedResources": "Recursos Selecionados",
"enableSelected": "Habilitar Selecionados",
"disableSelected": "Desativar Selecionados",
"checkSelectedStatus": "Status de Verificação dos Selecionados",
"credentials": "Credenciais",
"savecredentials": "Salvar Credenciais",
"regeneratecredentials": "Rechave",
"regenerateCredentials": "Regenerar e salvar suas credenciais",
"generatedcredentials": "Credenciais Geradas",
"copyandsavethesecredentials": "Copiar e salvar estas credenciais",
"copyandsavethesecredentialsdescription": "Essas credenciais não serão exibidas novamente depois que você sair desta página. Salve elas com segurança agora.",
"credentialsSaved": "Credenciais salvas",
"credentialsSavedDescription": "As credenciais foram regeneradas e salvas com sucesso.",
"credentialsSaveError": "Erro ao Salvar Credenciais",
"credentialsSaveErrorDescription": "Ocorreu um erro enquanto regenerava e salvava as credenciais.",
"regenerateCredentialsWarning": "Regenerar credenciais irá invalidar as anteriores. Certifique-se de atualizar qualquer configuração que use essas credenciais.",
"confirm": "Confirmar",
"regenerateCredentialsConfirmation": "Você tem certeza que deseja recriar as credenciais?",
"endpoint": "Endpoint",
"Id": "Id",
"SecretKey": "Chave secreta",
"featureDisabledTooltip": "Este recurso só está disponível no plano corporativo e requer que uma licença utilize.",
"niceId": "Belo ID",
"niceIdUpdated": "Bom ID atualizado",
"niceIdUpdatedSuccessfully": "Bom ID atualizado com sucesso",
"niceIdUpdateError": "Erro ao atualizar Nice ID",
"niceIdUpdateErrorDescription": "Ocorreu um erro ao atualizar a ID de Nice.",
"niceIdCannotBeEmpty": "Bom ID não pode estar vazio",
"enterIdentifier": "Inserir identificador",
"identifier": "Identifier"
}

View File

@@ -131,7 +131,7 @@
"expireIn": "Срок действия",
"neverExpire": "Бессрочный доступ",
"shareExpireDescription": "Срок действия - это период, в течение которого ссылка будет работать и предоставлять доступ к ресурсу. После этого времени ссылка перестанет работать, и пользователи, использовавшие эту ссылку, потеряют доступ к ресурсу.",
"shareSeeOnce": "Вы сможете увидеть эту ссылку только один раз. Обязательно скопируйте её.",
"shareSeeOnce": "Вы сможете увидеть эту ссылку только один раз. Обязательно скопируйте ее.",
"shareAccessHint": "Любой, у кого есть эта ссылка, может получить доступ к ресурсу. Делитесь ею с осторожностью.",
"shareTokenUsage": "Посмотреть использование токена доступа",
"createLink": "Создать ссылку",
@@ -179,7 +179,7 @@
"baseDomain": "Базовый домен",
"subdomnainDescription": "Поддомен, на котором будет доступен ресурс.",
"resourceRawSettings": "Настройки TCP/UDP",
"resourceRawSettingsDescription": "Настройте, как будет осуществляться доступ к вашему ресурсу через TCP/UDP",
"resourceRawSettingsDescription": "Настройте доступ к вашему ресурсу по TCP/UDP. Вы соотносите ресурс с портом на сервере хоста Pangolin, так что вы можете получить доступ к ресурсу с сервера server-public-ip:mapped-порта.",
"protocol": "Протокол",
"protocolSelect": "Выберите протокол",
"resourcePortNumber": "Номер порта",
@@ -1165,13 +1165,13 @@
"sidebarDomains": "Домены",
"sidebarBluePrints": "Чертежи",
"blueprints": "Чертежи",
"blueprintsDescription": "Чертежи являются декларативными конфигурациями YAML, которые определяют ваши ресурсы и их настройки",
"blueprintsDescription": "Применить декларирующие конфигурации и просмотреть предыдущие запуски",
"blueprintAdd": "Добавить чертёж",
"blueprintGoBack": "Посмотреть все чертежи",
"blueprintCreate": "Создать чертёж",
"blueprintCreateDescription2": "Для создания и применения нового чертежа выполните следующие шаги",
"blueprintDetails": "Подробности чертежа",
"blueprintDetailsDescription": "Посмотреть детали запуска чертежа",
"blueprintDetails": "Детали чертежа",
"blueprintDetailsDescription": "Посмотреть результат примененного чертежа и все возникшие ошибки",
"blueprintInfo": "Информация о чертеже",
"message": "Сообщение",
"blueprintContentsDescription": "Определите содержимое YAML, описывающее вашу инфраструктуру",
@@ -1181,7 +1181,7 @@
"appliedAt": "Заявка на",
"source": "Источник",
"contents": "Содержание",
"parsedContents": "Обработанное содержимое",
"parsedContents": "Переработанное содержимое (только для чтения)",
"enableDockerSocket": "Включить чертёж Docker",
"enableDockerSocketDescription": "Включить scraping ярлыка Docker Socket для ярлыков чертежей. Путь к сокету должен быть предоставлен в Newt.",
"enableDockerSocketLink": "Узнать больше",
@@ -1279,6 +1279,15 @@
"settingsErrorUpdateDescription": "Произошла ошибка при обновлении настроек",
"sidebarCollapse": "Свернуть",
"sidebarExpand": "Развернуть",
"productUpdateMoreInfo": "{noOfUpdates} больше обновлений",
"productUpdateInfo": "{noOfUpdates} обновлений",
"productUpdateWhatsNew": "Что нового",
"productUpdateTitle": "Обновления продуктов",
"productUpdateEmpty": "Нет обновлений",
"dismissAll": "Отклонить все",
"pangolinUpdateAvailable": "Доступна новая версия",
"pangolinUpdateAvailableInfo": "Версия {version} готова к установке",
"pangolinUpdateAvailableReleaseNotes": "Просмотреть заметки о выпуске",
"newtUpdateAvailable": "Доступно обновление",
"newtUpdateAvailableInfo": "Доступна новая версия Newt. Пожалуйста, обновитесь до последней версии для лучшего опыта.",
"domainPickerEnterDomain": "Домен",
@@ -1525,6 +1534,12 @@
"resourcesTableTheseResourcesForUseWith": "Эти ресурсы предназначены для использования с",
"resourcesTableClients": "Клиенты",
"resourcesTableAndOnlyAccessibleInternally": "и доступны только внутренне при подключении с клиентом.",
"resourcesTableNoTargets": "Нет ярлыков",
"resourcesTableHealthy": "Здоровые",
"resourcesTableDegraded": "Ухудшение",
"resourcesTableOffline": "Оффлайн",
"resourcesTableUnknown": "Неизвестен",
"resourcesTableNotMonitored": "Не отслеживается",
"editInternalResourceDialogEditClientResource": "Редактировать ресурс клиента",
"editInternalResourceDialogUpdateResourceProperties": "Обновите свойства ресурса и настройку цели для {resourceName}.",
"editInternalResourceDialogResourceProperties": "Свойства ресурса",
@@ -2080,5 +2095,46 @@
"supportSending": "Отправка...",
"supportSend": "Отправить",
"supportMessageSent": "Сообщение отправлено!",
"supportWillContact": "Мы скоро свяжемся с Вами!"
"supportWillContact": "Мы скоро свяжемся с Вами!",
"selectLogRetention": "Выберите удержание журнала",
"showColumns": "Показать колонки",
"hideColumns": "Скрыть столбцы",
"columnVisibility": "Видимость столбцов",
"toggleColumn": "Столбец {columnName}",
"allColumns": "Все колонки",
"defaultColumns": "Столбцы по умолчанию",
"customizeView": "Настроить вид",
"viewOptions": "Параметры просмотра",
"selectAll": "Выделить все",
"selectNone": "Не выбирать",
"selectedResources": "Выбранные ресурсы",
"enableSelected": "Включить выбранные",
"disableSelected": "Отключить выбранные",
"checkSelectedStatus": "Проверить статус выбранных",
"credentials": "Полномочия",
"savecredentials": "Сохранить учетные данные",
"regeneratecredentials": "Пере-ключ",
"regenerateCredentials": "Сгенерировать и сохранить ваши учетные данные",
"generatedcredentials": "Сгенерированные учетные данные",
"copyandsavethesecredentials": "Копировать и сохранить эти учетные данные",
"copyandsavethesecredentialsdescription": "Эти учетные данные не будут отображаться снова после того, как вы покинете эту страницу. Сохраните их сейчас.",
"credentialsSaved": "Учетные данные сохранены",
"credentialsSavedDescription": "Учетные данные были успешно восстановлены и сохранены.",
"credentialsSaveError": "Ошибка сохранения учетных данных",
"credentialsSaveErrorDescription": "Произошла ошибка при восстановлении и сохранении учетных данных.",
"regenerateCredentialsWarning": "Восстановление учётных данных приведет к недействительным предыдущим. Убедитесь, что все конфигурации, использующие эти учетные данные.",
"confirm": "Подтвердить",
"regenerateCredentialsConfirmation": "Вы уверены, что хотите восстановить учетные данные?",
"endpoint": "Endpoint",
"Id": "Id",
"SecretKey": "Секретный ключ",
"featureDisabledTooltip": "Эта функция доступна только в плане предприятия и требует лицензии на ее использование.",
"niceId": "Неплохой ID",
"niceIdUpdated": "Хороший ID обновлен",
"niceIdUpdatedSuccessfully": "Неплохой ID успешно обновлен",
"niceIdUpdateError": "Ошибка обновления Nice ID",
"niceIdUpdateErrorDescription": "Произошла ошибка при обновлении Nice ID.",
"niceIdCannotBeEmpty": "Неправильный ID не может быть пустым",
"enterIdentifier": "Введите идентификатор",
"identifier": "Identifier"
}

View File

@@ -179,7 +179,7 @@
"baseDomain": "Temel Alan Adı",
"subdomnainDescription": "Kaynağınızın erişilebileceği alt alan adı.",
"resourceRawSettings": "TCP/UDP Ayarları",
"resourceRawSettingsDescription": "Kaynağınıza TCP/UDP üzerinden erişimin nasıl sağlanacağını yapılandırın",
"resourceRawSettingsDescription": "Kaynağınızın TCP/UDP üzerinden nasıl erişileceğini yapılandırın. Kaynağı, sunucudan erişebilmeniz için bir ana bilgisayar Pangolin sunucusundaki bir bağlantı noktasına eşlersiniz: sunucu genel-IP: eşlenen-bağlantı-noktası.",
"protocol": "Protokol",
"protocolSelect": "Bir protokol seçin",
"resourcePortNumber": "Port Numarası",
@@ -1165,13 +1165,13 @@
"sidebarDomains": "Alan Adları",
"sidebarBluePrints": "Planlar",
"blueprints": "Planlar",
"blueprintsDescription": "Planlar, kaynaklarınızı ve ayarlarını tanımlayan bildirimsel YAML yapılandırmalarıdır",
"blueprintsDescription": "Deklaratif yapılandırmaları uygulayın ve önceki çalışmaları görüntüleyin",
"blueprintAdd": "Plan Ekle",
"blueprintGoBack": "Tüm Planları Gör",
"blueprintCreate": "Plan Oluştur",
"blueprintCreateDescription2": "Yeni bir plan oluşturup uygulamak için aşağıdaki adımları izleyin",
"blueprintDetails": "Plan Detayları",
"blueprintDetailsDescription": "Plan çalıştırma detaylarını görün",
"blueprintDetails": "Mavi Yazılım Detayları",
"blueprintDetailsDescription": "Uygulanan mavi yazılımın sonucunu ve oluşan hataları görün",
"blueprintInfo": "Plan Bilgileri",
"message": "Mesaj",
"blueprintContentsDescription": "Altyapınızı tanımlayan YAML içeriğini tanımlayın",
@@ -1181,7 +1181,7 @@
"appliedAt": "Uygulama Zamanı",
"source": "Kaynak",
"contents": "İçerik",
"parsedContents": "Ayrıştırılmış İçerik",
"parsedContents": "Verilerin Ayrıştırılmış İçeriği (Salt Okunur)",
"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.",
"enableDockerSocketLink": "Daha fazla bilgi",
@@ -1279,6 +1279,15 @@
"settingsErrorUpdateDescription": "Ayarları güncellerken bir hata oluştu",
"sidebarCollapse": "Daralt",
"sidebarExpand": "Genişlet",
"productUpdateMoreInfo": "{noOfUpdates} daha fazla güncelleme",
"productUpdateInfo": "{noOfUpdates} güncellemeler",
"productUpdateWhatsNew": "Neler Yeni",
"productUpdateTitle": "Ürün Güncellemeleri",
"productUpdateEmpty": "Güncelleme yok",
"dismissAll": "Hepsini Kapat",
"pangolinUpdateAvailable": "Yeni sürüm mevcut",
"pangolinUpdateAvailableInfo": "Sürüm {version} yüklenmeye hazır",
"pangolinUpdateAvailableReleaseNotes": "Sürüm notlarını görüntüleyin",
"newtUpdateAvailable": "Güncelleme Mevcut",
"newtUpdateAvailableInfo": "Newt'in yeni bir versiyonu mevcut. En iyi deneyim için lütfen en son sürüme güncelleyin.",
"domainPickerEnterDomain": "Alan Adı",
@@ -1525,6 +1534,12 @@
"resourcesTableTheseResourcesForUseWith": "Bu kaynaklar ile kullanılmak için",
"resourcesTableClients": "İstemciler",
"resourcesTableAndOnlyAccessibleInternally": "veyalnızca bir istemci ile bağlandığında dahili olarak erişilebilir.",
"resourcesTableNoTargets": "Hedef yok",
"resourcesTableHealthy": "Sağlıklı",
"resourcesTableDegraded": "Düşük Performanslı",
"resourcesTableOffline": "Çevrimdışı",
"resourcesTableUnknown": "Bilinmiyor",
"resourcesTableNotMonitored": "İzlenmiyor",
"editInternalResourceDialogEditClientResource": "İstemci Kaynağı Düzenleyin",
"editInternalResourceDialogUpdateResourceProperties": "{resourceName} için kaynak özelliklerini ve hedef yapılandırmasını güncelleyin.",
"editInternalResourceDialogResourceProperties": "Kaynak Özellikleri",
@@ -2080,5 +2095,46 @@
"supportSending": "Gönderiliyor...",
"supportSend": "Gönder",
"supportMessageSent": "Mesaj Gönderildi!",
"supportWillContact": "En kısa sürede size geri döneceğiz!"
"supportWillContact": "En kısa sürede size geri döneceğiz!",
"selectLogRetention": "Kayıt saklama seç",
"showColumns": "Sütunları Göster",
"hideColumns": "Sütunları Gizle",
"columnVisibility": "Sütun Görünürlüğü",
"toggleColumn": "{columnName} sütununu aç/kapat",
"allColumns": "Tüm Sütunlar",
"defaultColumns": "Varsayılan Sütunlar",
"customizeView": "Görünümü Özelleştir",
"viewOptions": "Görünüm Seçenekleri",
"selectAll": "Tümünü Seç",
"selectNone": "Hiçbirini Seçme",
"selectedResources": "Seçilen Kaynaklar",
"enableSelected": "Seçilenleri Etkinleştir",
"disableSelected": "Seçilenleri Devre Dışı Bırak",
"checkSelectedStatus": "Seçilenlerin Durumunu Kontrol Et",
"credentials": "Kimlik Bilgileri",
"savecredentials": "Kimlik Bilgilerini Kaydet",
"regeneratecredentials": "Yeniden Anahtarla",
"regenerateCredentials": "Kimlik bilgilerinizi yeniden oluşturun ve kaydedin",
"generatedcredentials": "Oluşturulan Kimlik Bilgileri",
"copyandsavethesecredentials": "Bu kimlik bilgilerini kopyalayın ve kaydedin",
"copyandsavethesecredentialsdescription": "Bu sayfadan ayrıldıktan sonra bu kimlik bilgileri tekrar gösterilmeyecek. Onları şimdi güvenli bir şekilde saklayın.",
"credentialsSaved": "Kimlik Bilgileri Kaydedildi",
"credentialsSavedDescription": "Kimlik bilgileri başarılı bir şekilde yeniden oluşturuldu ve kaydedildi.",
"credentialsSaveError": "Kimlik Bilgileri Kayıt Hatası",
"credentialsSaveErrorDescription": "Kimlik bilgilerini yeniden oluştururken ve kaydederken bir hata oluştu.",
"regenerateCredentialsWarning": "Kimlik bilgilerini yeniden oluşturmak önceki bilgileri geçersiz kılacaktır. Bu kimlik bilgilerini kullanan tüm yapılandırmaları güncellediğinizden emin olun.",
"confirm": "Onayla",
"regenerateCredentialsConfirmation": "Kimlik bilgilerini yeniden oluşturmak istediğinizden emin misiniz?",
"endpoint": "Uç Nokta",
"Id": "Kimlik",
"SecretKey": "Gizli Anahtar",
"featureDisabledTooltip": "Bu özellik yalnızca kurumsal planda mevcuttur ve kullanmak için lisans gerektirir.",
"niceId": "Güzel Kimlik",
"niceIdUpdated": "Güzel Kimlik Güncellendi",
"niceIdUpdatedSuccessfully": "Güzel Kimlik Başarıyla Güncellendi",
"niceIdUpdateError": "Güzel Kimlik güncellenirken hata",
"niceIdUpdateErrorDescription": "Güzel Kimlik güncellenirken bir hata oluştu.",
"niceIdCannotBeEmpty": "Güzel Kimlik boş olamaz",
"enterIdentifier": "Tanımlayıcıyı girin",
"identifier": "Tanımlayıcı"
}

View File

@@ -131,7 +131,7 @@
"expireIn": "过期时间",
"neverExpire": "永不过期",
"shareExpireDescription": "过期时间是链接可以使用并提供对资源的访问时间。 此时间后,链接将不再工作,使用此链接的用户将失去对资源的访问。",
"shareSeeOnce": "您只能看到此链接。请确保复制它。",
"shareSeeOnce": "您只能看到一次此链接。请确保复制它。",
"shareAccessHint": "任何具有此链接的人都可以访问该资源。小心地分享它。",
"shareTokenUsage": "查看访问令牌使用情况",
"createLink": "创建链接",
@@ -179,7 +179,7 @@
"baseDomain": "根域名",
"subdomnainDescription": "您的资源可以访问的子域名。",
"resourceRawSettings": "TCP/UDP 设置",
"resourceRawSettingsDescription": "配置如何通过 TCP/UDP 访问您的资源",
"resourceRawSettingsDescription": "配置如何通过 TCP/UDP 访问您的资源。 您映射资源到主机Pangolin服务器上的端口这样您就可以访问服务器-公共-ip:mapped端口的资源。",
"protocol": "协议",
"protocolSelect": "选择协议",
"resourcePortNumber": "端口号",
@@ -1165,13 +1165,13 @@
"sidebarDomains": "域",
"sidebarBluePrints": "蓝图",
"blueprints": "蓝图",
"blueprintsDescription": "蓝图是用于定义资源及其设置的 YAML 声明配置",
"blueprintsDescription": "应用声明配置并查看先前运行的",
"blueprintAdd": "添加蓝图",
"blueprintGoBack": "查看所有蓝图",
"blueprintCreate": "创建蓝图",
"blueprintCreateDescription2": "按照下面的步骤创建和应用新的蓝图",
"blueprintDetails": "蓝图详细信息",
"blueprintDetailsDescription": "查看蓝图运行详情",
"blueprintDetailsDescription": "查看应用蓝图的结果和发生的任何错误",
"blueprintInfo": "蓝图信息",
"message": "留言",
"blueprintContentsDescription": "定义描述您基础设施的 YAML 内容",
@@ -1181,7 +1181,7 @@
"appliedAt": "应用于",
"source": "来源",
"contents": "目录",
"parsedContents": "解析内容",
"parsedContents": "解析内容 (只读)",
"enableDockerSocket": "启用 Docker 蓝图",
"enableDockerSocketDescription": "启用 Docker Socket 标签擦除蓝图标签。套接字路径必须提供给新的。",
"enableDockerSocketLink": "了解更多",
@@ -1279,6 +1279,15 @@
"settingsErrorUpdateDescription": "更新设置时发生错误",
"sidebarCollapse": "折叠",
"sidebarExpand": "展开",
"productUpdateMoreInfo": "{noOfUpdates} 个更新",
"productUpdateInfo": "{noOfUpdates} 个更新",
"productUpdateWhatsNew": "新功能",
"productUpdateTitle": "产品更新",
"productUpdateEmpty": "无更新",
"dismissAll": "关闭所有",
"pangolinUpdateAvailable": "新版本可用",
"pangolinUpdateAvailableInfo": "版本 {version} 已准备就绪",
"pangolinUpdateAvailableReleaseNotes": "查看发布笔记",
"newtUpdateAvailable": "更新可用",
"newtUpdateAvailableInfo": "新版本的 Newt 已可用。请更新到最新版本以获得最佳体验。",
"domainPickerEnterDomain": "域名",
@@ -1525,6 +1534,12 @@
"resourcesTableTheseResourcesForUseWith": "这些资源供...使用",
"resourcesTableClients": "客户端",
"resourcesTableAndOnlyAccessibleInternally": "且仅在与客户端连接时可内部访问。",
"resourcesTableNoTargets": "没有目标",
"resourcesTableHealthy": "健康的",
"resourcesTableDegraded": "降级",
"resourcesTableOffline": "离线的",
"resourcesTableUnknown": "未知的",
"resourcesTableNotMonitored": "未监视的",
"editInternalResourceDialogEditClientResource": "编辑客户端资源",
"editInternalResourceDialogUpdateResourceProperties": "更新{resourceName}的资源属性和目标配置。",
"editInternalResourceDialogResourceProperties": "资源属性",
@@ -2080,5 +2095,46 @@
"supportSending": "正在发送...",
"supportSend": "发送",
"supportMessageSent": "消息已发送!",
"supportWillContact": "我们很快就会联系起来!"
"supportWillContact": "我们很快就会联系起来!",
"selectLogRetention": "选择保留日志",
"showColumns": "显示列",
"hideColumns": "隐藏列",
"columnVisibility": "列可见性",
"toggleColumn": "切换 {columnName} 列",
"allColumns": "全部列",
"defaultColumns": "默认列",
"customizeView": "自定义视图",
"viewOptions": "查看选项",
"selectAll": "选择所有",
"selectNone": "没有选择",
"selectedResources": "选定的资源",
"enableSelected": "启用选中的",
"disableSelected": "禁用选中的",
"checkSelectedStatus": "检查选中的状态",
"credentials": "全权证书",
"savecredentials": "保存证书",
"regeneratecredentials": "重置键",
"regenerateCredentials": "重新生成和保存您的凭据",
"generatedcredentials": "生成的证书",
"copyandsavethesecredentials": "复制和保存这些凭据",
"copyandsavethesecredentialsdescription": "这些凭据将不会在您离开此页面后再显示。现在安全地保存。",
"credentialsSaved": "凭据已保存",
"credentialsSavedDescription": "已成功生成和保存凭据。",
"credentialsSaveError": "证书保存错误",
"credentialsSaveErrorDescription": "更新和保存凭据时出错。",
"regenerateCredentialsWarning": "重新生成凭据将使以前的凭据失效。请确保更新使用这些凭据的任何配置。",
"confirm": "确认",
"regenerateCredentialsConfirmation": "您确定要重新生成凭据吗?",
"endpoint": "Endpoint",
"Id": "Id",
"SecretKey": "秘密密钥",
"featureDisabledTooltip": "此功能仅在企业计划中可用,需要许可证才能使用。",
"niceId": "好的 ID",
"niceIdUpdated": "好的 ID 已更新",
"niceIdUpdatedSuccessfully": "Nice ID 更新成功",
"niceIdUpdateError": "更新Nice ID时出错",
"niceIdUpdateErrorDescription": "更新Nice ID时出错。",
"niceIdCannotBeEmpty": "好的 ID 不能为空",
"enterIdentifier": "输入标识符",
"identifier": "Identifier"
}

2099
messages/zh-TW.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,16 @@
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin();
/** @type {import("next").NextConfig} */
const nextConfig = {
const nextConfig: NextConfig = {
eslint: {
ignoreDuringBuilds: true
},
output: "standalone",
experimental: {
reactCompiler: true
},
output: "standalone"
};
export default withNextIntl(nextConfig);

10748
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,8 +22,8 @@
"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 && cp tsconfig.saas.json tsconfig.json",
"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:pg": "echo 'export * from \"./pg\";' > server/db/index.ts",
"set:sqlite": "echo 'export * from \"./sqlite\";\nexport const driver: \"pg\" | \"sqlite\" = \"sqlite\";' > server/db/index.ts",
"set:pg": "echo 'export * from \"./pg\";\nexport const driver: \"pg\" | \"sqlite\" = \"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: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",
@@ -32,27 +32,29 @@
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs"
},
"dependencies": {
"@asteasolutions/zod-to-openapi": "^7.3.4",
"@aws-sdk/client-s3": "3.908.0",
"@asteasolutions/zod-to-openapi": "8.1.0",
"@aws-sdk/client-s3": "3.922.0",
"@faker-js/faker": "^10.1.0",
"@headlessui/react": "^2.2.9",
"@hookform/resolvers": "5.2.2",
"@monaco-editor/react": "^4.7.0",
"@node-rs/argon2": "^2.0.2",
"@oslojs/crypto": "1.0.1",
"@oslojs/encoding": "1.1.0",
"@radix-ui/react-avatar": "1.1.10",
"@radix-ui/react-avatar": "1.1.11",
"@radix-ui/react-checkbox": "1.3.3",
"@radix-ui/react-collapsible": "1.1.12",
"@radix-ui/react-dialog": "1.1.15",
"@radix-ui/react-dropdown-menu": "2.1.16",
"@radix-ui/react-icons": "1.3.2",
"@radix-ui/react-label": "2.1.7",
"@radix-ui/react-label": "2.1.8",
"@radix-ui/react-popover": "1.1.15",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-radio-group": "1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "2.2.6",
"@radix-ui/react-separator": "1.1.7",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-separator": "1.1.8",
"@radix-ui/react-slot": "1.2.4",
"@radix-ui/react-switch": "1.2.6",
"@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-toast": "1.2.15",
@@ -63,9 +65,10 @@
"@simplewebauthn/browser": "^13.2.2",
"@simplewebauthn/server": "^13.2.2",
"@tailwindcss/forms": "^0.5.10",
"@tanstack/react-query": "^5.90.6",
"@tanstack/react-table": "8.21.3",
"arctic": "^3.7.0",
"axios": "^1.12.2",
"axios": "^1.13.2",
"better-sqlite3": "11.7.0",
"canvas-confetti": "1.9.4",
"class-variance-authority": "^0.7.1",
@@ -76,96 +79,103 @@
"cookies": "^0.9.1",
"cors": "2.8.5",
"crypto-js": "^4.2.0",
"d3": "^7.9.0",
"date-fns": "4.1.0",
"drizzle-orm": "0.44.7",
"eslint": "9.37.0",
"eslint-config-next": "15.5.6",
"eslint": "9.39.1",
"eslint-config-next": "16.0.3",
"express": "5.1.0",
"express-rate-limit": "8.1.0",
"glob": "11.0.3",
"express-rate-limit": "8.2.1",
"glob": "11.1.0",
"helmet": "8.1.0",
"http-errors": "2.0.0",
"i": "^0.3.7",
"input-otp": "1.4.2",
"ioredis": "5.8.2",
"jmespath": "^0.16.0",
"js-yaml": "4.1.0",
"js-yaml": "4.1.1",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.545.0",
"maxmind": "5.0.0",
"lucide-react": "^0.552.0",
"maxmind": "5.0.1",
"moment": "2.30.1",
"next": "15.5.6",
"next-intl": "^4.3.12",
"next": "15.5.7",
"next-intl": "^4.4.0",
"next-themes": "0.4.6",
"nextjs-toploader": "^3.9.17",
"node-cache": "5.1.2",
"node-fetch": "3.3.2",
"nodemailer": "7.0.10",
"npm": "^11.6.2",
"npm": "^11.6.4",
"nprogress": "^0.2.0",
"oslo": "1.2.1",
"pg": "^8.16.2",
"posthog-node": "^5.10.4",
"posthog-node": "^5.11.2",
"qrcode.react": "4.2.0",
"react": "19.2.0",
"react": "19.2.1",
"react-day-picker": "9.11.1",
"react-dom": "19.2.0",
"react-dom": "19.2.1",
"react-easy-sort": "^1.8.0",
"react-hook-form": "7.65.0",
"react-hook-form": "7.66.0",
"react-icons": "^5.5.0",
"rebuild": "0.1.2",
"recharts": "^2.15.4",
"reodotdev": "^1.0.0",
"resend": "^6.1.2",
"resend": "^6.4.2",
"semver": "^7.7.3",
"stripe": "18.2.1",
"swagger-ui-express": "^5.0.1",
"tailwind-merge": "3.3.1",
"topojson-client": "^3.1.0",
"tw-animate-css": "^1.3.8",
"uuid": "^13.0.0",
"vaul": "1.1.2",
"visionscarto-world-atlas": "^1.0.0",
"winston": "3.18.3",
"winston-daily-rotate-file": "5.0.0",
"ws": "8.18.3",
"yaml": "^2.8.1",
"yargs": "18.0.0",
"zod": "3.25.76",
"zod-validation-error": "3.5.2",
"@faker-js/faker": "^10.1.0"
"zod": "4.1.12",
"zod-validation-error": "5.0.0"
},
"devDependencies": {
"@dotenvx/dotenvx": "1.51.0",
"@dotenvx/dotenvx": "1.51.1",
"@esbuild-plugins/tsconfig-paths": "0.1.2",
"@react-email/preview-server": "4.3.2",
"@tailwindcss/postcss": "^4.1.16",
"@tailwindcss/postcss": "^4.1.17",
"@tanstack/react-query-devtools": "^5.90.2",
"@types/better-sqlite3": "7.6.12",
"@types/cookie-parser": "1.4.10",
"@types/cors": "2.8.19",
"@types/crypto-js": "^4.2.2",
"@types/d3": "^7.4.3",
"@types/express": "5.0.5",
"@types/express-session": "^1.18.2",
"@types/jmespath": "^0.15.2",
"@types/js-yaml": "4.0.9",
"@types/jsonwebtoken": "^9.0.10",
"@types/nprogress": "^0.2.3",
"@types/node": "24.9.2",
"@types/node": "24.10.1",
"@types/nodemailer": "7.0.3",
"@types/nprogress": "^0.2.3",
"@types/pg": "8.15.6",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"@types/semver": "^7.7.1",
"@types/swagger-ui-express": "^4.1.8",
"@types/topojson-client": "^3.1.5",
"@types/ws": "8.18.1",
"@types/yargs": "17.0.34",
"babel-plugin-react-compiler": "^1.0.0",
"drizzle-kit": "0.31.6",
"esbuild": "0.25.11",
"esbuild-node-externals": "1.18.0",
"esbuild": "0.27.0",
"esbuild-node-externals": "1.19.1",
"postcss": "^8",
"react-email": "4.3.2",
"tailwindcss": "^4.1.4",
"tsc-alias": "1.8.16",
"tsx": "4.20.6",
"typescript": "^5",
"typescript-eslint": "^8.46.2"
"typescript-eslint": "^8.46.3"
},
"overrides": {
"emblor": {

View File

@@ -79,6 +79,12 @@ export function createApiServer() {
// Add request timeout middleware
apiServer.use(requestTimeoutMiddleware(60000)); // 60 second timeout
apiServer.use(logIncomingMiddleware);
if (build !== "oss") {
apiServer.use(`${prefix}/hybrid`, hybridRouter); // put before rate limiting because we will rate limit there separately because some of the routes are heavily used
}
if (!dev) {
apiServer.use(
rateLimit({
@@ -101,11 +107,7 @@ export function createApiServer() {
}
// API routes
apiServer.use(logIncomingMiddleware);
apiServer.use(prefix, unauthenticated);
if (build !== "oss") {
apiServer.use(`${prefix}/hybrid`, hybridRouter);
}
apiServer.use(prefix, authenticated);
// WebSocket routes

View File

@@ -19,6 +19,7 @@ export enum ActionsEnum {
getSite = "getSite",
listSites = "listSites",
updateSite = "updateSite",
reGenerateSecret = "reGenerateSecret",
createResource = "createResource",
deleteResource = "deleteResource",
getResource = "getResource",
@@ -85,6 +86,7 @@ export enum ActionsEnum {
updateOrgDomain = "updateOrgDomain",
getDNSRecords = "getDNSRecords",
createNewt = "createNewt",
createOlm = "createOlm",
createIdp = "createIdp",
updateIdp = "updateIdp",
deleteIdp = "deleteIdp",

View File

@@ -36,13 +36,15 @@ export async function createSession(
const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(token))
);
const session: Session = {
sessionId: sessionId,
userId,
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
issuedAt: new Date().getTime()
};
await db.insert(sessions).values(session);
const [session] = await db
.insert(sessions)
.values({
sessionId: sessionId,
userId,
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
issuedAt: new Date().getTime()
})
.returning();
return session;
}

View File

@@ -1,9 +1,43 @@
import { Request } from "express";
import { validateSessionToken, SESSION_COOKIE_NAME } from "@server/auth/sessions/app";
import {
validateSessionToken,
SESSION_COOKIE_NAME
} from "@server/auth/sessions/app";
export async function verifySession(req: Request) {
export async function verifySession(req: Request, forceLogin?: boolean) {
const res = await validateSessionToken(
req.cookies[SESSION_COOKIE_NAME] ?? "",
req.cookies[SESSION_COOKIE_NAME] ?? ""
);
if (!forceLogin) {
return res;
}
if (!res.session || !res.user) {
return {
session: null,
user: null
};
}
if (res.session.deviceAuthUsed) {
return {
session: null,
user: null
};
}
if (!res.session.issuedAt) {
return {
session: null,
user: null
};
}
const mins = 5 * 60 * 1000;
const now = new Date().getTime();
if (now - res.session.issuedAt > mins) {
return {
session: null,
user: null
};
}
return res;
}

View File

@@ -42,11 +42,17 @@ export async function getUniqueResourceName(orgId: string): Promise<string> {
}
const name = generateName();
const count = await db
.select({ niceId: resources.niceId, orgId: resources.orgId })
.from(resources)
.where(and(eq(resources.niceId, name), eq(resources.orgId, orgId)));
if (count.length === 0) {
const [resourceCount, siteResourceCount] = await Promise.all([
db
.select({ niceId: resources.niceId, orgId: resources.orgId })
.from(resources)
.where(and(eq(resources.niceId, name), eq(resources.orgId, orgId))),
db
.select({ niceId: siteResources.niceId, orgId: siteResources.orgId })
.from(siteResources)
.where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId)))
]);
if (resourceCount.length === 0 && siteResourceCount.length === 0) {
return name;
}
loops++;
@@ -61,11 +67,17 @@ export async function getUniqueSiteResourceName(orgId: string): Promise<string>
}
const name = generateName();
const count = await db
.select({ niceId: siteResources.niceId, orgId: siteResources.orgId })
.from(siteResources)
.where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId)));
if (count.length === 0) {
const [resourceCount, siteResourceCount] = await Promise.all([
db
.select({ niceId: resources.niceId, orgId: resources.orgId })
.from(resources)
.where(and(eq(resources.niceId, name), eq(resources.orgId, orgId))),
db
.select({ niceId: siteResources.niceId, orgId: siteResources.orgId })
.from(siteResources)
.where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId)))
]);
if (resourceCount.length === 0 && siteResourceCount.length === 0) {
return name;
}
loops++;

View File

@@ -13,9 +13,12 @@ function createDb() {
connection_string: process.env.POSTGRES_CONNECTION_STRING
};
if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) {
const replicas = process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(",").map((conn) => ({
connection_string: conn.trim()
}));
const replicas =
process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(
","
).map((conn) => ({
connection_string: conn.trim()
}));
config.postgres.replicas = replicas;
}
} else {
@@ -40,28 +43,44 @@ function createDb() {
connectionString,
max: poolConfig?.max_connections || 20,
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000,
connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000
});
const replicas = [];
if (!replicaConnections.length) {
replicas.push(DrizzlePostgres(primaryPool));
replicas.push(
DrizzlePostgres(primaryPool, {
logger: process.env.NODE_ENV === "development"
})
);
} else {
for (const conn of replicaConnections) {
const replicaPool = new Pool({
connectionString: conn.connection_string,
max: poolConfig?.max_replica_connections || 20,
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000,
connectionTimeoutMillis:
poolConfig?.connection_timeout_ms || 5000
});
replicas.push(DrizzlePostgres(replicaPool));
replicas.push(
DrizzlePostgres(replicaPool, {
logger: process.env.NODE_ENV === "development"
})
);
}
}
return withReplicas(DrizzlePostgres(primaryPool), replicas as any);
return withReplicas(
DrizzlePostgres(primaryPool, {
logger: process.env.QUERY_LOGGING === "true"
}),
replicas as any
);
}
export const db = createDb();
export default db;
export type Transaction = Parameters<Parameters<typeof db["transaction"]>[0]>[0];
export type Transaction = Parameters<
Parameters<(typeof db)["transaction"]>[0]
>[0];

View File

@@ -11,6 +11,7 @@ const runMigrations = async () => {
migrationsFolder: migrationsFolder
});
console.log("Migrations completed successfully.");
process.exit(0);
} catch (error) {
console.error("Error running migrations:", error);
process.exit(1);

View File

@@ -167,6 +167,7 @@ export const remoteExitNodes = pgTable("remoteExitNode", {
secretHash: varchar("secretHash").notNull(),
dateCreated: varchar("dateCreated").notNull(),
version: varchar("version"),
secondaryVersion: varchar("secondaryVersion"), // This is to detect the new nodes after the transition to pangolin-node
exitNodeId: integer("exitNodeId").references(() => exitNodes.exitNodeId, {
onDelete: "cascade"
})

View File

@@ -11,6 +11,7 @@ import {
} from "drizzle-orm/pg-core";
import { InferSelectModel } from "drizzle-orm";
import { randomUUID } from "crypto";
import { alias } from "yargs";
export const domains = pgTable("domains", {
domainId: varchar("domainId").primaryKey(),
@@ -40,6 +41,7 @@ export const orgs = pgTable("orgs", {
orgId: varchar("orgId").primaryKey(),
name: varchar("name").notNull(),
subnet: varchar("subnet"),
utilitySubnet: varchar("utilitySubnet"), // this is the subnet for utility addresses
createdAt: text("createdAt"),
requireTwoFactor: boolean("requireTwoFactor"),
maxSessionLengthHours: integer("maxSessionLengthHours"),
@@ -88,8 +90,7 @@ export const sites = pgTable("sites", {
publicKey: varchar("publicKey"),
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
listenPort: integer("listenPort"),
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true)
});
export const resources = pgTable("resources", {
@@ -175,7 +176,8 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
hcFollowRedirects: boolean("hcFollowRedirects").default(true),
hcMethod: varchar("hcMethod").default("GET"),
hcStatus: integer("hcStatus"), // http code
hcHealth: text("hcHealth").default("unknown") // "unknown", "healthy", "unhealthy"
hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy"
hcTlsServerName: text("hcTlsServerName"),
});
export const exitNodes = pgTable("exitNodes", {
@@ -204,11 +206,41 @@ export const siteResources = pgTable("siteResources", {
.references(() => orgs.orgId, { onDelete: "cascade" }),
niceId: varchar("niceId").notNull(),
name: varchar("name").notNull(),
protocol: varchar("protocol").notNull(),
proxyPort: integer("proxyPort").notNull(),
destinationPort: integer("destinationPort").notNull(),
destinationIp: varchar("destinationIp").notNull(),
enabled: boolean("enabled").notNull().default(true)
mode: varchar("mode").notNull(), // "host" | "cidr" | "port"
protocol: varchar("protocol"), // only for port mode
proxyPort: integer("proxyPort"), // only for port mode
destinationPort: integer("destinationPort"), // only for port mode
destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode
enabled: boolean("enabled").notNull().default(true),
alias: varchar("alias"),
aliasAddress: varchar("aliasAddress")
});
export const clientSiteResources = pgTable("clientSiteResources", {
clientId: integer("clientId")
.notNull()
.references(() => clients.clientId, { onDelete: "cascade" }),
siteResourceId: integer("siteResourceId")
.notNull()
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
});
export const roleSiteResources = pgTable("roleSiteResources", {
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" }),
siteResourceId: integer("siteResourceId")
.notNull()
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
});
export const userSiteResources = pgTable("userSiteResources", {
userId: varchar("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
siteResourceId: integer("siteResourceId")
.notNull()
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
});
export const users = pgTable("user", {
@@ -256,7 +288,8 @@ export const sessions = pgTable("session", {
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
issuedAt: bigint("issuedAt", { mode: "number" })
issuedAt: bigint("issuedAt", { mode: "number" }),
deviceAuthUsed: boolean("deviceAuthUsed").notNull().default(false)
});
export const newtSessions = pgTable("newtSession", {
@@ -598,7 +631,7 @@ export const idpOrg = pgTable("idpOrg", {
});
export const clients = pgTable("clients", {
clientId: serial("id").primaryKey(),
clientId: serial("clientId").primaryKey(),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
@@ -607,6 +640,11 @@ export const clients = pgTable("clients", {
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
onDelete: "set null"
}),
userId: text("userId").references(() => users.userId, {
// optionally tied to a user and in this case delete when the user deletes
onDelete: "cascade"
}),
olmId: text("olmId"), // to lock it to a specific olm optionally
name: varchar("name").notNull(),
pubKey: varchar("pubKey"),
subnet: varchar("subnet").notNull(),
@@ -621,23 +659,40 @@ export const clients = pgTable("clients", {
maxConnections: integer("maxConnections")
});
export const clientSites = pgTable("clientSites", {
clientId: integer("clientId")
.notNull()
.references(() => clients.clientId, { onDelete: "cascade" }),
siteId: integer("siteId")
.notNull()
.references(() => sites.siteId, { onDelete: "cascade" }),
isRelayed: boolean("isRelayed").notNull().default(false),
endpoint: varchar("endpoint")
});
export const clientSitesAssociationsCache = pgTable(
"clientSitesAssociationsCache",
{
clientId: integer("clientId") // not a foreign key here so after its deleted the rebuild function can delete it and send the message
.notNull(),
siteId: integer("siteId").notNull(),
isRelayed: boolean("isRelayed").notNull().default(false),
endpoint: varchar("endpoint"),
publicKey: varchar("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
}
);
export const clientSiteResourcesAssociationsCache = pgTable(
"clientSiteResourcesAssociationsCache",
{
clientId: integer("clientId") // not a foreign key here so after its deleted the rebuild function can delete it and send the message
.notNull(),
siteResourceId: integer("siteResourceId").notNull()
}
);
export const olms = pgTable("olms", {
olmId: varchar("id").primaryKey(),
secretHash: varchar("secretHash").notNull(),
dateCreated: varchar("dateCreated").notNull(),
version: text("version"),
agent: text("agent"),
name: varchar("name"),
clientId: integer("clientId").references(() => clients.clientId, {
// we will switch this depending on the current org it wants to connect to
onDelete: "set null"
}),
userId: text("userId").references(() => users.userId, {
// optionally tied to a user and in this case delete when the user deletes
onDelete: "cascade"
})
});
@@ -753,6 +808,21 @@ export const requestAuditLog = pgTable(
]
);
export const deviceWebAuthCodes = pgTable("deviceWebAuthCodes", {
codeId: serial("codeId").primaryKey(),
code: text("code").notNull().unique(),
ip: text("ip"),
city: text("city"),
deviceName: text("deviceName"),
applicationName: text("applicationName").notNull(),
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
verified: boolean("verified").notNull().default(false),
userId: varchar("userId").references(() => users.userId, {
onDelete: "cascade"
})
});
export type Org = InferSelectModel<typeof orgs>;
export type User = InferSelectModel<typeof users>;
export type Site = InferSelectModel<typeof sites>;
@@ -793,7 +863,7 @@ export type ApiKey = InferSelectModel<typeof apiKeys>;
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
export type Client = InferSelectModel<typeof clients>;
export type ClientSite = InferSelectModel<typeof clientSites>;
export type ClientSite = InferSelectModel<typeof clientSitesAssociationsCache>;
export type Olm = InferSelectModel<typeof olms>;
export type OlmSession = InferSelectModel<typeof olmSessions>;
export type UserClient = InferSelectModel<typeof userClients>;
@@ -808,4 +878,5 @@ export type Blueprint = InferSelectModel<typeof blueprints>;
export type LicenseKey = InferSelectModel<typeof licenseKey>;
export type SecurityKey = InferSelectModel<typeof securityKeys>;
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;

View File

@@ -14,8 +14,7 @@ bootstrapVolume();
function createDb() {
const sqlite = new Database(location);
return DrizzleSqlite(sqlite, {
schema,
logger: process.env.NODE_ENV === "development"
schema
});
}

View File

@@ -162,6 +162,7 @@ export const remoteExitNodes = sqliteTable("remoteExitNode", {
secretHash: text("secretHash").notNull(),
dateCreated: text("dateCreated").notNull(),
version: text("version"),
secondaryVersion: text("secondaryVersion"), // This is to detect the new nodes after the transition to pangolin-node
exitNodeId: integer("exitNodeId").references(() => exitNodes.exitNodeId, {
onDelete: "cascade"
})

View File

@@ -1,7 +1,7 @@
import { randomUUID } from "crypto";
import { InferSelectModel } from "drizzle-orm";
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
import { boolean } from "yargs";
import { no } from "zod/v4/locales";
export const domains = sqliteTable("domains", {
domainId: text("domainId").primaryKey(),
@@ -25,15 +25,15 @@ export const dnsRecords = sqliteTable("dnsRecords", {
recordType: text("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT"
baseDomain: text("baseDomain"),
value: text("value").notNull(),
verified: integer("verified", { mode: "boolean" }).notNull().default(false),
value: text("value").notNull(),
verified: integer("verified", { mode: "boolean" }).notNull().default(false)
});
export const orgs = sqliteTable("orgs", {
orgId: text("orgId").primaryKey(),
name: text("name").notNull(),
subnet: text("subnet"),
utilitySubnet: text("utilitySubnet"), // this is the subnet for utility addresses
createdAt: text("createdAt"),
requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" }),
maxSessionLengthHours: integer("maxSessionLengthHours"), // hours
@@ -95,8 +95,7 @@ export const sites = sqliteTable("sites", {
listenPort: integer("listenPort"),
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
.notNull()
.default(true),
remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access
.default(true)
});
export const resources = sqliteTable("resources", {
@@ -142,9 +141,10 @@ export const resources = sqliteTable("resources", {
onDelete: "set null"
}),
headers: text("headers"), // comma-separated list of headers to add to the request
proxyProtocol: integer("proxyProtocol", { mode: "boolean" }).notNull().default(false),
proxyProtocol: integer("proxyProtocol", { mode: "boolean" })
.notNull()
.default(false),
proxyProtocolVersion: integer("proxyProtocolVersion").default(1)
});
export const targets = sqliteTable("targets", {
@@ -195,7 +195,8 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
}).default(true),
hcMethod: text("hcMethod").default("GET"),
hcStatus: integer("hcStatus"), // http code
hcHealth: text("hcHealth").default("unknown") // "unknown", "healthy", "unhealthy"
hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy"
hcTlsServerName: text("hcTlsServerName"),
});
export const exitNodes = sqliteTable("exitNodes", {
@@ -226,11 +227,41 @@ export const siteResources = sqliteTable("siteResources", {
.references(() => orgs.orgId, { onDelete: "cascade" }),
niceId: text("niceId").notNull(),
name: text("name").notNull(),
protocol: text("protocol").notNull(),
proxyPort: integer("proxyPort").notNull(),
destinationPort: integer("destinationPort").notNull(),
destinationIp: text("destinationIp").notNull(),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true)
mode: text("mode").notNull(), // "host" | "cidr" | "port"
protocol: text("protocol"), // only for port mode
proxyPort: integer("proxyPort"), // only for port mode
destinationPort: integer("destinationPort"), // only for port mode
destination: text("destination").notNull(), // ip, cidr, hostname
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
alias: text("alias"),
aliasAddress: text("aliasAddress")
});
export const clientSiteResources = sqliteTable("clientSiteResources", {
clientId: integer("clientId")
.notNull()
.references(() => clients.clientId, { onDelete: "cascade" }),
siteResourceId: integer("siteResourceId")
.notNull()
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
});
export const roleSiteResources = sqliteTable("roleSiteResources", {
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" }),
siteResourceId: integer("siteResourceId")
.notNull()
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
});
export const userSiteResources = sqliteTable("userSiteResources", {
userId: text("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
siteResourceId: integer("siteResourceId")
.notNull()
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
});
export const users = sqliteTable("user", {
@@ -306,7 +337,7 @@ export const newts = sqliteTable("newt", {
});
export const clients = sqliteTable("clients", {
clientId: integer("id").primaryKey({ autoIncrement: true }),
clientId: integer("clientId").primaryKey({ autoIncrement: true }),
orgId: text("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
@@ -315,8 +346,14 @@ export const clients = sqliteTable("clients", {
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
onDelete: "set null"
}),
userId: text("userId").references(() => users.userId, {
// optionally tied to a user and in this case delete when the user deletes
onDelete: "cascade"
}),
name: text("name").notNull(),
pubKey: text("pubKey"),
olmId: text("olmId"), // to lock it to a specific olm optionally
subnet: text("subnet").notNull(),
megabytesIn: integer("bytesIn"),
megabytesOut: integer("bytesOut"),
@@ -328,25 +365,42 @@ export const clients = sqliteTable("clients", {
lastHolePunch: integer("lastHolePunch")
});
export const clientSites = sqliteTable("clientSites", {
clientId: integer("clientId")
.notNull()
.references(() => clients.clientId, { onDelete: "cascade" }),
siteId: integer("siteId")
.notNull()
.references(() => sites.siteId, { onDelete: "cascade" }),
isRelayed: integer("isRelayed", { mode: "boolean" })
.notNull()
.default(false),
endpoint: text("endpoint")
});
export const clientSitesAssociationsCache = sqliteTable(
"clientSitesAssociationsCache",
{
clientId: integer("clientId") // not a foreign key here so after its deleted the rebuild function can delete it and send the message
.notNull(),
siteId: integer("siteId").notNull(),
isRelayed: integer("isRelayed", { mode: "boolean" })
.notNull()
.default(false),
endpoint: text("endpoint"),
publicKey: text("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
}
);
export const clientSiteResourcesAssociationsCache = sqliteTable(
"clientSiteResourcesAssociationsCache",
{
clientId: integer("clientId") // not a foreign key here so after its deleted the rebuild function can delete it and send the message
.notNull(),
siteResourceId: integer("siteResourceId").notNull()
}
);
export const olms = sqliteTable("olms", {
olmId: text("id").primaryKey(),
secretHash: text("secretHash").notNull(),
dateCreated: text("dateCreated").notNull(),
version: text("version"),
agent: text("agent"),
name: text("name"),
clientId: integer("clientId").references(() => clients.clientId, {
// we will switch this depending on the current org it wants to connect to
onDelete: "set null"
}),
userId: text("userId").references(() => users.userId, {
// optionally tied to a user and in this case delete when the user deletes
onDelete: "cascade"
})
});
@@ -365,7 +419,10 @@ export const sessions = sqliteTable("session", {
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
expiresAt: integer("expiresAt").notNull(),
issuedAt: integer("issuedAt")
issuedAt: integer("issuedAt"),
deviceAuthUsed: integer("deviceAuthUsed", { mode: "boolean" })
.notNull()
.default(false)
});
export const newtSessions = sqliteTable("newtSession", {
@@ -802,6 +859,21 @@ export const requestAuditLog = sqliteTable(
]
);
export const deviceWebAuthCodes = sqliteTable("deviceWebAuthCodes", {
codeId: integer("codeId").primaryKey({ autoIncrement: true }),
code: text("code").notNull().unique(),
ip: text("ip"),
city: text("city"),
deviceName: text("deviceName"),
applicationName: text("applicationName").notNull(),
expiresAt: integer("expiresAt").notNull(),
createdAt: integer("createdAt").notNull(),
verified: integer("verified", { mode: "boolean" }).notNull().default(false),
userId: text("userId").references(() => users.userId, {
onDelete: "cascade"
})
});
export type Org = InferSelectModel<typeof orgs>;
export type User = InferSelectModel<typeof users>;
export type Site = InferSelectModel<typeof sites>;
@@ -840,7 +912,7 @@ export type ResourceRule = InferSelectModel<typeof resourceRules>;
export type Domain = InferSelectModel<typeof domains>;
export type DnsRecord = InferSelectModel<typeof dnsRecords>;
export type Client = InferSelectModel<typeof clients>;
export type ClientSite = InferSelectModel<typeof clientSites>;
export type ClientSite = InferSelectModel<typeof clientSitesAssociationsCache>;
export type RoleClient = InferSelectModel<typeof roleClients>;
export type UserClient = InferSelectModel<typeof userClients>;
export type SupporterKey = InferSelectModel<typeof supporterKey>;
@@ -859,3 +931,4 @@ export type LicenseKey = InferSelectModel<typeof licenseKey>;
export type SecurityKey = InferSelectModel<typeof securityKeys>;
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;

View File

@@ -11,6 +11,7 @@ import {
ApiKeyOrg,
RemoteExitNode,
Session,
SiteResource,
User,
UserOrg
} from "@server/db";
@@ -77,6 +78,8 @@ declare global {
userOrgId?: string;
userOrgIds?: string[];
remoteExitNode?: RemoteExitNode;
siteResource?: SiteResource;
orgPolicyAllowed?: boolean;
}
}
}

View File

@@ -122,19 +122,17 @@ export async function applyBlueprint({
)
.limit(1);
if (site) {
logger.debug(
`Updating client resource ${result.resource.siteResourceId} on site ${site.sites.siteId}`
);
logger.debug(
`Updating client resource ${result.resource.siteResourceId} on site ${site.sites.siteId}`
);
await addClientTargets(
site.newt.newtId,
result.resource.destinationIp,
result.resource.destinationPort,
result.resource.protocol,
result.resource.proxyPort
);
}
// await addClientTargets(
// site.newt.newtId,
// result.resource.destination,
// result.resource.destinationPort,
// result.resource.protocol,
// result.resource.proxyPort
// );
}
blueprintSucceeded = true;

View File

@@ -29,6 +29,15 @@ export async function applyNewtDockerBlueprint(
logger.debug(`Received Docker blueprint: ${JSON.stringify(blueprint)}`);
// make sure this is not an empty object
if (isEmptyObject(blueprint)) {
return;
}
if (isEmptyObject(blueprint["proxy-resources"]) && isEmptyObject(blueprint["client-resources"])) {
return;
}
// Update the blueprint in the database
await applyBlueprint({
orgId: site.orgId,
@@ -42,7 +51,7 @@ export async function applyNewtDockerBlueprint(
type: "newt/blueprint/results",
data: {
success: false,
message: `Failed to update database from config: ${error}`
message: `Failed to apply blueprint from config: ${error}`
}
});
return;
@@ -56,3 +65,10 @@ export async function applyNewtDockerBlueprint(
}
});
}
function isEmptyObject(obj: any) {
if (obj === null || obj === undefined) {
return true;
}
return Object.keys(obj).length === 0 && obj.constructor === Object;
}

View File

@@ -75,8 +75,9 @@ export async function updateClientResources(
.set({
name: resourceData.name || resourceNiceId,
siteId: site.siteId,
mode: "port",
proxyPort: resourceData["proxy-port"]!,
destinationIp: resourceData.hostname,
destination: resourceData.hostname,
destinationPort: resourceData["internal-port"],
protocol: resourceData.protocol
})
@@ -98,8 +99,9 @@ export async function updateClientResources(
siteId: site.siteId,
niceId: resourceNiceId,
name: resourceData.name || resourceNiceId,
mode: "port",
proxyPort: resourceData["proxy-port"]!,
destinationIp: resourceData.hostname,
destination: resourceData.hostname,
destinationPort: resourceData["internal-port"],
protocol: resourceData.protocol
})

View File

@@ -30,6 +30,7 @@ import { pickPort } from "@server/routers/target/helpers";
import { resourcePassword } from "@server/db";
import { hashPassword } from "@server/auth/password";
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
import { get } from "http";
export type ProxyResourcesResults = {
proxyResource: Resource;
@@ -114,7 +115,12 @@ export async function updateProxyResources(
internalPort: internalPortToCreate,
path: targetData.path,
pathMatchType: targetData["path-match"],
rewritePath: targetData.rewritePath,
rewritePath:
targetData.rewritePath ||
targetData["rewrite-path"] ||
(targetData["rewrite-match"] === "stripPrefix"
? "/"
: undefined),
rewritePathType: targetData["rewrite-match"],
priority: targetData.priority
})
@@ -139,10 +145,14 @@ export async function updateProxyResources(
hcHostname: healthcheckData?.hostname,
hcPort: healthcheckData?.port,
hcInterval: healthcheckData?.interval,
hcUnhealthyInterval: healthcheckData?.unhealthyInterval,
hcUnhealthyInterval:
healthcheckData?.unhealthyInterval ||
healthcheckData?.["unhealthy-interval"],
hcTimeout: healthcheckData?.timeout,
hcHeaders: hcHeaders,
hcFollowRedirects: healthcheckData?.followRedirects,
hcFollowRedirects:
healthcheckData?.followRedirects ||
healthcheckData?.["follow-redirects"],
hcMethod: healthcheckData?.method,
hcStatus: healthcheckData?.status,
hcHealth: "unknown"
@@ -211,6 +221,7 @@ export async function updateProxyResources(
domainId: domain ? domain.domainId : null,
enabled: resourceEnabled,
sso: resourceData.auth?.["sso-enabled"] || false,
skipToIdpId: resourceData.auth?.["auto-login-idp"] || null,
ssl: resourceSsl,
setHostHeader: resourceData["host-header"] || null,
tlsServerName: resourceData["tls-server-name"] || null,
@@ -392,7 +403,12 @@ export async function updateProxyResources(
enabled: targetData.enabled,
path: targetData.path,
pathMatchType: targetData["path-match"],
rewritePath: targetData.rewritePath,
rewritePath:
targetData.rewritePath ||
targetData["rewrite-path"] ||
(targetData["rewrite-match"] === "stripPrefix"
? "/"
: undefined),
rewritePathType: targetData["rewrite-match"],
priority: targetData.priority
})
@@ -452,10 +468,13 @@ export async function updateProxyResources(
hcPort: healthcheckData?.port,
hcInterval: healthcheckData?.interval,
hcUnhealthyInterval:
healthcheckData?.unhealthyInterval,
healthcheckData?.unhealthyInterval ||
healthcheckData?.["unhealthy-interval"],
hcTimeout: healthcheckData?.timeout,
hcHeaders: hcHeaders,
hcFollowRedirects: healthcheckData?.followRedirects,
hcFollowRedirects:
healthcheckData?.followRedirects ||
healthcheckData?.["follow-redirects"],
hcMethod: healthcheckData?.method,
hcStatus: healthcheckData?.status
})
@@ -527,7 +546,7 @@ export async function updateProxyResources(
if (
existingRule.action !== getRuleAction(rule.action) ||
existingRule.match !== rule.match.toUpperCase() ||
existingRule.value !== rule.value.toUpperCase()
existingRule.value !== getRuleValue(rule.match.toUpperCase(), rule.value)
) {
validateRule(rule);
await trx
@@ -535,7 +554,7 @@ export async function updateProxyResources(
.set({
action: getRuleAction(rule.action),
match: rule.match.toUpperCase(),
value: rule.value.toUpperCase(),
value: getRuleValue(rule.match.toUpperCase(), rule.value),
})
.where(
eq(resourceRules.ruleId, existingRule.ruleId)
@@ -547,7 +566,7 @@ export async function updateProxyResources(
resourceId: existingResource.resourceId,
action: getRuleAction(rule.action),
match: rule.match.toUpperCase(),
value: rule.value.toUpperCase(),
value: getRuleValue(rule.match.toUpperCase(), rule.value),
priority: index + 1 // start priorities at 1
});
}
@@ -592,6 +611,7 @@ export async function updateProxyResources(
domainId: domain ? domain.domainId : null,
enabled: resourceEnabled,
sso: resourceData.auth?.["sso-enabled"] || false,
skipToIdpId: resourceData.auth?.["auto-login-idp"] || null,
setHostHeader: resourceData["host-header"] || null,
tlsServerName: resourceData["tls-server-name"] || null,
ssl: resourceSsl,
@@ -705,7 +725,7 @@ export async function updateProxyResources(
resourceId: newResource.resourceId,
action: getRuleAction(rule.action),
match: rule.match.toUpperCase(),
value: rule.value.toUpperCase(),
value: getRuleValue(rule.match.toUpperCase(), rule.value),
priority: index + 1 // start priorities at 1
});
}
@@ -735,6 +755,14 @@ function getRuleAction(input: string) {
return action;
}
function getRuleValue(match: string, value: string) {
// if the match is a country, uppercase the value
if (match == "COUNTRY") {
return value.toUpperCase();
}
return value;
}
function validateRule(rule: any) {
if (rule.match === "cidr") {
if (!isValidCIDR(rule.value)) {
@@ -763,10 +791,6 @@ async function syncRoleResources(
.where(eq(roleResources.resourceId, resourceId));
for (const roleName of ssoRoles) {
if (roleName === "Admin") {
continue; // never add admin access
}
const [role] = await trx
.select()
.from(roles)
@@ -777,6 +801,10 @@ async function syncRoleResources(
throw new Error(`Role not found: ${roleName} in org ${orgId}`);
}
if (role.isAdmin) {
continue; // never add admin access
}
const existingRoleResource = existingRoleResources.find(
(rr) => rr.roleId === role.roleId
);

View File

@@ -7,18 +7,20 @@ export const SiteSchema = z.object({
export const TargetHealthCheckSchema = z.object({
hostname: z.string(),
port: z.number().int().min(1).max(65535),
port: z.int().min(1).max(65535),
enabled: z.boolean().optional().default(true),
path: z.string().optional(),
scheme: z.string().optional(),
mode: z.string().default("http"),
interval: z.number().int().default(30),
unhealthyInterval: z.number().int().default(30),
timeout: z.number().int().default(5),
interval: z.int().default(30),
"unhealthy-interval": z.int().default(30),
unhealthyInterval: z.int().optional(), // deprecated alias
timeout: z.int().default(5),
headers: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional().default(null),
followRedirects: z.boolean().default(true),
"follow-redirects": z.boolean().default(true),
followRedirects: z.boolean().optional(), // deprecated alias
method: z.string().default("GET"),
status: z.number().int().optional()
status: z.int().optional()
});
// Schema for individual target within a resource
@@ -26,15 +28,16 @@ export const TargetSchema = z.object({
site: z.string().optional(),
method: z.enum(["http", "https", "h2c"]).optional(),
hostname: z.string(),
port: z.number().int().min(1).max(65535),
port: z.int().min(1).max(65535),
enabled: z.boolean().optional().default(true),
"internal-port": z.number().int().min(1).max(65535).optional(),
"internal-port": z.int().min(1).max(65535).optional(),
path: z.string().optional(),
"path-match": z.enum(["exact", "prefix", "regex"]).optional().nullable(),
healthcheck: TargetHealthCheckSchema.optional(),
rewritePath: z.string().optional(),
rewritePath: z.string().optional(), // deprecated alias
"rewrite-path": z.string().optional(),
"rewrite-match": z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(),
priority: z.number().int().min(1).max(1000).optional().default(100)
priority: z.int().min(1).max(1000).optional().default(100)
});
export type TargetData = z.infer<typeof TargetSchema>;
@@ -52,10 +55,11 @@ export const AuthSchema = z.object({
.optional()
.default([])
.refine((roles) => !roles.includes("Admin"), {
message: "Admin role cannot be included in sso-roles"
error: "Admin role cannot be included in sso-roles"
}),
"sso-users": z.array(z.string().email()).optional().default([]),
"whitelist-users": z.array(z.string().email()).optional().default([]),
"sso-users": z.array(z.email()).optional().default([]),
"whitelist-users": z.array(z.email()).optional().default([]),
"auto-login-idp": z.int().positive().optional(),
});
export const RuleSchema = z.object({
@@ -76,7 +80,7 @@ export const ResourceSchema = z
protocol: z.enum(["http", "tcp", "udp"]).optional(),
ssl: z.boolean().optional(),
"full-domain": z.string().optional(),
"proxy-port": z.number().int().min(1).max(65535).optional(),
"proxy-port": z.int().min(1).max(65535).optional(),
enabled: z.boolean().optional(),
targets: z.array(TargetSchema.nullable()).optional().default([]),
auth: AuthSchema.optional(),
@@ -97,9 +101,8 @@ export const ResourceSchema = z
);
},
{
message:
"Resource must either be targets-only (only 'targets' field) or have both 'name' and 'protocol' fields at a minimum",
path: ["name", "protocol"]
path: ["name", "protocol"],
error: "Resource must either be targets-only (only 'targets' field) or have both 'name' and 'protocol' fields at a minimum"
}
)
.refine(
@@ -114,6 +117,20 @@ export const ResourceSchema = z
(target) => target == null || target.method !== undefined
);
}
return true;
},
{
path: ["targets"],
error: "When protocol is 'http', all targets must have a 'method' field"
}
)
.refine(
(resource) => {
if (isTargetsOnlyResource(resource)) {
return true;
}
// If protocol is tcp or udp, no target should have method field
if (resource.protocol === "tcp" || resource.protocol === "udp") {
return resource.targets.every(
@@ -122,19 +139,9 @@ export const ResourceSchema = z
}
return true;
},
(resource) => {
if (resource.protocol === "http") {
return {
message:
"When protocol is 'http', all targets must have a 'method' field",
path: ["targets"]
};
}
return {
message:
"When protocol is 'tcp' or 'udp', targets must not have a 'method' field",
path: ["targets"]
};
{
path: ["targets"],
error: "When protocol is 'tcp' or 'udp', targets must not have a 'method' field"
}
)
.refine(
@@ -153,9 +160,8 @@ export const ResourceSchema = z
return true;
},
{
message:
"When protocol is 'http', a 'full-domain' must be provided",
path: ["full-domain"]
path: ["full-domain"],
error: "When protocol is 'http', a 'full-domain' must be provided"
}
)
.refine(
@@ -171,9 +177,8 @@ export const ResourceSchema = z
return true;
},
{
message:
"When protocol is 'tcp' or 'udp', 'proxy-port' must be provided",
path: ["proxy-port", "exit-node"]
path: ["proxy-port", "exit-node"],
error: "When protocol is 'tcp' or 'udp', 'proxy-port' must be provided"
}
)
.refine(
@@ -190,9 +195,8 @@ export const ResourceSchema = z
return true;
},
{
message:
"When protocol is 'tcp' or 'udp', 'auth' must not be provided",
path: ["auth"]
path: ["auth"],
error: "When protocol is 'tcp' or 'udp', 'auth' must not be provided"
}
);
@@ -213,36 +217,12 @@ export const ClientResourceSchema = z.object({
// Schema for the entire configuration object
export const ConfigSchema = z
.object({
"proxy-resources": z.record(z.string(), ResourceSchema).optional().default({}),
"client-resources": z.record(z.string(), ClientResourceSchema).optional().default({}),
sites: z.record(z.string(), SiteSchema).optional().default({})
"proxy-resources": z.record(z.string(), ResourceSchema).optional().prefault({}),
"client-resources": z.record(z.string(), ClientResourceSchema).optional().prefault({}),
sites: z.record(z.string(), SiteSchema).optional().prefault({})
})
.refine(
// Enforce the full-domain uniqueness across resources in the same stack
(config) => {
// Extract all full-domain values with their resource keys
const fullDomainMap = new Map<string, string[]>();
Object.entries(config["proxy-resources"]).forEach(
([resourceKey, resource]) => {
const fullDomain = resource["full-domain"];
if (fullDomain) {
// Only process if full-domain is defined
if (!fullDomainMap.has(fullDomain)) {
fullDomainMap.set(fullDomain, []);
}
fullDomainMap.get(fullDomain)!.push(resourceKey);
}
}
);
// Find duplicates
const duplicates = Array.from(fullDomainMap.entries()).filter(
([_, resourceKeys]) => resourceKeys.length > 1
);
return duplicates.length === 0;
},
(config) => {
// Extract duplicates for error message
const fullDomainMap = new Map<string, string[]>();
@@ -268,38 +248,16 @@ export const ConfigSchema = z
)
.join("; ");
return {
message: `Duplicate 'full-domain' values found: ${duplicates}`,
path: ["resources"]
};
if (duplicates.length !== 0) {
return {
path: ["resources"],
error: `Duplicate 'full-domain' values found: ${duplicates}`
};
}
}
)
.refine(
// Enforce proxy-port uniqueness within proxy-resources per protocol
(config) => {
const protocolPortMap = new Map<string, string[]>();
Object.entries(config["proxy-resources"]).forEach(
([resourceKey, resource]) => {
const proxyPort = resource["proxy-port"];
const protocol = resource.protocol;
if (proxyPort !== undefined && protocol !== undefined) {
const key = `${protocol}:${proxyPort}`;
if (!protocolPortMap.has(key)) {
protocolPortMap.set(key, []);
}
protocolPortMap.get(key)!.push(resourceKey);
}
}
);
// Find duplicates
const duplicates = Array.from(protocolPortMap.entries()).filter(
([_, resourceKeys]) => resourceKeys.length > 1
);
return duplicates.length === 0;
},
(config) => {
// Extract duplicates for error message
const protocolPortMap = new Map<string, string[]>();
@@ -328,36 +286,16 @@ export const ConfigSchema = z
)
.join("; ");
return {
message: `Duplicate 'proxy-port' values found in proxy-resources: ${duplicates}`,
path: ["proxy-resources"]
};
if (duplicates.length !== 0) {
return {
path: ["proxy-resources"],
error: `Duplicate 'proxy-port' values found in proxy-resources: ${duplicates}`
};
}
}
)
.refine(
// Enforce proxy-port uniqueness within client-resources
(config) => {
const proxyPortMap = new Map<number, string[]>();
Object.entries(config["client-resources"]).forEach(
([resourceKey, resource]) => {
const proxyPort = resource["proxy-port"];
if (proxyPort !== undefined) {
if (!proxyPortMap.has(proxyPort)) {
proxyPortMap.set(proxyPort, []);
}
proxyPortMap.get(proxyPort)!.push(resourceKey);
}
}
);
// Find duplicates
const duplicates = Array.from(proxyPortMap.entries()).filter(
([_, resourceKeys]) => resourceKeys.length > 1
);
return duplicates.length === 0;
},
(config) => {
// Extract duplicates for error message
const proxyPortMap = new Map<number, string[]>();
@@ -382,10 +320,12 @@ export const ConfigSchema = z
)
.join("; ");
return {
message: `Duplicate 'proxy-port' values found in client-resources: ${duplicates}`,
path: ["client-resources"]
};
if (duplicates.length !== 0) {
return {
path: ["client-resources"],
error: `Duplicate 'proxy-port' values found in client-resources: ${duplicates}`
};
}
}
);

View File

@@ -0,0 +1,286 @@
import {
clients,
db,
olms,
orgs,
roleClients,
roles,
userClients,
userOrgs,
Transaction
} from "@server/db";
import { eq, and, notInArray } from "drizzle-orm";
import { listExitNodes } from "#dynamic/lib/exitNodes";
import { getNextAvailableClientSubnet } from "@server/lib/ip";
import logger from "@server/logger";
import { rebuildClientAssociationsFromClient } from "./rebuildClientAssociations";
import { sendTerminateClient } from "@server/routers/client/terminate";
export async function calculateUserClientsForOrgs(
userId: string,
trx?: Transaction
): Promise<void> {
const execute = async (transaction: Transaction) => {
// Get all OLMs for this user
const userOlms = await transaction
.select()
.from(olms)
.where(eq(olms.userId, userId));
if (userOlms.length === 0) {
// No OLMs for this user, but we should still clean up any orphaned clients
await cleanupOrphanedClients(userId, transaction);
return;
}
// Get all user orgs
const allUserOrgs = await transaction
.select()
.from(userOrgs)
.where(eq(userOrgs.userId, userId));
const userOrgIds = allUserOrgs.map((uo) => uo.orgId);
// For each OLM, ensure there's a client in each org the user is in
for (const olm of userOlms) {
for (const userOrg of allUserOrgs) {
const orgId = userOrg.orgId;
const [org] = await transaction
.select()
.from(orgs)
.where(eq(orgs.orgId, orgId));
if (!org) {
logger.warn(
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): org not found`
);
continue;
}
if (!org.subnet) {
logger.warn(
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): org has no subnet configured`
);
continue;
}
// Get admin role for this org (needed for access grants)
const [adminRole] = await transaction
.select()
.from(roles)
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
.limit(1);
if (!adminRole) {
logger.warn(
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no admin role found`
);
continue;
}
// Check if a client already exists for this OLM+user+org combination
const [existingClient] = await transaction
.select()
.from(clients)
.where(
and(
eq(clients.userId, userId),
eq(clients.orgId, orgId),
eq(clients.olmId, olm.olmId)
)
)
.limit(1);
if (existingClient) {
// Ensure admin role has access to the client
const [existingRoleClient] = await transaction
.select()
.from(roleClients)
.where(
and(
eq(roleClients.roleId, adminRole.roleId),
eq(
roleClients.clientId,
existingClient.clientId
)
)
)
.limit(1);
if (!existingRoleClient) {
await transaction.insert(roleClients).values({
roleId: adminRole.roleId,
clientId: existingClient.clientId
});
logger.debug(
`Granted admin role access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
);
}
// Ensure user has access to the client
const [existingUserClient] = await transaction
.select()
.from(userClients)
.where(
and(
eq(userClients.userId, userId),
eq(
userClients.clientId,
existingClient.clientId
)
)
)
.limit(1);
if (!existingUserClient) {
await transaction.insert(userClients).values({
userId,
clientId: existingClient.clientId
});
logger.debug(
`Granted user access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
);
}
logger.debug(
`Client already exists for OLM ${olm.olmId} in org ${orgId} (user ${userId}), skipping creation`
);
continue;
}
// Get exit nodes for this org
const exitNodesList = await listExitNodes(orgId);
if (exitNodesList.length === 0) {
logger.warn(
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no exit nodes found`
);
continue;
}
const randomExitNode =
exitNodesList[
Math.floor(Math.random() * exitNodesList.length)
];
// Get next available subnet
const newSubnet = await getNextAvailableClientSubnet(orgId);
if (!newSubnet) {
logger.warn(
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no available subnet found`
);
continue;
}
const subnet = newSubnet.split("/")[0];
const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`;
// Create the client
const [newClient] = await transaction
.insert(clients)
.values({
userId,
orgId: userOrg.orgId,
exitNodeId: randomExitNode.exitNodeId,
name: olm.name || "User Client",
subnet: updatedSubnet,
olmId: olm.olmId,
type: "olm"
})
.returning();
await rebuildClientAssociationsFromClient(
newClient,
transaction
);
// Grant admin role access to the client
await transaction.insert(roleClients).values({
roleId: adminRole.roleId,
clientId: newClient.clientId
});
// Grant user access to the client
await transaction.insert(userClients).values({
userId,
clientId: newClient.clientId
});
logger.debug(
`Created client for OLM ${olm.olmId} in org ${orgId} (user ${userId}) with access granted to admin role and user`
);
}
}
// Clean up clients in orgs the user is no longer in
await cleanupOrphanedClients(userId, transaction, userOrgIds);
};
if (trx) {
// Use provided transaction
await execute(trx);
} else {
// Create new transaction
await db.transaction(async (transaction) => {
await execute(transaction);
});
}
}
async function cleanupOrphanedClients(
userId: string,
trx: Transaction,
userOrgIds: string[] = []
): Promise<void> {
// Find all OLM clients for this user that should be deleted
// If userOrgIds is empty, delete all OLM clients (user has no orgs)
// If userOrgIds has values, delete clients in orgs they're not in
const clientsToDelete = await trx
.select({ clientId: clients.clientId })
.from(clients)
.where(
userOrgIds.length > 0
? and(
eq(clients.userId, userId),
notInArray(clients.orgId, userOrgIds)
)
: and(eq(clients.userId, userId))
);
if (clientsToDelete.length > 0) {
const deletedClients = await trx
.delete(clients)
.where(
userOrgIds.length > 0
? and(
eq(clients.userId, userId),
notInArray(clients.orgId, userOrgIds)
)
: and(eq(clients.userId, userId))
)
.returning();
// Rebuild associations for each deleted client to clean up related data
for (const deletedClient of deletedClients) {
await rebuildClientAssociationsFromClient(deletedClient, trx);
if (deletedClient.olmId) {
await sendTerminateClient(
deletedClient.clientId,
deletedClient.olmId
);
}
}
if (userOrgIds.length === 0) {
logger.debug(
`Deleted all ${clientsToDelete.length} OLM client(s) for user ${userId} (user has no orgs)`
);
} else {
logger.debug(
`Deleted ${clientsToDelete.length} orphaned OLM client(s) for user ${userId} in orgs they're no longer in`
);
}
}
}

View File

@@ -85,7 +85,13 @@ export class Config {
? "true"
: "false";
process.env.FLAGS_ENABLE_CLIENTS = parsedConfig.flags?.enable_clients
process.env.PRODUCT_UPDATES_NOTIFICATION_ENABLED = parsedConfig.app
.notifications.product_updates
? "true"
: "false";
process.env.NEW_RELEASES_NOTIFICATION_ENABLED = parsedConfig.app
.notifications.new_releases
? "true"
: "false";
@@ -158,7 +164,7 @@ export class Config {
try {
const response = await fetch(
"https://api.fossorial.io/api/v1/license/validate",
`https://api.fossorial.io/api/v1/license/validate`,
{
method: "POST",
headers: {

View File

@@ -2,7 +2,7 @@ import path from "path";
import { fileURLToPath } from "url";
// This is a placeholder value replaced by the build process
export const APP_VERSION = "1.12.0";
export const APP_VERSION = "1.12.3";
export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME);

View File

@@ -18,6 +18,7 @@ import { defaultRoleAllowedActions } from "@server/routers/role";
import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/billing";
import { createCustomer } from "#dynamic/lib/billing";
import { usageService } from "@server/lib/billing/usageService";
import config from "@server/lib/config";
export async function createUserAccountOrg(
userId: string,
@@ -76,6 +77,8 @@ export async function createUserAccountOrg(
.from(domains)
.where(eq(domains.configManaged, true));
const utilitySubnet = config.getRawConfig().orgs.utility_subnet_group;
const newOrg = await trx
.insert(orgs)
.values({
@@ -83,6 +86,7 @@ export async function createUserAccountOrg(
name,
// subnet
subnet: "100.90.128.0/24", // TODO: this should not be hardcoded - or can it be the same in all orgs?
utilitySubnet: utilitySubnet,
createdAt: new Date().toISOString()
})
.returning();

View File

@@ -1,7 +1,15 @@
import { db } from "@server/db";
import {
clientSitesAssociationsCache,
db,
SiteResource,
siteResources,
Transaction
} from "@server/db";
import { clients, orgs, sites } from "@server/db";
import { and, eq, isNotNull } from "drizzle-orm";
import config from "@server/lib/config";
import z from "zod";
import logger from "@server/logger";
interface IPRange {
start: bigint;
@@ -279,6 +287,56 @@ export async function getNextAvailableClientSubnet(
return subnet;
}
export async function getNextAvailableAliasAddress(
orgId: string
): Promise<string> {
const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId));
if (!org) {
throw new Error(`Organization with ID ${orgId} not found`);
}
if (!org.subnet) {
throw new Error(`Organization with ID ${orgId} has no subnet defined`);
}
if (!org.utilitySubnet) {
throw new Error(
`Organization with ID ${orgId} has no utility subnet defined`
);
}
const existingAddresses = await db
.select({
aliasAddress: siteResources.aliasAddress
})
.from(siteResources)
.where(
and(
isNotNull(siteResources.aliasAddress),
eq(siteResources.orgId, orgId)
)
);
const addresses = [
...existingAddresses.map(
(site) => `${site.aliasAddress?.split("/")[0]}/32`
),
// reserve a /29 for the dns server and other stuff
`${org.utilitySubnet.split("/")[0]}/29`
].filter((address) => address !== null) as string[];
let subnet = findNextAvailableCidr(addresses, 32, org.utilitySubnet);
if (!subnet) {
throw new Error("No available subnets remaining in space");
}
// remove the cidr
subnet = subnet.split("/")[0];
return subnet;
}
export async function getNextAvailableOrgSubnet(): Promise<string> {
const existingAddresses = await db
.select({
@@ -300,3 +358,113 @@ export async function getNextAvailableOrgSubnet(): Promise<string> {
return subnet;
}
export function generateRemoteSubnets(allSiteResources: SiteResource[]): string[] {
const remoteSubnets = allSiteResources
.filter((sr) => {
if (sr.mode === "cidr") return true;
if (sr.mode === "host") {
// check if its a valid IP using zod
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
const parseResult = ipSchema.safeParse(sr.destination);
return parseResult.success;
}
return false;
})
.map((sr) => {
if (sr.mode === "cidr") return sr.destination;
if (sr.mode === "host") {
return `${sr.destination}/32`;
}
return ""; // This should never be reached due to filtering, but satisfies TypeScript
})
.filter((subnet) => subnet !== ""); // Remove empty strings just to be safe
// remove duplicates
return Array.from(new Set(remoteSubnets));
}
export type Alias = { alias: string | null; aliasAddress: string | null };
export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
let aliasConfigs = allSiteResources
.filter((sr) => sr.alias && sr.aliasAddress && sr.mode == "host")
.map((sr) => ({
alias: sr.alias,
aliasAddress: sr.aliasAddress
}));
return aliasConfigs;
}
export type SubnetProxyTarget = {
sourcePrefix: string; // must be a cidr
destPrefix: string; // must be a cidr
rewriteTo?: string; // must be a cidr
portRange?: {
min: number;
max: number;
}[];
};
export function generateSubnetProxyTargets(
siteResource: SiteResource,
clients: {
clientId: number;
pubKey: string | null;
subnet: string | null;
}[]
): SubnetProxyTarget[] {
const targets: SubnetProxyTarget[] = [];
if (clients.length === 0) {
logger.debug(
`No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.`
);
return [];
}
for (const clientSite of clients) {
if (!clientSite.subnet) {
logger.debug(
`Client ${clientSite.clientId} has no subnet, skipping for site resource ${siteResource.siteResourceId}.`
);
continue;
}
const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`;
if (siteResource.mode == "host") {
let destination = siteResource.destination;
// check if this is a valid ip
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
if (ipSchema.safeParse(destination).success) {
destination = `${destination}/32`;
targets.push({
sourcePrefix: clientPrefix,
destPrefix: destination
});
}
if (siteResource.alias && siteResource.aliasAddress) {
// also push a match for the alias address
targets.push({
sourcePrefix: clientPrefix,
destPrefix: `${siteResource.aliasAddress}/32`,
rewriteTo: destination
});
}
} else if (siteResource.mode == "cidr") {
targets.push({
sourcePrefix: clientPrefix,
destPrefix: siteResource.destination
});
}
}
// print a nice representation of the targets
// logger.debug(
// `Generated subnet proxy targets for: ${JSON.stringify(targets, null, 2)}`
// );
return targets;
}

111
server/lib/lock.ts Normal file
View File

@@ -0,0 +1,111 @@
export class LockManager {
/**
* Acquire a distributed lock using Redis SET with NX and PX options
* @param lockKey - Unique identifier for the lock
* @param ttlMs - Time to live in milliseconds
* @returns Promise<boolean> - true if lock acquired, false otherwise
*/
async acquireLock(
lockKey: string,
ttlMs: number = 30000
): Promise<boolean> {
return true;
}
/**
* Release a lock using Lua script to ensure atomicity
* @param lockKey - Unique identifier for the lock
*/
async releaseLock(lockKey: string): Promise<void> {}
/**
* Force release a lock regardless of owner (use with caution)
* @param lockKey - Unique identifier for the lock
*/
async forceReleaseLock(lockKey: string): Promise<void> {}
/**
* Check if a lock exists and get its info
* @param lockKey - Unique identifier for the lock
* @returns Promise<{exists: boolean, ownedByMe: boolean, ttl: number}>
*/
async getLockInfo(lockKey: string): Promise<{
exists: boolean;
ownedByMe: boolean;
ttl: number;
owner?: string;
}> {
return { exists: true, ownedByMe: true, ttl: 0 };
}
/**
* Extend the TTL of an existing lock owned by this worker
* @param lockKey - Unique identifier for the lock
* @param ttlMs - New TTL in milliseconds
* @returns Promise<boolean> - true if extended successfully
*/
async extendLock(lockKey: string, ttlMs: number): Promise<boolean> {
return true;
}
/**
* Attempt to acquire lock with retries and exponential backoff
* @param lockKey - Unique identifier for the lock
* @param ttlMs - Time to live in milliseconds
* @param maxRetries - Maximum number of retry attempts
* @param baseDelayMs - Base delay between retries in milliseconds
* @returns Promise<boolean> - true if lock acquired
*/
async acquireLockWithRetry(
lockKey: string,
ttlMs: number = 30000,
maxRetries: number = 5,
baseDelayMs: number = 100
): Promise<boolean> {
return true;
}
/**
* Execute a function while holding a lock
* @param lockKey - Unique identifier for the lock
* @param fn - Function to execute while holding the lock
* @param ttlMs - Lock TTL in milliseconds
* @returns Promise<T> - Result of the executed function
*/
async withLock<T>(
lockKey: string,
fn: () => Promise<T>,
ttlMs: number = 30000
): Promise<T> {
const acquired = await this.acquireLock(lockKey, ttlMs);
if (!acquired) {
throw new Error(`Failed to acquire lock: ${lockKey}`);
}
try {
return await fn();
} finally {
await this.releaseLock(lockKey);
}
}
/**
* Clean up expired locks - Redis handles this automatically, but this method
* can be used to get statistics about locks
* @returns Promise<{activeLocksCount: number, locksOwnedByMe: number}>
*/
async getLockStatistics(): Promise<{
activeLocksCount: number;
locksOwnedByMe: number;
}> {
return { activeLocksCount: 0, locksOwnedByMe: 0 };
}
/**
* Close the Redis connection
*/
async disconnect(): Promise<void> {}
}
export const lockManager = new LockManager();

View File

@@ -14,10 +14,8 @@ export const configSchema = z
.object({
app: z
.object({
dashboard_url: z
.string()
.url()
.pipe(z.string().url())
dashboard_url: z.url()
.pipe(z.url())
.transform((url) => url.toLowerCase())
.optional(),
log_level: z
@@ -31,7 +29,14 @@ export const configSchema = z
anonymous_usage: z.boolean().optional().default(true)
})
.optional()
.default({})
.prefault({}),
notifications: z
.object({
product_updates: z.boolean().optional().default(true),
new_releases: z.boolean().optional().default(true)
})
.optional()
.prefault({})
})
.optional()
.default({
@@ -40,6 +45,10 @@ export const configSchema = z
log_failed_attempts: false,
telemetry: {
anonymous_usage: true
},
notifications: {
product_updates: true,
new_releases: true
}
}),
domains: z
@@ -96,7 +105,7 @@ export const configSchema = z
token: z.string().optional().default("P-Access-Token")
})
.optional()
.default({}),
.prefault({}),
resource_session_request_param: z
.string()
.optional()
@@ -121,7 +130,7 @@ export const configSchema = z
credentials: z.boolean().optional()
})
.optional(),
trust_proxy: z.number().int().gte(0).optional().default(1),
trust_proxy: z.int().gte(0).optional().default(1),
secret: z.string().pipe(z.string().min(8)).optional(),
maxmind_db_path: z.string().optional()
})
@@ -178,7 +187,7 @@ export const configSchema = z
.default(5000)
})
.optional()
.default({})
.prefault({})
})
.optional(),
traefik: z
@@ -205,10 +214,13 @@ export const configSchema = z
.default(["newt", "wireguard", "local"]),
allow_raw_resources: z.boolean().optional().default(true),
file_mode: z.boolean().optional().default(false),
pp_transport_prefix: z.string().optional().default("pp-transport-v")
pp_transport_prefix: z
.string()
.optional()
.default("pp-transport-v")
})
.optional()
.default({}),
.prefault({}),
gerbil: z
.object({
exit_node_name: z.string().optional(),
@@ -217,6 +229,11 @@ export const configSchema = z
.default(51820)
.transform(stoi)
.pipe(portSchema),
clients_start_port: portSchema
.optional()
.default(21820)
.transform(stoi)
.pipe(portSchema),
base_endpoint: z
.string()
.optional()
@@ -233,16 +250,18 @@ export const configSchema = z
.default(30)
})
.optional()
.default({}),
.prefault({}),
orgs: z
.object({
block_size: z.number().positive().gt(0).optional().default(24),
subnet_group: z.string().optional().default("100.90.128.0/24")
subnet_group: z.string().optional().default("100.90.128.0/24"),
utility_subnet_group: z.string().optional().default("100.96.128.0/24") //just hardcode this for now as well
})
.optional()
.default({
block_size: 24,
subnet_group: "100.90.128.0/24"
subnet_group: "100.90.128.0/24",
utility_subnet_group: "100.96.128.0/24"
}),
rate_limits: z
.object({
@@ -262,7 +281,7 @@ export const configSchema = z
.default(500)
})
.optional()
.default({}),
.prefault({}),
auth: z
.object({
window_minutes: z
@@ -279,10 +298,10 @@ export const configSchema = z
.default(500)
})
.optional()
.default({})
.prefault({})
})
.optional()
.default({}),
.prefault({}),
email: z
.object({
smtp_host: z.string().optional(),
@@ -294,7 +313,7 @@ export const configSchema = z
.transform(getEnvOrYaml("EMAIL_SMTP_PASS")),
smtp_secure: z.boolean().optional(),
smtp_tls_reject_unauthorized: z.boolean().optional(),
no_reply: z.string().email().optional()
no_reply: z.email().optional()
})
.optional(),
flags: z
@@ -306,8 +325,7 @@ export const configSchema = z
enable_integration_api: z.boolean().optional(),
disable_local_sites: z.boolean().optional(),
disable_basic_wireguard_sites: z.boolean().optional(),
disable_config_managed_domains: z.boolean().optional(),
enable_clients: z.boolean().optional().default(true)
disable_config_managed_domains: z.boolean().optional()
})
.optional(),
dns: z
@@ -315,11 +333,18 @@ export const configSchema = z
nameservers: z
.array(z.string().optional().optional())
.optional()
.default(["ns1.pangolin.net", "ns2.pangolin.net", "ns3.pangolin.net"]),
cname_extension: z.string().optional().default("cname.pangolin.net")
.default([
"ns1.pangolin.net",
"ns2.pangolin.net",
"ns3.pangolin.net"
]),
cname_extension: z
.string()
.optional()
.default("cname.pangolin.net")
})
.optional()
.default({})
.prefault({})
})
.refine(
(data) => {
@@ -334,7 +359,7 @@ export const configSchema = z
return true;
},
{
message: "At least one domain must be defined"
error: "At least one domain must be defined"
}
)
.refine(
@@ -349,7 +374,7 @@ export const configSchema = z
);
},
{
message: "Server secret must be defined"
error: "Server secret must be defined"
}
)
.refine(
@@ -361,7 +386,7 @@ export const configSchema = z
);
},
{
message: "Dashboard URL must be defined"
error: "Dashboard URL must be defined"
}
);

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ import { getHostMeta } from "./hostMeta";
import logger from "@server/logger";
import { apiKeys, db, roles } from "@server/db";
import { sites, users, orgs, resources, clients, idp } from "@server/db";
import { eq, count, notInArray } from "drizzle-orm";
import { eq, count, notInArray, and } from "drizzle-orm";
import { APP_VERSION } from "./consts";
import crypto from "crypto";
import { UserType } from "@server/types/UserTypes";
@@ -113,7 +113,12 @@ class TelemetryClient {
const [customRoles] = await db
.select({ count: count() })
.from(roles)
.where(notInArray(roles.name, ["Admin", "Member"]));
.where(
and(
eq(roles.isAdmin, false),
notInArray(roles.name, ["Member"])
)
);
const adminUsers = await db
.select({ email: users.email })
@@ -188,7 +193,7 @@ class TelemetryClient {
license_tier: licenseStatus.tier || "unknown"
}
};
logger.debug("Sending enterprise startup telemtry payload:", {
logger.debug("Sending enterprise startup telemetry payload:", {
payload
});
// this.client.capture(payload);

View File

@@ -80,7 +80,8 @@ export async function getTraefikConfig(
subnet: sites.subnet,
exitNodeId: sites.exitNodeId,
// Domain cert resolver fields
domainCertResolver: domains.certResolver
domainCertResolver: domains.certResolver,
preferWildcardCert: domains.preferWildcardCert
})
.from(sites)
.innerJoin(targets, eq(targets.siteId, sites.siteId))
@@ -178,7 +179,8 @@ export async function getTraefikConfig(
rewritePathType: row.rewritePathType,
priority: priority,
// Store domain cert resolver fields
domainCertResolver: row.domainCertResolver
domainCertResolver: row.domainCertResolver,
preferWildcardCert: row.preferWildcardCert
});
}
@@ -343,9 +345,9 @@ export async function getTraefikConfig(
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) {
logger.error(
`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`

View File

@@ -1,11 +1,12 @@
import z from "zod";
import ipaddr from "ipaddr.js";
export function isValidCIDR(cidr: string): boolean {
return z.string().cidr().safeParse(cidr).success;
return z.cidrv4().safeParse(cidr).success || z.cidrv6().safeParse(cidr).success;
}
export function isValidIP(ip: string): boolean {
return z.string().ip().safeParse(ip).success;
return z.ipv4().safeParse(ip).success || z.ipv6().safeParse(ip).success;
}
export function isValidUrlGlobPattern(pattern: string): boolean {
@@ -68,11 +69,11 @@ export function isUrlValid(url: string | undefined) {
if (!url) return true; // the link is optional in the schema so if it's empty it's valid
var pattern = new RegExp(
"^(https?:\\/\\/)?" + // protocol
"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name
"((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address
"(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path
"(\\?[;&a-z\\d%_.~+=-]*)?" + // query string
"(\\#[-a-z\\d_]*)?$",
"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name
"((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address
"(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path
"(\\?[;&a-z\\d%_.~+=-]*)?" + // query string
"(\\#[-a-z\\d_]*)?$",
"i"
);
return !!pattern.test(url);
@@ -83,12 +84,15 @@ export function isTargetValid(value: string | undefined) {
const DOMAIN_REGEX =
/^[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9_])?(?:\.[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9_])?)*$/;
const IPV4_REGEX =
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i;
// const IPV4_REGEX =
// /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
// const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i;
if (IPV4_REGEX.test(value) || IPV6_REGEX.test(value)) {
return true;
try {
const addr = ipaddr.parse(value);
return addr.kind() === "ipv4" || addr.kind() === "ipv6";
} catch {
// fall through to domain regex check
}
return DOMAIN_REGEX.test(value);
@@ -169,10 +173,10 @@ export function isSecondLevelDomain(domain: string): boolean {
}
const trimmedDomain = domain.trim().toLowerCase();
// Split into parts
const parts = trimmedDomain.split('.');
// Should have exactly 2 parts for a second-level domain (e.g., "example.com")
if (parts.length !== 2) {
return false;

View File

@@ -11,6 +11,7 @@ export * from "./verifyRoleAccess";
export * from "./verifyUserAccess";
export * from "./verifyAdmin";
export * from "./verifySetResourceUsers";
export * from "./verifySetResourceClients";
export * from "./verifyUserInRole";
export * from "./verifyAccessTokenAccess";
export * from "./requestTimeout";
@@ -24,7 +25,7 @@ export * from "./integration";
export * from "./verifyUserHasAction";
export * from "./verifyApiKeyAccess";
export * from "./verifyDomainAccess";
export * from "./verifyClientsEnabled";
export * from "./verifyUserIsOrgOwner";
export * from "./verifySiteResourceAccess";
export * from "./logActionAudit";
export * from "./logActionAudit";
export * from "./verifyOlmAccess";

View File

@@ -7,6 +7,7 @@ export * from "./verifyApiKeyTargetAccess";
export * from "./verifyApiKeyRoleAccess";
export * from "./verifyApiKeyUserAccess";
export * from "./verifyApiKeySetResourceUsers";
export * from "./verifyApiKeySetResourceClients";
export * from "./verifyAccessTokenAccess";
export * from "./verifyApiKeyIsRoot";
export * from "./verifyApiKeyApiKeyAccess";

View File

@@ -0,0 +1,73 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { clients } from "@server/db";
import { and, eq, inArray } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyApiKeySetResourceClients(
req: Request,
res: Response,
next: NextFunction
) {
const apiKey = req.apiKey;
const singleClientId = req.params.clientId || req.body.clientId || req.query.clientId;
const { clientIds } = req.body;
const allClientIds = clientIds || (singleClientId ? [parseInt(singleClientId as string)] : []);
if (!apiKey) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
);
}
if (apiKey.isRoot) {
// Root keys can access any client in any org
return next();
}
if (!req.apiKeyOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have access to this organization"
)
);
}
if (allClientIds.length === 0) {
return next();
}
try {
const orgId = req.apiKeyOrg.orgId;
const clientsData = await db
.select()
.from(clients)
.where(
and(
inArray(clients.clientId, allClientIds),
eq(clients.orgId, orgId)
)
);
if (clientsData.length !== allClientIds.length) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have access to one or more specified clients"
)
);
}
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error checking if key has access to the specified clients"
)
);
}
}

View File

@@ -11,7 +11,9 @@ export async function verifyApiKeySetResourceUsers(
next: NextFunction
) {
const apiKey = req.apiKey;
const userIds = req.body.userIds;
const singleUserId = req.params.userId || req.body.userId || req.query.userId;
const { userIds } = req.body;
const allUserIds = userIds || (singleUserId ? [singleUserId] : []);
if (!apiKey) {
return next(
@@ -33,11 +35,7 @@ export async function verifyApiKeySetResourceUsers(
);
}
if (!userIds) {
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs"));
}
if (userIds.length === 0) {
if (allUserIds.length === 0) {
return next();
}
@@ -48,12 +46,12 @@ export async function verifyApiKeySetResourceUsers(
.from(userOrgs)
.where(
and(
inArray(userOrgs.userId, userIds),
inArray(userOrgs.userId, allUserIds),
eq(userOrgs.orgId, orgId)
)
);
if (userOrgsData.length !== userIds.length) {
if (userOrgsData.length !== allUserIds.length) {
return next(
createHttpError(
HttpCode.FORBIDDEN,

View File

@@ -13,8 +13,6 @@ export async function verifyApiKeySiteResourceAccess(
try {
const apiKey = req.apiKey;
const siteResourceId = parseInt(req.params.siteResourceId);
const siteId = parseInt(req.params.siteId);
const orgId = req.params.orgId;
if (!apiKey) {
return next(
@@ -22,11 +20,11 @@ export async function verifyApiKeySiteResourceAccess(
);
}
if (!siteResourceId || !siteId || !orgId) {
if (!siteResourceId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Missing required parameters"
"Missing siteResourceId parameter"
)
);
}
@@ -41,9 +39,7 @@ export async function verifyApiKeySiteResourceAccess(
.select()
.from(siteResources)
.where(and(
eq(siteResources.siteResourceId, siteResourceId),
eq(siteResources.siteId, siteId),
eq(siteResources.orgId, orgId)
eq(siteResources.siteResourceId, siteResourceId)
))
.limit(1);
@@ -64,11 +60,11 @@ export async function verifyApiKeySiteResourceAccess(
.where(
and(
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
eq(apiKeyOrg.orgId, orgId)
eq(apiKeyOrg.orgId, siteResource.orgId)
)
)
.limit(1);
if (apiKeyOrgRes.length === 0) {
return next(
createHttpError(
@@ -77,12 +73,11 @@ export async function verifyApiKeySiteResourceAccess(
)
);
}
req.apiKeyOrg = apiKeyOrgRes[0];
}
// Attach the siteResource to the request for use in the next middleware/route
// @ts-ignore - Extending Request type
req.siteResource = siteResource;
return next();

View File

@@ -5,6 +5,7 @@ import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { canUserAccessResource } from "@server/auth/canUserAccessResource";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
export async function verifyAccessTokenAccess(
req: Request,
@@ -96,6 +97,24 @@ export async function verifyAccessTokenAccess(
req.userOrgId = resource[0].orgId!;
}
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
const policyCheck = await checkOrgAccessPolicy({
orgId: req.userOrg.orgId,
userId,
session: req.session
});
req.orgPolicyAllowed = policyCheck.allowed;
if (!policyCheck.allowed || policyCheck.error) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Failed organization access policy check: " +
(policyCheck.error || "Unknown error")
)
);
}
}
const resourceAllowed = await canUserAccessResource({
userId,
resourceId,

View File

@@ -4,6 +4,7 @@ import { roles, userOrgs } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
export async function verifyAdmin(
req: Request,
@@ -43,6 +44,24 @@ export async function verifyAdmin(
);
}
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
const policyCheck = await checkOrgAccessPolicy({
orgId: req.userOrg.orgId,
userId,
session: req.session
});
req.orgPolicyAllowed = policyCheck.allowed;
if (!policyCheck.allowed || policyCheck.error) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Failed organization access policy check: " +
(policyCheck.error || "Unknown error")
)
);
}
}
const userRole = await db
.select()
.from(roles)

View File

@@ -4,6 +4,7 @@ import { userOrgs, apiKeys, apiKeyOrg } from "@server/db";
import { and, eq, or } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
export async function verifyApiKeyAccess(
req: Request,
@@ -84,6 +85,24 @@ export async function verifyApiKeyAccess(
);
}
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
const policyCheck = await checkOrgAccessPolicy({
orgId: req.userOrg.orgId,
userId,
session: req.session
});
req.orgPolicyAllowed = policyCheck.allowed;
if (!policyCheck.allowed || policyCheck.error) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Failed organization access policy check: " +
(policyCheck.error || "Unknown error")
)
);
}
}
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;

View File

@@ -4,6 +4,7 @@ import { userOrgs, clients, roleClients, userClients } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
export async function verifyClientAccess(
req: Request,
@@ -75,6 +76,24 @@ export async function verifyClientAccess(
);
}
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
const policyCheck = await checkOrgAccessPolicy({
orgId: req.userOrg.orgId,
userId,
session: req.session
});
req.orgPolicyAllowed = policyCheck.allowed;
if (!policyCheck.allowed || policyCheck.error) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Failed organization access policy check: " +
(policyCheck.error || "Unknown error")
)
);
}
}
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;
req.userOrgId = client.orgId;

View File

@@ -1,29 +0,0 @@
import { Request, Response, NextFunction } from "express";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import config from "@server/lib/config";
export async function verifyClientsEnabled(
req: Request,
res: Response,
next: NextFunction
) {
try {
if (!config.getRawConfig().flags?.enable_clients) {
return next(
createHttpError(
HttpCode.NOT_IMPLEMENTED,
"Clients are not enabled on this server."
)
);
}
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to check if clients are enabled"
)
);
}
}

View File

@@ -4,6 +4,7 @@ import { userOrgs, apiKeyOrg } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
export async function verifyDomainAccess(
req: Request,
@@ -78,6 +79,24 @@ export async function verifyDomainAccess(
);
}
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
const policyCheck = await checkOrgAccessPolicy({
orgId: req.userOrg.orgId,
userId,
session: req.session
});
req.orgPolicyAllowed = policyCheck.allowed;
if (!policyCheck.allowed || policyCheck.error) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Failed organization access policy check: " +
(policyCheck.error || "Unknown error")
)
);
}
}
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;

View File

@@ -0,0 +1,45 @@
import { Request, Response, NextFunction } from "express";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { db, olms } from "@server/db";
import { and, eq } from "drizzle-orm";
export async function verifyOlmAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
const userId = req.user!.userId;
const olmId = req.params.olmId || req.body.olmId || req.query.olmId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
const [existingOlm] = await db
.select()
.from(olms)
.where(and(eq(olms.olmId, olmId), eq(olms.userId, userId)));
if (!existingOlm) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this olm"
)
);
}
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error checking if user has access to this user"
)
);
}
}

View File

@@ -47,22 +47,22 @@ export async function verifyOrgAccess(
);
}
const policyCheck = await checkOrgAccessPolicy({
orgId,
userId,
session: req.session
});
logger.debug("Org check policy result", { policyCheck });
if (!policyCheck.allowed || policyCheck.error) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Failed organization access policy check: " +
(policyCheck.error || "Unknown error")
)
);
if (req.orgPolicyAllowed === undefined) {
const policyCheck = await checkOrgAccessPolicy({
orgId,
userId,
session: req.session
});
req.orgPolicyAllowed = policyCheck.allowed;
if (!policyCheck.allowed || policyCheck.error) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Failed organization access policy check: " +
(policyCheck.error || "Unknown error")
)
);
}
}
// User has access, attach the user's role to the request for potential future use

View File

@@ -1,14 +1,10 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import {
resources,
userOrgs,
userResources,
roleResources,
} from "@server/db";
import { resources, userOrgs, userResources, roleResources } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
export async function verifyResourceAccess(
req: Request,
@@ -73,6 +69,24 @@ export async function verifyResourceAccess(
);
}
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
const policyCheck = await checkOrgAccessPolicy({
orgId: req.userOrg.orgId,
userId,
session: req.session
});
req.orgPolicyAllowed = policyCheck.allowed;
if (!policyCheck.allowed || policyCheck.error) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Failed organization access policy check: " +
(policyCheck.error || "Unknown error")
)
);
}
}
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;
req.userOrgId = resource[0].orgId;

View File

@@ -5,6 +5,7 @@ import { and, eq, inArray } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import logger from "@server/logger";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
export async function verifyRoleAccess(
req: Request,
@@ -105,6 +106,33 @@ export async function verifyRoleAccess(
req.userOrgRoleId = userOrg[0].roleId;
}
if (!req.userOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this organization"
)
);
}
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
const policyCheck = await checkOrgAccessPolicy({
orgId: req.userOrg.orgId,
userId,
session: req.session
});
req.orgPolicyAllowed = policyCheck.allowed;
if (!policyCheck.allowed || policyCheck.error) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Failed organization access policy check: " +
(policyCheck.error || "Unknown error")
)
);
}
}
return next();
} catch (error) {
logger.error("Error verifying role access:", error);
@@ -116,4 +144,3 @@ export async function verifyRoleAccess(
);
}
}

View File

@@ -1,10 +1,5 @@
import { NextFunction, Response } from "express";
import ErrorResponse from "@server/types/ErrorResponse";
import { db } from "@server/db";
import { users } from "@server/db";
import { eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { verifySession } from "@server/auth/sessions/verifySession";
import { unauthorized } from "@server/auth/unauthorizedResponse";
@@ -13,24 +8,15 @@ export const verifySessionMiddleware = async (
res: Response<ErrorResponse>,
next: NextFunction
) => {
const { session, user } = await verifySession(req);
const { forceLogin } = req.query;
const { session, user } = await verifySession(req, forceLogin === "true");
if (!session || !user) {
return next(unauthorized());
}
const existingUser = await db
.select()
.from(users)
.where(eq(users.userId, user.userId));
if (!existingUser || !existingUser[0]) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "User does not exist")
);
}
req.user = existingUser[0];
req.user = user;
req.session = session;
next();
return next();
};

View File

@@ -0,0 +1,90 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { clients } from "@server/db";
import { and, eq, inArray } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
export async function verifySetResourceClients(
req: Request,
res: Response,
next: NextFunction
) {
const userId = req.user!.userId;
const singleClientId =
req.params.clientId || req.body.clientId || req.query.clientId;
const { clientIds } = req.body;
const allClientIds =
clientIds ||
(singleClientId ? [parseInt(singleClientId as string)] : []);
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (!req.userOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this organization"
)
);
}
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
const policyCheck = await checkOrgAccessPolicy({
orgId: req.userOrg.orgId,
userId,
session: req.session
});
req.orgPolicyAllowed = policyCheck.allowed;
if (!policyCheck.allowed || policyCheck.error) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Failed organization access policy check: " +
(policyCheck.error || "Unknown error")
)
);
}
}
if (allClientIds.length === 0) {
return next();
}
try {
const orgId = req.userOrg.orgId;
// get all clients for the clientIds
const clientsData = await db
.select()
.from(clients)
.where(
and(
inArray(clients.clientId, allClientIds),
eq(clients.orgId, orgId)
)
);
if (clientsData.length !== allClientIds.length) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to one or more specified clients"
)
);
}
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error checking if user has access to the specified clients"
)
);
}
}

View File

@@ -4,6 +4,7 @@ import { userOrgs } from "@server/db";
import { and, eq, inArray, or } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
export async function verifySetResourceUsers(
req: Request,
@@ -28,6 +29,24 @@ export async function verifySetResourceUsers(
);
}
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
const policyCheck = await checkOrgAccessPolicy({
orgId: req.userOrg.orgId,
userId,
session: req.session
});
req.orgPolicyAllowed = policyCheck.allowed;
if (!policyCheck.allowed || policyCheck.error) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Failed organization access policy check: " +
(policyCheck.error || "Unknown error")
)
);
}
}
if (!userIds) {
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs"));
}

View File

@@ -1,16 +1,11 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import {
sites,
userOrgs,
userSites,
roleSites,
roles,
} from "@server/db";
import { sites, userOrgs, userSites, roleSites, roles } from "@server/db";
import { and, eq, or } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import logger from "@server/logger";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
export async function verifySiteAccess(
req: Request,
@@ -82,6 +77,24 @@ export async function verifySiteAccess(
);
}
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
const policyCheck = await checkOrgAccessPolicy({
orgId: req.userOrg.orgId,
userId,
session: req.session
});
req.orgPolicyAllowed = policyCheck.allowed;
if (!policyCheck.allowed || policyCheck.error) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Failed organization access policy check: " +
(policyCheck.error || "Unknown error")
)
);
}
}
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;
req.userOrgId = site[0].orgId;

View File

@@ -1,10 +1,11 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { db, roleSiteResources, userOrgs, userSiteResources } from "@server/db";
import { siteResources } from "@server/db";
import { eq, and } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import logger from "@server/logger";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
export async function verifySiteResourceAccess(
req: Request,
@@ -12,44 +13,145 @@ export async function verifySiteResourceAccess(
next: NextFunction
): Promise<any> {
try {
const siteResourceId = parseInt(req.params.siteResourceId);
const siteId = parseInt(req.params.siteId);
const orgId = req.params.orgId;
const userId = req.user!.userId;
const siteResourceId =
req.params.siteResourceId ||
req.body.siteResourceId ||
req.query.siteResourceId;
if (!siteResourceId || !siteId || !orgId) {
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (!siteResourceId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Missing required parameters"
"Site resource ID is required"
)
);
}
const siteResourceIdNum = parseInt(siteResourceId as string, 10);
if (isNaN(siteResourceIdNum)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid site resource ID"
)
);
}
// Check if the site resource exists and belongs to the specified site and org
const [siteResource] = await db
.select()
.from(siteResources)
.where(and(
eq(siteResources.siteResourceId, siteResourceId),
eq(siteResources.siteId, siteId),
eq(siteResources.orgId, orgId)
))
.where(eq(siteResources.siteResourceId, siteResourceIdNum))
.limit(1);
if (!siteResource) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Site resource not found"
`Site resource with ID ${siteResourceIdNum} not found`
)
);
}
if (!siteResource.orgId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`Site resource with ID ${siteResourceIdNum} does not have an organization ID`
)
);
}
if (!req.userOrg) {
const userOrgRole = await db
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.userId, userId),
eq(userOrgs.orgId, siteResource.orgId)
)
)
.limit(1);
req.userOrg = userOrgRole[0];
}
if (!req.userOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this organization"
)
);
}
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
const policyCheck = await checkOrgAccessPolicy({
orgId: req.userOrg.orgId,
userId,
session: req.session
});
req.orgPolicyAllowed = policyCheck.allowed;
if (!policyCheck.allowed || policyCheck.error) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Failed organization access policy check: " +
(policyCheck.error || "Unknown error")
)
);
}
}
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;
req.userOrgId = siteResource.orgId;
// Attach the siteResource to the request for use in the next middleware/route
// @ts-ignore - Extending Request type
req.siteResource = siteResource;
next();
const roleResourceAccess = await db
.select()
.from(roleSiteResources)
.where(
and(
eq(roleSiteResources.siteResourceId, siteResourceIdNum),
eq(roleSiteResources.roleId, userOrgRoleId)
)
)
.limit(1);
if (roleResourceAccess.length > 0) {
return next();
}
const userResourceAccess = await db
.select()
.from(userSiteResources)
.where(
and(
eq(userSiteResources.userId, userId),
eq(userSiteResources.siteResourceId, siteResourceIdNum)
)
)
.limit(1);
if (userResourceAccess.length > 0) {
return next();
}
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this resource"
)
);
} catch (error) {
logger.error("Error verifying site resource access:", error);
return next(

View File

@@ -5,6 +5,7 @@ import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { canUserAccessResource } from "../auth/canUserAccessResource";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
export async function verifyTargetAccess(
req: Request,
@@ -102,6 +103,26 @@ export async function verifyTargetAccess(
req.userOrgId = resource[0].orgId!;
}
const orgId = req.userOrg.orgId;
if (req.orgPolicyAllowed === undefined && orgId) {
const policyCheck = await checkOrgAccessPolicy({
orgId,
userId,
session: req.session
});
req.orgPolicyAllowed = policyCheck.allowed;
if (!policyCheck.allowed || policyCheck.error) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Failed organization access policy check: " +
(policyCheck.error || "Unknown error")
)
);
}
}
const resourceAllowed = await canUserAccessResource({
userId,
resourceId,

View File

@@ -15,7 +15,9 @@ export const verifySessionUserMiddleware = async (
res: Response<ErrorResponse>,
next: NextFunction
) => {
const { session, user } = await verifySession(req);
const { forceLogin } = req.query;
const { session, user } = await verifySession(req, forceLogin === "true");
if (!session || !user) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(`User session not found. IP: ${req.ip}.`);

View File

@@ -4,6 +4,7 @@ import { userOrgs } from "@server/db";
import { and, eq, or } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
export async function verifyUserAccess(
req: Request,
@@ -47,6 +48,24 @@ export async function verifyUserAccess(
);
}
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
const policyCheck = await checkOrgAccessPolicy({
orgId: req.userOrg.orgId,
userId,
session: req.session
});
req.orgPolicyAllowed = policyCheck.allowed;
if (!policyCheck.allowed || policyCheck.error) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Failed organization access policy check: " +
(policyCheck.error || "Unknown error")
)
);
}
}
return next();
} catch (error) {
return next(

View File

@@ -45,6 +45,11 @@ export class PrivateConfig {
this.rawPrivateConfig = parsedPrivateConfig;
process.env.BRANDING_HIDE_AUTH_LAYOUT_FOOTER =
this.rawPrivateConfig.branding?.hide_auth_layout_footer === true
? "true"
: "false";
if (this.rawPrivateConfig.branding?.colors) {
process.env.BRANDING_COLORS = JSON.stringify(
this.rawPrivateConfig.branding?.colors

View File

@@ -197,7 +197,7 @@ export async function listExitNodes(orgId: string, filterOnline = false, noCloud
// // set the item in the database if it is offline
// if (isActuallyOnline != node.online) {
// await db
// await trx
// .update(exitNodes)
// .set({ online: isActuallyOnline })
// .where(eq(exitNodes.exitNodeId, node.exitNodeId));

363
server/private/lib/lock.ts Normal file
View File

@@ -0,0 +1,363 @@
/*
* 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 "@server/lib/config";
import logger from "@server/logger";
import { redis } from "#private/lib/redis";
export class LockManager {
/**
* Acquire a distributed lock using Redis SET with NX and PX options
* @param lockKey - Unique identifier for the lock
* @param ttlMs - Time to live in milliseconds
* @returns Promise<boolean> - true if lock acquired, false otherwise
*/
async acquireLock(
lockKey: string,
ttlMs: number = 30000
): Promise<boolean> {
if (!redis || !redis.status || redis.status !== "ready") {
return true;
}
const lockValue = `${
config.getRawConfig().gerbil.exit_node_name
}:${Date.now()}`;
const redisKey = `lock:${lockKey}`;
try {
// Use SET with NX (only set if not exists) and PX (expire in milliseconds)
// This is atomic and handles both setting and expiration
const result = await redis.set(
redisKey,
lockValue,
"PX",
ttlMs,
"NX"
);
if (result === "OK") {
logger.debug(
`Lock acquired: ${lockKey} by ${
config.getRawConfig().gerbil.exit_node_name
}`
);
return true;
}
// Check if the existing lock is from this worker (reentrant behavior)
const existingValue = await redis.get(redisKey);
if (
existingValue &&
existingValue.startsWith(
`${config.getRawConfig().gerbil.exit_node_name}:`
)
) {
// Extend the lock TTL since it's the same worker
await redis.pexpire(redisKey, ttlMs);
logger.debug(
`Lock extended: ${lockKey} by ${
config.getRawConfig().gerbil.exit_node_name
}`
);
return true;
}
return false;
} catch (error) {
logger.error(`Failed to acquire lock ${lockKey}:`, error);
return false;
}
}
/**
* Release a lock using Lua script to ensure atomicity
* @param lockKey - Unique identifier for the lock
*/
async releaseLock(lockKey: string): Promise<void> {
if (!redis || !redis.status || redis.status !== "ready") {
return;
}
const redisKey = `lock:${lockKey}`;
// Lua script to ensure we only delete the lock if it belongs to this worker
const luaScript = `
local key = KEYS[1]
local worker_prefix = ARGV[1]
local current_value = redis.call('GET', key)
if current_value and string.find(current_value, worker_prefix, 1, true) == 1 then
return redis.call('DEL', key)
else
return 0
end
`;
try {
const result = (await redis.eval(
luaScript,
1,
redisKey,
`${config.getRawConfig().gerbil.exit_node_name}:`
)) as number;
if (result === 1) {
logger.debug(
`Lock released: ${lockKey} by ${
config.getRawConfig().gerbil.exit_node_name
}`
);
} else {
logger.warn(
`Lock not released - not owned by worker: ${lockKey} by ${
config.getRawConfig().gerbil.exit_node_name
}`
);
}
} catch (error) {
logger.error(`Failed to release lock ${lockKey}:`, error);
}
}
/**
* Force release a lock regardless of owner (use with caution)
* @param lockKey - Unique identifier for the lock
*/
async forceReleaseLock(lockKey: string): Promise<void> {
if (!redis || !redis.status || redis.status !== "ready") {
return;
}
const redisKey = `lock:${lockKey}`;
try {
const result = await redis.del(redisKey);
if (result === 1) {
logger.debug(`Lock force released: ${lockKey}`);
}
} catch (error) {
logger.error(`Failed to force release lock ${lockKey}:`, error);
}
}
/**
* Check if a lock exists and get its info
* @param lockKey - Unique identifier for the lock
* @returns Promise<{exists: boolean, ownedByMe: boolean, ttl: number}>
*/
async getLockInfo(lockKey: string): Promise<{
exists: boolean;
ownedByMe: boolean;
ttl: number;
owner?: string;
}> {
if (!redis || !redis.status || redis.status !== "ready") {
return { exists: false, ownedByMe: true, ttl: 0 };
}
const redisKey = `lock:${lockKey}`;
try {
const [value, ttl] = await Promise.all([
redis.get(redisKey),
redis.pttl(redisKey)
]);
const exists = value !== null;
const ownedByMe =
exists &&
value!.startsWith(`${config.getRawConfig().gerbil.exit_node_name}:`);
const owner = exists ? value!.split(":")[0] : undefined;
return {
exists,
ownedByMe,
ttl: ttl > 0 ? ttl : 0,
owner
};
} catch (error) {
logger.error(`Failed to get lock info ${lockKey}:`, error);
return { exists: false, ownedByMe: false, ttl: 0 };
}
}
/**
* Extend the TTL of an existing lock owned by this worker
* @param lockKey - Unique identifier for the lock
* @param ttlMs - New TTL in milliseconds
* @returns Promise<boolean> - true if extended successfully
*/
async extendLock(lockKey: string, ttlMs: number): Promise<boolean> {
if (!redis || !redis.status || redis.status !== "ready") {
return true;
}
const redisKey = `lock:${lockKey}`;
// Lua script to extend TTL only if lock is owned by this worker
const luaScript = `
local key = KEYS[1]
local worker_prefix = ARGV[1]
local ttl = tonumber(ARGV[2])
local current_value = redis.call('GET', key)
if current_value and string.find(current_value, worker_prefix, 1, true) == 1 then
return redis.call('PEXPIRE', key, ttl)
else
return 0
end
`;
try {
const result = (await redis.eval(
luaScript,
1,
redisKey,
`${config.getRawConfig().gerbil.exit_node_name}:`,
ttlMs.toString()
)) as number;
if (result === 1) {
logger.debug(
`Lock extended: ${lockKey} by ${
config.getRawConfig().gerbil.exit_node_name
} for ${ttlMs}ms`
);
return true;
}
return false;
} catch (error) {
logger.error(`Failed to extend lock ${lockKey}:`, error);
return false;
}
}
/**
* Attempt to acquire lock with retries and exponential backoff
* @param lockKey - Unique identifier for the lock
* @param ttlMs - Time to live in milliseconds
* @param maxRetries - Maximum number of retry attempts
* @param baseDelayMs - Base delay between retries in milliseconds
* @returns Promise<boolean> - true if lock acquired
*/
async acquireLockWithRetry(
lockKey: string,
ttlMs: number = 30000,
maxRetries: number = 5,
baseDelayMs: number = 100
): Promise<boolean> {
if (!redis || !redis.status || redis.status !== "ready") {
return true;
}
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const acquired = await this.acquireLock(lockKey, ttlMs);
if (acquired) {
return true;
}
if (attempt < maxRetries) {
// Exponential backoff with jitter
const delay =
baseDelayMs * Math.pow(2, attempt) + Math.random() * 100;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
logger.warn(
`Failed to acquire lock ${lockKey} after ${maxRetries + 1} attempts`
);
return false;
}
/**
* Execute a function while holding a lock
* @param lockKey - Unique identifier for the lock
* @param fn - Function to execute while holding the lock
* @param ttlMs - Lock TTL in milliseconds
* @returns Promise<T> - Result of the executed function
*/
async withLock<T>(
lockKey: string,
fn: () => Promise<T>,
ttlMs: number = 30000
): Promise<T> {
if (!redis || !redis.status || redis.status !== "ready") {
return await fn();
}
const acquired = await this.acquireLock(lockKey, ttlMs);
if (!acquired) {
throw new Error(`Failed to acquire lock: ${lockKey}`);
}
try {
return await fn();
} finally {
await this.releaseLock(lockKey);
}
}
/**
* Clean up expired locks - Redis handles this automatically, but this method
* can be used to get statistics about locks
* @returns Promise<{activeLocksCount: number, locksOwnedByMe: number}>
*/
async getLockStatistics(): Promise<{
activeLocksCount: number;
locksOwnedByMe: number;
}> {
if (!redis || !redis.status || redis.status !== "ready") {
return { activeLocksCount: 0, locksOwnedByMe: 0 };
}
try {
const keys = await redis.keys("lock:*");
let locksOwnedByMe = 0;
if (keys.length > 0) {
const values = await redis.mget(...keys);
locksOwnedByMe = values.filter(
(value) =>
value &&
value.startsWith(
`${config.getRawConfig().gerbil.exit_node_name}:`
)
).length;
}
return {
activeLocksCount: keys.length,
locksOwnedByMe
};
} catch (error) {
logger.error("Failed to get lock statistics:", error);
return { activeLocksCount: 0, locksOwnedByMe: 0 };
}
}
/**
* Close the Redis connection
*/
async disconnect(): Promise<void> {
if (!redis || !redis.status || redis.status !== "ready") {
return;
}
await redis.quit();
}
}
export const lockManager = new LockManager();

View File

@@ -72,6 +72,43 @@ export class RateLimitService {
return `ratelimit:${clientId}:${messageType}`;
}
// Helper function to clean up old timestamp fields from a Redis hash
private async cleanupOldTimestamps(key: string, windowStart: number): Promise<void> {
if (!redisManager.isRedisEnabled()) return;
try {
const client = redisManager.getClient();
if (!client) return;
// Get all fields in the hash
const allData = await redisManager.hgetall(key);
if (!allData || Object.keys(allData).length === 0) return;
// Find fields that are older than the window
const fieldsToDelete: string[] = [];
for (const timestamp of Object.keys(allData)) {
const time = parseInt(timestamp);
if (time < windowStart) {
fieldsToDelete.push(timestamp);
}
}
// Delete old fields in batches to avoid call stack size exceeded errors
// The spread operator can cause issues with very large arrays
if (fieldsToDelete.length > 0) {
const batchSize = 1000; // Process 1000 fields at a time
for (let i = 0; i < fieldsToDelete.length; i += batchSize) {
const batch = fieldsToDelete.slice(i, i + batchSize);
await client.hdel(key, ...batch);
}
logger.debug(`Cleaned up ${fieldsToDelete.length} old timestamp fields from ${key}`);
}
} catch (error) {
logger.error(`Failed to cleanup old timestamps for key ${key}:`, error);
// Don't throw - cleanup failures shouldn't block rate limiting
}
}
// Helper function to sync local rate limit data to Redis
private async syncRateLimitToRedis(
clientId: string,
@@ -81,8 +118,12 @@ export class RateLimitService {
try {
const currentTime = Math.floor(Date.now() / 1000);
const windowStart = currentTime - RATE_LIMIT_WINDOW;
const globalKey = this.getRateLimitKey(clientId);
// Clean up old timestamp fields before writing
await this.cleanupOldTimestamps(globalKey, windowStart);
// Get current value and add pending count
const currentValue = await redisManager.hget(
globalKey,
@@ -93,7 +134,7 @@ export class RateLimitService {
).toString();
await redisManager.hset(globalKey, currentTime.toString(), newValue);
// Set TTL using the client directly
// Set TTL using the client directly - this prevents the key from persisting forever
if (redisManager.getClient()) {
await redisManager
.getClient()
@@ -119,8 +160,12 @@ export class RateLimitService {
try {
const currentTime = Math.floor(Date.now() / 1000);
const windowStart = currentTime - RATE_LIMIT_WINDOW;
const messageTypeKey = this.getMessageTypeRateLimitKey(clientId, messageType);
// Clean up old timestamp fields before writing
await this.cleanupOldTimestamps(messageTypeKey, windowStart);
// Get current value and add pending count
const currentValue = await redisManager.hget(
messageTypeKey,
@@ -135,7 +180,7 @@ export class RateLimitService {
newValue
);
// Set TTL using the client directly
// Set TTL using the client directly - this prevents the key from persisting forever
if (redisManager.getClient()) {
await redisManager
.getClient()
@@ -170,6 +215,10 @@ export class RateLimitService {
try {
const globalKey = this.getRateLimitKey(clientId);
// Clean up old timestamp fields before reading
await this.cleanupOldTimestamps(globalKey, windowStart);
const globalRateLimitData = await redisManager.hgetall(globalKey);
let count = 0;
@@ -215,6 +264,10 @@ export class RateLimitService {
try {
const messageTypeKey = this.getMessageTypeRateLimitKey(clientId, messageType);
// Clean up old timestamp fields before reading
await this.cleanupOldTimestamps(messageTypeKey, windowStart);
const messageTypeRateLimitData = await redisManager.hgetall(messageTypeKey);
let count = 0;

View File

@@ -50,14 +50,14 @@ export const privateConfigSchema = z.object({
host: z.string(),
port: portSchema,
password: z.string().optional(),
db: z.number().int().nonnegative().optional().default(0),
db: z.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)
db: z.int().nonnegative().optional().default(0)
})
)
.optional()
@@ -79,14 +79,14 @@ export const privateConfigSchema = z.object({
.default("http://gerbil:3004")
})
.optional()
.default({}),
.prefault({}),
flags: z
.object({
enable_redis: z.boolean().optional().default(false),
use_pangolin_dns: z.boolean().optional().default(false)
})
.optional()
.default({}),
.prefault({}),
branding: z
.object({
app_name: z.string().optional(),
@@ -124,6 +124,7 @@ export const privateConfigSchema = z.object({
})
)
.optional(),
hide_auth_layout_footer: z.boolean().optional().default(false),
login_page: z
.object({
subtitle_text: z.string().optional(),

View File

@@ -16,7 +16,7 @@ import privateConfig from "#private/lib/config";
import logger from "@server/logger";
export enum AudienceIds {
SignUps = "5cfbf99b-c592-40a9-9b8a-577a4681c158",
SignUps = "6c4e77b2-0851-4bd6-bac8-f51f91360f1a",
Subscribed = "870b43fd-387f-44de-8fc1-707335f30b20",
Churned = "f3ae92bd-2fdb-4d77-8746-2118afd62549",
Newsletter = "5500c431-191c-42f0-a5d4-8b6d445b4ea0"

View File

@@ -111,7 +111,8 @@ export async function getTraefikConfig(
domainNamespaceId: domainNamespaces.domainNamespaceId,
// Certificate
certificateStatus: certificates.status,
domainCertResolver: domains.certResolver
domainCertResolver: domains.certResolver,
preferWildcardCert: domains.preferWildcardCert
})
.from(sites)
.innerJoin(targets, eq(targets.siteId, sites.siteId))
@@ -218,7 +219,8 @@ export async function getTraefikConfig(
rewritePath: row.rewritePath,
rewritePathType: row.rewritePathType,
priority: priority, // may be null, we fallback later
domainCertResolver: row.domainCertResolver
domainCertResolver: row.domainCertResolver,
preferWildcardCert: row.preferWildcardCert
});
}
@@ -378,7 +380,7 @@ export async function getTraefikConfig(
(cert) => cert.queriedDomain === resource.fullDomain
);
if (!matchingCert) {
logger.warn(
logger.debug(
`No matching certificate found for domain: ${resource.fullDomain}`
);
continue;
@@ -432,9 +434,9 @@ export async function getTraefikConfig(
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) {
logger.error(
`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`

View File

@@ -16,4 +16,5 @@ export * from "./verifyRemoteExitNodeAccess";
export * from "./verifyIdpAccess";
export * from "./verifyLoginPageAccess";
export * from "./logActionAudit";
export * from "./verifySubscription";
export * from "./verifySubscription";
export * from "./verifyValidLicense";

View File

@@ -30,17 +30,22 @@ export const queryAccessAuditLogsQuery = z.object({
timeStart: z
.string()
.refine((val) => !isNaN(Date.parse(val)), {
message: "timeStart must be a valid ISO date string"
error: "timeStart must be a valid ISO date string"
})
.transform((val) => Math.floor(new Date(val).getTime() / 1000)),
timeEnd: z
.string()
.refine((val) => !isNaN(Date.parse(val)), {
message: "timeEnd must be a valid ISO date string"
error: "timeEnd must be a valid ISO date string"
})
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
.optional()
.default(new Date().toISOString()),
.prefault(new Date().toISOString())
.openapi({
type: "string",
format: "date-time",
description: "End time as ISO date string (defaults to current time)"
}),
action: z
.union([z.boolean(), z.string()])
.transform((val) => (typeof val === "string" ? val === "true" : val))
@@ -51,7 +56,7 @@ export const queryAccessAuditLogsQuery = z.object({
.string()
.optional()
.transform(Number)
.pipe(z.number().int().positive())
.pipe(z.int().positive())
.optional(),
actor: z.string().optional(),
type: z.string().optional(),
@@ -61,13 +66,13 @@ export const queryAccessAuditLogsQuery = z.object({
.optional()
.default("1000")
.transform(Number)
.pipe(z.number().int().positive()),
.pipe(z.int().positive()),
offset: z
.string()
.optional()
.default("0")
.transform(Number)
.pipe(z.number().int().nonnegative())
.pipe(z.int().nonnegative())
});
export const queryAccessAuditLogsParams = z.object({

View File

@@ -30,17 +30,22 @@ export const queryActionAuditLogsQuery = z.object({
timeStart: z
.string()
.refine((val) => !isNaN(Date.parse(val)), {
message: "timeStart must be a valid ISO date string"
error: "timeStart must be a valid ISO date string"
})
.transform((val) => Math.floor(new Date(val).getTime() / 1000)),
timeEnd: z
.string()
.refine((val) => !isNaN(Date.parse(val)), {
message: "timeEnd must be a valid ISO date string"
error: "timeEnd must be a valid ISO date string"
})
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
.optional()
.default(new Date().toISOString()),
.prefault(new Date().toISOString())
.openapi({
type: "string",
format: "date-time",
description: "End time as ISO date string (defaults to current time)"
}),
action: z.string().optional(),
actorType: z.string().optional(),
actorId: z.string().optional(),
@@ -50,13 +55,13 @@ export const queryActionAuditLogsQuery = z.object({
.optional()
.default("1000")
.transform(Number)
.pipe(z.number().int().positive()),
.pipe(z.int().positive()),
offset: z
.string()
.optional()
.default("0")
.transform(Number)
.pipe(z.number().int().nonnegative())
.pipe(z.int().nonnegative())
});
export const queryActionAuditLogsParams = z.object({

View File

@@ -28,7 +28,7 @@ import { response } from "@server/lib/response";
import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
const paramsSchema = z.object({}).strict();
const paramsSchema = z.strictObject({});
export type GetSessionTransferTokenRenponse = {
token: string;

View File

@@ -62,10 +62,10 @@ import { isTargetValid } from "@server/lib/validators";
import { listExitNodes } from "#private/lib/exitNodes";
const bodySchema = z.object({
email: z.string().toLowerCase().email(),
email: z.email().toLowerCase(),
ip: z.string().refine(isTargetValid),
method: z.enum(["http", "https"]),
port: z.number().int().min(1).max(65535),
port: z.int().min(1).max(65535),
pincode: z
.string()
.regex(/^\d{6}$/)

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