Compare commits

...

251 Commits

Author SHA1 Message Date
miloschwartz
200a7fcd40 fix sidebar spacing 2025-08-15 16:00:58 -07:00
miloschwartz
5c04b1e14a add site targets, client resources, and auto login 2025-08-14 18:24:21 -07:00
miloschwartz
67ba225003 add statistics 2025-08-14 17:06:07 -07:00
miloschwartz
74d2527af5 make email lower case in pangctl reset password closes #1210 2025-08-13 22:00:20 -07:00
Owen
eeb1d4954d Use an epoch number for the clients online to fix query 2025-08-13 20:27:57 -07:00
Owen
4c463de45f Version does not need to be notNull 2025-08-13 14:47:03 -07:00
Owen
1f4a7a7f6f Add olm version 2025-08-13 12:35:09 -07:00
Owen
e7df29104e Fix backwards compat path 2025-08-13 12:28:45 -07:00
Owen
9987b35b60 Update package lock again 2025-08-13 12:26:38 -07:00
Owen
16e876ab68 Clean up checkbox 2025-08-13 12:13:47 -07:00
Owen
50fc2fc74e Add newt install command 2025-08-13 11:30:21 -07:00
Owen
c244dc9c0c Add accept clients to install 2025-08-13 11:15:14 -07:00
Owen
0f50981573 Update lock 2025-08-13 11:15:06 -07:00
Owen
0c1cb20936 Merge branch 'main' into dev 2025-08-13 10:58:04 -07:00
Owen Schwartz
192617a884 Merge pull request #1268 from fosrl/crowdin_dev
New Crowdin updates
2025-08-13 10:18:21 -07:00
Owen Schwartz
297991ef5f New translations en-us.json (Chinese Simplified) 2025-08-12 22:16:21 -07:00
Owen Schwartz
75f97c4a31 New translations en-us.json (Turkish) 2025-08-12 22:16:20 -07:00
Owen Schwartz
40f520086c New translations en-us.json (Russian) 2025-08-12 22:16:18 -07:00
Owen Schwartz
c8dda4f90d New translations en-us.json (Portuguese) 2025-08-12 22:16:17 -07:00
Owen Schwartz
5f09f97032 New translations en-us.json (Polish) 2025-08-12 22:16:15 -07:00
Owen Schwartz
168056d595 New translations en-us.json (Dutch) 2025-08-12 22:16:14 -07:00
Owen Schwartz
c70eaa0096 New translations en-us.json (Korean) 2025-08-12 22:16:13 -07:00
Owen Schwartz
5f36b13408 New translations en-us.json (Italian) 2025-08-12 22:16:11 -07:00
Owen Schwartz
9dc73efa3a New translations en-us.json (German) 2025-08-12 22:16:10 -07:00
Owen Schwartz
e9c2868998 New translations en-us.json (Czech) 2025-08-12 22:16:09 -07:00
Owen Schwartz
0a13b04c55 New translations en-us.json (Bulgarian) 2025-08-12 22:16:07 -07:00
Owen Schwartz
cf12d3ee56 New translations en-us.json (Spanish) 2025-08-12 22:16:06 -07:00
Owen Schwartz
cea7190453 New translations en-us.json (French) 2025-08-12 22:16:04 -07:00
Owen Schwartz
c6d78680fb New translations en-us.json (Norwegian Bokmal) 2025-08-12 22:16:03 -07:00
Owen Schwartz
0bf302e013 Merge pull request #1129 from adrianeastles/feature/form-signup-improvements
Form Signup Improvements - Password Requirements & Pre-Filled Email on signup form Invite
2025-08-12 21:52:44 -07:00
Owen
1351fb6689 Merge branch 'feature/form-signup-improvements' of github.com:adrianeastles/pangolin into adrianeastles-feature/form-signup-improvements 2025-08-12 21:40:55 -07:00
Owen
af638d666c Dont look for port if not root; causes permission 2025-08-12 21:34:24 -07:00
Owen Schwartz
e4fe601d9d Merge pull request #1208 from adrianeastles/feature/setup-token-security
Add setup token security for initial server setup
2025-08-12 21:31:58 -07:00
Owen
4f3cd71e1e Merge branch 'feature/setup-token-security' of github.com:adrianeastles/pangolin into adrianeastles-feature/setup-token-security 2025-08-12 21:12:55 -07:00
Owen Schwartz
9c0295db9f Merge pull request #1246 from Lokowitz/update-express
Update express to version 5
2025-08-12 20:38:02 -07:00
Owen Schwartz
3fc2d1df80 Merge pull request #1252 from fosrl/dependabot/npm_and_yarn/prod-minor-updates-ce9f23b7ef
Bump the prod-minor-updates group with 2 updates
2025-08-12 20:37:13 -07:00
Owen Schwartz
4a6747dcc7 Merge pull request #1266 from fosrl/dependabot/npm_and_yarn/dev-patch-updates-eb33d068de
Bump the dev-patch-updates group across 1 directory with 6 updates
2025-08-12 20:36:51 -07:00
Owen Schwartz
54b3c92953 Merge pull request #1262 from jackrosenberg/patch-1
fix: fixed api error message in createSite.ts
2025-08-12 20:36:26 -07:00
dependabot[bot]
a4d460e850 Bump the dev-patch-updates group across 1 directory with 6 updates
Bumps the dev-patch-updates group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `24.2.0` | `24.2.1` |
| [@types/pg](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/pg) | `8.15.4` | `8.15.5` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.1.9` | `19.1.10` |
| [esbuild](https://github.com/evanw/esbuild) | `0.25.6` | `0.25.9` |
| [tsx](https://github.com/privatenumber/tsx) | `4.20.3` | `4.20.4` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.39.0` | `8.39.1` |



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

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

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

Updates `esbuild` from 0.25.6 to 0.25.9
- [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.6...v0.25.9)

Updates `tsx` from 4.20.3 to 4.20.4
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.20.3...v4.20.4)

Updates `typescript-eslint` from 8.39.0 to 8.39.1
- [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.39.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.2.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@types/pg"
  dependency-version: 8.15.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@types/react"
  dependency-version: 19.1.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: esbuild
  dependency-version: 0.25.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: tsx
  dependency-version: 4.20.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: typescript-eslint
  dependency-version: 8.39.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-13 02:39:05 +00:00
Owen
ae52fcc757 Merge branch 'dev' into clients-multi-pop 2025-08-12 15:01:00 -07:00
jack rosenberg
03c8d82471 fix: fixed api error message in createSite.ts 2025-08-12 17:34:40 +02:00
dependabot[bot]
14d7a138a5 Bump the prod-minor-updates group with 2 updates
Bumps the prod-minor-updates group with 2 updates: [eslint](https://github.com/eslint/eslint) and [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react).


Updates `eslint` from 9.32.0 to 9.33.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.32.0...v9.33.0)

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

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: lucide-react
  dependency-version: 0.539.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-08-11 02:26:46 +00:00
Marvin
a829eb949b modified: package-lock.json
modified:   package.json
	modified:   server/nextServer.ts
2025-08-10 19:02:50 +00:00
Owen Schwartz
fd605d9c81 Merge pull request #1244 from fosrl/crowdin_dev
New Crowdin updates
2025-08-10 10:17:31 -07:00
Owen
55b4a9eddb Merge branch 'main' into dev 2025-08-10 10:16:47 -07:00
Owen Schwartz
9ccf77b99c Merge pull request #1113 from fosrl/copilot/fix-1112
Fix ESLint issues: prefer-const warnings and missing semicolons
2025-08-10 10:15:58 -07:00
Owen
ea27075bab Lint fix 2025-08-10 10:14:45 -07:00
Owen
c3723d0fce Add semi 2025-08-10 10:14:04 -07:00
Owen
0edb3cd316 Merge branch 'main' of github.com:fosrl/pangolin into copilot/fix-1112 2025-08-10 10:11:19 -07:00
Owen
e9e6b0bc4f Merge branch 'main' into copilot/fix-1112 2025-08-10 10:10:10 -07:00
Owen
4701da201d Fix a few consts to lets 2025-08-10 10:09:52 -07:00
Owen
d6d2e052dd Fix missing bracket 2025-08-10 10:04:41 -07:00
Owen Schwartz
d3d1dcfe1d New translations en-us.json (Norwegian Bokmal) 2025-08-09 12:24:58 -07:00
Owen Schwartz
918ebf5e65 Merge pull request #1228 from fosrl/dependabot/npm_and_yarn/prod-patch-updates-7f7b11108e
Bump the prod-patch-updates group across 1 directory with 7 updates
2025-08-09 10:13:26 -07:00
Owen Schwartz
67184b88a8 Merge pull request #1134 from Lokowitz/update-node-version
update node to LTS 22
2025-08-09 10:12:57 -07:00
dependabot[bot]
fb0f4c3939 Bump the prod-patch-updates group across 1 directory with 7 updates
Bumps the prod-patch-updates group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@react-email/tailwind](https://github.com/resend/react-email/tree/HEAD/packages/tailwind) | `1.2.1` | `1.2.2` |
| [drizzle-orm](https://github.com/drizzle-team/drizzle-orm) | `0.44.2` | `0.44.4` |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.1.0` | `19.1.1` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.1.8` | `19.1.9` |
| [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.1.0` | `19.1.1` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.1.6` | `19.1.7` |
| [tw-animate-css](https://github.com/Wombosvideo/tw-animate-css) | `1.3.5` | `1.3.6` |



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

Updates `drizzle-orm` from 0.44.2 to 0.44.4
- [Release notes](https://github.com/drizzle-team/drizzle-orm/releases)
- [Commits](https://github.com/drizzle-team/drizzle-orm/compare/0.44.2...0.44.4)

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

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

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

Updates `@types/react-dom` from 19.1.6 to 19.1.7
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `tw-animate-css` from 1.3.5 to 1.3.6
- [Release notes](https://github.com/Wombosvideo/tw-animate-css/releases)
- [Commits](https://github.com/Wombosvideo/tw-animate-css/compare/v1.3.5...v1.3.6)

---
updated-dependencies:
- dependency-name: "@react-email/tailwind"
  dependency-version: 1.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: drizzle-orm
  dependency-version: 0.44.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: react
  dependency-version: 19.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@types/react"
  dependency-version: 19.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: react-dom
  dependency-version: 19.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@types/react-dom"
  dependency-version: 19.1.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: tw-animate-css
  dependency-version: 1.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-09 17:09:19 +00:00
Owen Schwartz
aae5343543 Merge pull request #1192 from fosrl/dependabot/npm_and_yarn/dev-minor-updates-157f1fe21d
Bump the dev-minor-updates group across 1 directory with 5 updates
2025-08-09 10:06:16 -07:00
Owen Schwartz
51e9762ca8 Merge pull request #1229 from fosrl/dependabot/npm_and_yarn/prod-minor-updates-12f913bac4
Bump the prod-minor-updates group across 1 directory with 9 updates
2025-08-09 10:05:47 -07:00
Owen Schwartz
330dafc652 Merge pull request #1242 from EliasTors/translation-nb-NO
Translation nb no
2025-08-09 10:02:47 -07:00
Owen Schwartz
7ddf9fa54e Merge pull request #1234 from fosrl/crowdin_dev
New Crowdin updates
2025-08-09 10:02:21 -07:00
Owen Schwartz
f2ca09eedd Merge pull request #1237 from adrianeastles/feature/passkey-pangctl-support
Pangctl command to reset a user’s passkeys (WebAuthn credentials)
2025-08-09 10:01:40 -07:00
Elias Torstensen
f0e2c8416d Merge branch 'dev' into translation-nb-NO 2025-08-08 21:41:43 +02:00
EliasT05
338b7a8c13 Added nb-NO to list of locals 2025-08-08 21:33:11 +02:00
EliasT05
b4284f82f3 Added nb-NO translation 2025-08-08 21:23:09 +02:00
Owen Schwartz
0ce430cab5 New translations en-us.json (Bulgarian) 2025-08-07 15:04:32 -07:00
Owen Schwartz
95c0f6c093 New translations en-us.json (Russian) 2025-08-07 15:04:30 -07:00
Owen Schwartz
387dbc360e New translations en-us.json (Chinese Simplified) 2025-08-07 15:04:29 -07:00
Owen Schwartz
a88be89c2f New translations en-us.json (Turkish) 2025-08-07 15:04:24 -07:00
Owen Schwartz
8bc353442f New translations en-us.json (Portuguese) 2025-08-07 15:04:22 -07:00
Owen Schwartz
b3502bd627 New translations en-us.json (Polish) 2025-08-07 15:04:21 -07:00
Owen Schwartz
56da7c242d New translations en-us.json (Dutch) 2025-08-07 15:04:20 -07:00
Owen Schwartz
fa8f49e87d New translations en-us.json (Korean) 2025-08-07 15:04:18 -07:00
Owen Schwartz
6e08a70afc New translations en-us.json (Italian) 2025-08-07 15:04:17 -07:00
Owen Schwartz
bd4be2b05c New translations en-us.json (German) 2025-08-07 15:04:16 -07:00
Owen Schwartz
6b6ff0a95e New translations en-us.json (Czech) 2025-08-07 15:04:15 -07:00
Owen Schwartz
4755cae5cb New translations en-us.json (Spanish) 2025-08-07 15:04:14 -07:00
Owen Schwartz
b2384ccc06 New translations en-us.json (French) 2025-08-07 15:04:12 -07:00
Owen
e6589308dd Update readme 2025-08-07 14:53:46 -07:00
dependabot[bot]
879b25be9f 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 |
| --- | --- | --- |
| [@react-email/components](https://github.com/resend/react-email/tree/HEAD/packages/components) | `0.3.1` | `0.5.0` |
| [@react-email/render](https://github.com/resend/react-email/tree/HEAD/packages/render) | `1.1.3` | `1.2.0` |
| [axios](https://github.com/axios/axios) | `1.10.0` | `1.11.0` |
| [eslint](https://github.com/eslint/eslint) | `9.31.0` | `9.32.0` |
| [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `15.3.5` | `15.4.6` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.525.0` | `0.536.0` |
| [next](https://github.com/vercel/next.js) | `15.3.5` | `15.4.6` |
| [npm](https://github.com/npm/cli) | `11.4.2` | `11.5.2` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.60.0` | `7.62.0` |



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

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

Updates `axios` from 1.10.0 to 1.11.0
- [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.10.0...v1.11.0)

Updates `eslint` from 9.31.0 to 9.32.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.31.0...v9.32.0)

Updates `eslint-config-next` from 15.3.5 to 15.4.6
- [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/v15.4.6/packages/eslint-config-next)

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

Updates `next` from 15.3.5 to 15.4.6
- [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/compare/v15.3.5...v15.4.6)

Updates `npm` from 11.4.2 to 11.5.2
- [Release notes](https://github.com/npm/cli/releases)
- [Changelog](https://github.com/npm/cli/blob/latest/CHANGELOG.md)
- [Commits](https://github.com/npm/cli/compare/v11.4.2...v11.5.2)

Updates `react-hook-form` from 7.60.0 to 7.62.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.60.0...v7.62.0)

---
updated-dependencies:
- dependency-name: "@react-email/components"
  dependency-version: 0.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: "@react-email/render"
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: axios
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: eslint
  dependency-version: 9.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: eslint-config-next
  dependency-version: 15.4.6
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: lucide-react
  dependency-version: 0.536.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: next
  dependency-version: 15.4.6
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: npm
  dependency-version: 11.5.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react-hook-form
  dependency-version: 7.62.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-08-07 01:42:42 +00:00
dependabot[bot]
d3ad941b30 Bump the dev-minor-updates group across 1 directory with 5 updates
Bumps the dev-minor-updates group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@dotenvx/dotenvx](https://github.com/dotenvx/dotenvx) | `1.47.6` | `1.48.4` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `24.0.14` | `24.1.0` |
| [react-email](https://github.com/resend/react-email/tree/HEAD/packages/react-email) | `4.1.0` | `4.2.6` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.8.3` | `5.9.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.37.0` | `8.38.0` |



Updates `@dotenvx/dotenvx` from 1.47.6 to 1.48.4
- [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.47.6...v1.48.4)

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

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

Updates `typescript` from 5.8.3 to 5.9.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.8.3...v5.9.2)

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

---
updated-dependencies:
- dependency-name: "@dotenvx/dotenvx"
  dependency-version: 1.48.4
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: "@types/node"
  dependency-version: 24.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: react-email
  dependency-version: 4.2.6
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: typescript
  dependency-version: 5.9.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: typescript-eslint
  dependency-version: 8.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-07 01:40:47 +00:00
Owen Schwartz
f077fbc3f5 Merge pull request #1219 from aclfe/port-check80443
Added checks for port 80 and 443
2025-08-06 10:33:24 -07:00
Owen
4679ce968b Merge branch 'Xentrice-IPv6_optional' into dev 2025-08-06 10:22:19 -07:00
Owen
101e462649 Merge branch 'main' into dev 2025-08-06 10:21:54 -07:00
Owen
5d93ab9b9e Merge branch 'IPv6_optional' of github.com:Xentrice/pangolin into Xentrice-IPv6_optional 2025-08-06 10:19:51 -07:00
Owen
d557832509 Send this right IP this time 2025-08-05 11:37:45 -07:00
Owen
fe5c91db29 Change how you send the desitnations 2025-08-05 11:25:54 -07:00
Adrian Astles
b2947193ec Integrate setup token into installer, this will now parse the container logs to extract setup token automatically. Displays token with clear instructions and URL for initial admin setup. 2025-08-05 17:35:22 +08:00
Owen
f6440753b6 Only update proxy mapping if there is an existing 2025-08-04 21:34:07 -07:00
Owen
17cf903804 publicKey optional 2025-08-04 21:29:40 -07:00
Owen
dcf530d237 Add backward compatability 2025-08-04 20:36:25 -07:00
Owen
6b1808dab1 Handle multiple hp messages 2025-08-04 20:34:27 -07:00
Owen
5889efd74a Send all hp data now 2025-08-04 20:22:13 -07:00
Owen
1a9de1e5c5 Move endpoint to per site 2025-08-04 20:17:35 -07:00
Owen
d1404a2b07 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-08-04 20:02:08 -07:00
Sebastian Felber
664dbf3f4c make IPv6 optional during install 2025-08-04 15:45:33 +00:00
Owen Schwartz
f32a8e26b6 Merge pull request #1209 from fosrl/crowdin_dev
New Crowdin updates
2025-08-03 11:42:10 -07:00
Owen Schwartz
b1a92fd4e0 New translations en-us.json (Bulgarian) 2025-08-03 11:40:48 -07:00
Owen Schwartz
1ea9fd2d49 New translations en-us.json (Russian) 2025-08-03 11:40:47 -07:00
Owen Schwartz
f31e4e3176 New translations en-us.json (Chinese Simplified) 2025-08-03 11:40:46 -07:00
Owen Schwartz
e3287a7e9f New translations en-us.json (Turkish) 2025-08-03 11:40:45 -07:00
Owen Schwartz
ec21153d4b New translations en-us.json (Portuguese) 2025-08-03 11:40:44 -07:00
Owen Schwartz
917e7a8c1d New translations en-us.json (Polish) 2025-08-03 11:40:43 -07:00
Owen Schwartz
8e0a8dc272 New translations en-us.json (Dutch) 2025-08-03 11:40:41 -07:00
Owen Schwartz
91bac29ea3 New translations en-us.json (Korean) 2025-08-03 11:40:40 -07:00
Owen Schwartz
3e333769bb New translations en-us.json (Italian) 2025-08-03 11:40:39 -07:00
Owen Schwartz
b4bde6660a New translations en-us.json (German) 2025-08-03 11:40:38 -07:00
Owen Schwartz
917f752081 New translations en-us.json (Czech) 2025-08-03 11:40:37 -07:00
Owen Schwartz
915d561286 New translations en-us.json (Spanish) 2025-08-03 11:40:36 -07:00
Owen Schwartz
01ef809fd3 New translations en-us.json (French) 2025-08-03 11:40:35 -07:00
Owen Schwartz
19902092ce Merge pull request #1177 from Error-Gap/portbinding-fixes
Portbinding fixes
2025-08-03 11:37:30 -07:00
Owen Schwartz
39603b6e53 Merge pull request #1205 from Pallavikumarimdb/feature-default-language
System wide default language detection via browser language header
2025-08-03 11:35:41 -07:00
Adrian Astles
9c85a09d3e revert: package-lock.json to original state 2025-08-03 21:20:25 +08:00
Adrian Astles
69baa6785f feat: Add setup token security for initial server setup
- Add setupTokens database table with proper schema
- Implement setup token generation on first server startup
- Add token validation endpoint and modify admin creation
- Update initial setup page to require setup token
- Add migration scripts for both SQLite and PostgreSQL
- Add internationalization support for setup token fields
- Implement proper error handling and logging
- Add CLI command for resetting user security keys

This prevents unauthorized access during initial server setup by requiring
a token that is generated and displayed in the server console.
2025-08-03 21:17:18 +08:00
Adrian Astles
bb84d01e14 Reset a user's security keys (passkeys) by deleting all their webauthn credentials.
pangctl reset-user-security-keys --email user@example.com

This command will:
1. Find the user by email address
2. Check if they have any registered security keys
3. Delete all their security keys from the database
4. Provide feedback on the operation
2025-08-03 20:47:27 +08:00
Pallavi
616dae2d8b code format 2025-08-03 12:26:21 +05:30
Pallavi
3fbfe50e09 Default language detection via browser language header 2025-08-03 12:21:41 +05:30
Kairav Mittal
c0c8edb9d1 Added checks for port 80 and 443
In my issue #1203, I noticed there was a problem when ports 80 and 443 were already in use. This caused the docker containers to be created but not running
2025-08-03 11:30:33 +05:30
miloschwartz
84268e484d update docs links 2025-08-01 22:34:02 -07:00
Owen Schwartz
c473c2fa81 Merge pull request #1200 from Lokowitz/update-versions
Update versions
2025-08-01 21:24:39 -07:00
miloschwartz
7402590f49 remove api-key-org association for root keys 2025-08-01 15:56:03 -07:00
Marvin
529d1c9f66 modified: .github/workflows/cicd.yml 2025-08-01 18:37:08 +00:00
Marvin
e85b772ca5 update versions 2025-08-01 18:33:25 +00:00
Owen
f75169fc26 Add missing langs 2025-08-01 11:08:30 -07:00
Owen Schwartz
07b86521a5 Merge pull request #1196 from confusedalex/fix-nix
fix: adapt nix run command
2025-08-01 09:34:02 -07:00
confusedalex
961008bbe1 fix: adapt nix run command 2025-08-01 11:31:29 +02:00
Owen
6d359b6bb9 Add createdAt to org insert 2025-07-31 17:53:11 -07:00
Owen
ea6f803e78 Add createdAt to org 2025-07-31 17:51:30 -07:00
Owen
0151f8a6a9 Fix bad sourcePort 2025-07-31 15:57:30 -07:00
Owen
39c5101957 Merge branch 'main' into dev 2025-07-31 15:55:54 -07:00
Owen
9b1cd5f79c Ignore the config dir 2025-07-31 15:01:29 -07:00
Owen
36d0b83ed3 Fix errors again 2025-07-31 15:00:17 -07:00
Owen
f0138fad4f Improve gerbil logging 2025-07-31 14:25:22 -07:00
Owen
69802e78f8 Org is not optional 2025-07-31 11:06:07 -07:00
Owen
92e69f561f Org is not optional 2025-07-31 11:05:24 -07:00
miloschwartz
b351520e92 add clients enabled middleware 2025-07-30 23:18:51 -07:00
T Aviss
481714f095 Fix for issues with binding ports other than 80/443
server/routers/badger/verifySession.ts : verifyResourceSession() updated code behind "cleanHost" var to a regex which strips the trailing :port for any port (rather than a string match for 80/443)
src/app/auth/resource/[resourceId]/page.tsx : ResourceAuthPage() added a secondary match for serverResourceHost and redirectHost that accounts for ports
server/routers/badger/exchangeSession.ts : Updated exchangeSession() to use the same "cleanHost" type var (with port-stripping) as in verifyResourceSession(), replaced references to "host" with "cleanHost"
2025-07-30 22:16:46 -07:00
miloschwartz
d38656e026 add clients to int api 2025-07-30 21:31:16 -07:00
Owen
69b28b9b02 Merge branch 'dev' 2025-07-30 15:19:27 -07:00
Owen Schwartz
35a68703c2 Merge pull request #1173 from fosrl/crowdin_dev
New Crowdin updates
2025-07-30 15:19:04 -07:00
Owen Schwartz
c49fe04750 New translations en-us.json (Russian) 2025-07-30 15:18:36 -07:00
Owen Schwartz
31feabbec7 New translations en-us.json (Chinese Simplified) 2025-07-30 15:18:35 -07:00
Owen Schwartz
bc3cb2c3c9 New translations en-us.json (Turkish) 2025-07-30 15:18:34 -07:00
Owen Schwartz
5ec4481c92 New translations en-us.json (Portuguese) 2025-07-30 15:18:32 -07:00
Owen Schwartz
be5cb48dfe New translations en-us.json (Polish) 2025-07-30 15:18:31 -07:00
Owen Schwartz
48ff1ece16 New translations en-us.json (Dutch) 2025-07-30 15:18:30 -07:00
Owen Schwartz
ed20ed592f New translations en-us.json (Italian) 2025-07-30 15:18:28 -07:00
Owen Schwartz
4fb3435c29 New translations en-us.json (German) 2025-07-30 15:18:27 -07:00
Owen Schwartz
37eb14a01a New translations en-us.json (Spanish) 2025-07-30 15:18:25 -07:00
Owen Schwartz
d403bc86e3 New translations en-us.json (French) 2025-07-30 15:18:24 -07:00
Owen Schwartz
0e2f0f2a4d Merge pull request #1150 from fosrl/crowdin_dev
New Crowdin updates
2025-07-30 15:11:48 -07:00
Owen Schwartz
1a4d34a802 Merge pull request #1172 from fosrl/dev
1.8.0
2025-07-30 15:07:47 -07:00
Owen
bb15af9954 Add ports warn at start 2025-07-30 10:23:52 -07:00
Owen
8a250d1011 rm YC 2025-07-30 10:23:44 -07:00
Owen
2f9994f600 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-07-30 10:09:34 -07:00
Owen
1cca06a274 Add note about wintun 2025-07-29 23:09:49 -07:00
miloschwartz
8fdb3ea631 hide favicon 2025-07-29 10:46:08 -07:00
Owen
35823d5751 Fix adding sites to client 2025-07-28 22:40:27 -07:00
Owen
66f90a542a Rename to pg 2025-07-28 18:34:23 -07:00
Owen
49981c4bee Add 21820 to docker 2025-07-28 18:34:01 -07:00
Owen
d732c1a845 Clean up migrations 2025-07-28 17:32:15 -07:00
Owen
4d7e25f97b Complete migrations 2025-07-28 17:22:53 -07:00
Owen
80656f48e0 Sqlite migration done 2025-07-28 17:18:51 -07:00
Owen
ebde149980 Merge branch 'main' into dev 2025-07-28 17:15:05 -07:00
miloschwartz
adc0a81592 delete org domains and resources on org delete 2025-07-28 15:34:56 -07:00
Owen Schwartz
b596f00ce5 New translations en-us.json (Chinese Simplified) 2025-07-28 14:16:24 -07:00
Owen Schwartz
448442f92b New translations en-us.json (Turkish) 2025-07-28 14:16:23 -07:00
Owen Schwartz
8518201562 New translations en-us.json (Portuguese) 2025-07-28 14:16:21 -07:00
Owen Schwartz
17586c4559 New translations en-us.json (Polish) 2025-07-28 14:16:19 -07:00
Owen Schwartz
f8622da7d4 New translations en-us.json (Dutch) 2025-07-28 14:16:18 -07:00
Owen Schwartz
b1a27e9060 New translations en-us.json (Korean) 2025-07-28 14:16:17 -07:00
Owen Schwartz
3c6423d444 New translations en-us.json (Italian) 2025-07-28 14:16:15 -07:00
Owen Schwartz
91b03160ea New translations en-us.json (German) 2025-07-28 14:16:14 -07:00
Owen Schwartz
0c1e20ba48 New translations en-us.json (Czech) 2025-07-28 14:16:13 -07:00
Owen Schwartz
1dcac85c0d New translations en-us.json (Spanish) 2025-07-28 14:16:12 -07:00
Owen Schwartz
3fc72dbec2 New translations en-us.json (French) 2025-07-28 14:16:11 -07:00
miloschwartz
494329f568 delete resources on delete org 2025-07-28 12:55:20 -07:00
Owen
a1e8211ba7 Dont send enableProxy 2025-07-28 12:53:13 -07:00
miloschwartz
80aa7502af fix resource domain not required 2025-07-28 12:52:44 -07:00
miloschwartz
67bae76048 minor visual tweaks to member landing 2025-07-28 12:21:15 -07:00
Milo Schwartz
bda2aa46b6 Merge pull request #1124 from adrianeastles/feature/member-resouce-landing-page
New Member Resource Landing Page
2025-07-28 14:33:09 -04:00
copilot-swe-agent[bot]
27ac204bb6 Fix variables incorrectly changed from let to const - revert to let where variables are reassigned
Co-authored-by: oschwartz10612 <4999704+oschwartz10612@users.noreply.github.com>
2025-07-28 17:43:40 +00:00
copilot-swe-agent[bot]
a2526ea244 Revert mappings variable from const to let in getAllRelays.ts
Co-authored-by: oschwartz10612 <4999704+oschwartz10612@users.noreply.github.com>
2025-07-28 17:30:21 +00:00
Owen Schwartz
6d9ba8dd2f Merge pull request #1104 from jackrosenberg/nixos_newt
add nixos option for newt in site creation
2025-07-28 10:19:23 -07:00
Owen
2ca8febff7 We dont need this config 2025-07-27 14:12:01 -07:00
Owen
e105a523e4 Add log and fix default 2025-07-27 14:11:36 -07:00
Owen
28f8b05dbc Basic clients working 2025-07-27 10:21:27 -07:00
Owen Schwartz
d95286db0e Merge pull request #1139 from SigmaSquadron/push-xlmpuutwtnuy
add shebangs to migration and server scripts
2025-07-27 10:11:17 -07:00
Owen Schwartz
8e45c34e8e Merge pull request #1138 from SigmaSquadron/push-uolqlutswopp
add an environment variable for the smtp_pass config option
2025-07-27 10:09:25 -07:00
Fernando Rodrigues
9e87c42d0c add shebangs to migration and server scripts
In NixOS, we wrap these files in a bash script to allow users to just run them as normal executables, instead of calling them as arguments to Node.JS. In our build scripts, we just add the shebang after the files have been compiled, but adding it upstream will allow all Pangolin users to just run ./server.mjs to start their Pangolin instances.

Signed-off-by: Fernando Rodrigues <alpha@sigmasquadron.net>
2025-07-27 13:10:18 +10:00
Fernando Rodrigues
0b52cd002e add an environment variable for the smtp_pass config option
The password for secure authentication may be sensitive, so it is best
to not leave it lying around in a config file. This commit introduces
the EMAIL_SMTP_PASS environment variable, which can be set to configure
the SMTP password without writing it to the configuration file.

Signed-off-by: Fernando Rodrigues <alpha@sigmasquadron.net>
2025-07-27 13:03:29 +10:00
Marvin
39c43c0c09 modified: .github/workflows/cicd.yml
modified:   .github/workflows/linting.yml
	modified:   .github/workflows/test.yml
	modified:   .nvmrc
	modified:   Dockerfile.dev
	modified:   Dockerfile.pg
	modified:   Dockerfile.sqlite
	modified:   esbuild.mjs
	modified:   package-lock.json
	modified:   tsconfig.json
2025-07-26 14:17:55 +00:00
Adrian Astles
350485612e This improves the user experience by automatically filling the email field
and preventing users from changing the email they were invited with.

- Update invite link generation to include email parameter in URL
- Modify signup form to pre-fill and lock email field when provided via invite
- Update invite page and status card to preserve email through redirect chain
- Ensure existing invite URLs continue to work without breaking changes
2025-07-25 22:46:40 +08:00
Adrian Astles
df31c13912 added real-time password validation to signup form. 2025-07-25 21:59:25 +08:00
Owen
15adfcca8c Add remote subnets to ui 2025-07-24 22:01:22 -07:00
Owen
1466788f77 Clients ui done 2025-07-24 21:42:44 -07:00
Owen
760fe3aca9 Create client component done 2025-07-24 21:26:02 -07:00
Owen
5f75813e84 Handle relaying change values in gerbil 2025-07-24 20:47:39 -07:00
Owen
59cb06acf4 Support relaying on register 2025-07-24 14:48:24 -07:00
Adrian Astles
6349406523 Removed member resouce sidebar to work with new sidebar. 2025-07-24 21:30:20 +08:00
Adrian Astles
bcc2c59f08 Add member portal functionality - extracted from feature/member-landing-page 2025-07-24 21:04:55 +08:00
jack
52d46f9879 add nixos option for newt in site creation 2025-07-23 10:02:58 +02:00
Owen Schwartz
0b50a5474d Merge pull request #1041 from wayneyaoo/feature/podman-installer
Add podman support to the installer
2025-07-22 21:41:32 -07:00
copilot-swe-agent[bot]
2259879595 Fix ESLint issues: prefer-const warnings and missing semicolons
Co-authored-by: oschwartz10612 <4999704+oschwartz10612@users.noreply.github.com>
2025-07-22 19:07:43 +00:00
copilot-swe-agent[bot]
4f5091ed7f Initial commit: Document plan to fix ESLint issues
Co-authored-by: oschwartz10612 <4999704+oschwartz10612@users.noreply.github.com>
2025-07-22 18:56:04 +00:00
copilot-swe-agent[bot]
b5afd73024 Initial plan 2025-07-22 18:47:17 +00:00
Owen
5c929badeb Send endpoint 2025-07-22 11:21:39 -07:00
Owen Schwartz
3f2de333fb Merge pull request #1111 from Xentrice/main
add IPv6 support for docker network
2025-07-22 11:06:06 -07:00
Sebastian Felber
7c12b8ae25 add IPv6 support for docker network 2025-07-22 16:20:02 +02:00
miloschwartz
b54ccbfa2f fix log in loading button 2025-07-21 17:26:02 -07:00
miloschwartz
114ce8997f add tos and pp consent 2025-07-21 16:57:21 -07:00
Owen
f1bba3b958 Fix issues in pg schema 2025-07-21 16:32:13 -07:00
miloschwartz
053acef728 allow using password to log in if security keys are available 2025-07-21 14:28:32 -07:00
miloschwartz
9f2710185b center toast 2025-07-21 13:10:39 -07:00
Owen
d000879c01 Add config for domains 2025-07-21 12:42:50 -07:00
Owen
25ae169fee Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-07-21 11:48:32 -07:00
Wayne Yao
4443dda0f6 Fix a bug that error check prevents port configuration 2025-07-21 22:48:10 +08:00
Wayne Yao
c484e989a9 Merge branch 'fosrl:main' into feature/podman-installer 2025-07-21 21:36:10 +08:00
miloschwartz
86a4656651 fix multi level subdomain conflict bug 2025-07-19 22:54:30 -07:00
Owen
f25aefeb11 Merge branch 'main' of github.com:fosrl/pangolin 2025-07-19 10:33:49 -07:00
Owen Schwartz
228643c7d7 Merge pull request #1097 from ivenos/patch-1
Replace .io domain with .com domain in README
2025-07-19 10:32:08 -07:00
Owen Schwartz
072d6d7094 Merge pull request #1095 from Lokowitz/fix-test
fix test
2025-07-19 10:29:20 -07:00
Iven
de3ce672b8 Replace .io domain with .com domain 2025-07-19 18:31:08 +02:00
Marvin
6f5c191998 Update test.yml 2025-07-19 11:09:19 +02:00
Owen
bbaea4def0 Handle peer relay dynamically now 2025-07-18 21:42:12 -07:00
Milo Schwartz
54f9282166 Merge pull request #1091 from fosrl/dev
Dev
2025-07-18 18:53:45 -04:00
miloschwartz
a39b1db266 bump version 2025-07-18 15:50:55 -07:00
miloschwartz
2ddb4ec905 allow multi level sudomains in domain picker 2025-07-18 15:48:23 -07:00
miloschwartz
7a59e3acf7 fix external user select box 2025-07-18 14:45:16 -07:00
miloschwartz
b34c3db956 fix redirect bug for some accounts when disable create org is enabled 2025-07-18 12:59:57 -07:00
Owen
afea958aca Also limit to org 2025-07-18 11:48:14 -07:00
Owen
dca2a29865 Fix #1085 2025-07-18 11:32:07 -07:00
Owen
97b8e84143 Fix #1085 2025-07-18 11:16:10 -07:00
Owen Schwartz
23eb0da7d7 Merge pull request #1089 from tomribbens/unauthenticated_email
test if smtp user/pass config is set and if not set auth: null
2025-07-18 10:28:17 -07:00
Owen Schwartz
2edda471e7 Merge pull request #1087 from itsbhanusharma/patch-1
Small Typo causes crowdsec to fail
2025-07-18 10:26:20 -07:00
Tom Ribbens
676aa1358d test if user/pass config is set and if not set auth: null 2025-07-18 17:09:22 +02:00
Bhanu
87a36d6ae3 Small Typo causes crowdsec to fail
first rule is named iame instead of name. seems like a recent typo. I edited file manually and it seems to have allowed crowdsec to boot up.
2025-07-18 18:40:15 +05:30
Owen
b67611094e YC 2025-07-18 00:28:10 -07:00
Owen
2e986def78 const 2025-07-17 23:15:16 -07:00
miloschwartz
d16a05959d Merge branch 'main' into dev 2025-07-17 23:14:50 -07:00
Owen
7e58e0b490 Correctly handle ssl on new domains 2025-07-17 22:57:47 -07:00
Owen
9b01aecf3c Add default cert resovler 2025-07-17 22:37:33 -07:00
miloschwartz
86043fd5f8 add defaults for domain cert resolver and prefer wildcard cert 2025-07-17 22:35:07 -07:00
Milo Schwartz
372a1758e9 Update README.md 2025-07-17 19:35:27 -04:00
Owen
0a2b1d9e53 Use a records for the wildcard 2025-07-17 16:17:01 -07:00
Owen
e562946308 Fix logic 2025-07-17 16:03:34 -07:00
Owen
398e15b3c6 Format 2025-07-17 15:59:28 -07:00
Owen
342675276b Add type & cap 2025-07-13 15:58:58 -07:00
Owen
515a621eb4 Merge branch 'feature/podman-installer' of github.com:wayneyaoo/pangolin into wayneyaoo-feature/podman-installer 2025-07-13 15:09:39 -07:00
Wayne Yao
e83e8c2ee4 Add podman support to the installer. 2025-07-08 23:14:42 +08:00
Wayne Yao
607b168b56 Use explicity FQDN image path because Podman by default doesn't have unqualified-search, and we don't bother configuring it for users. Being explicit is also a good practice 2025-07-08 23:14:23 +08:00
Wayne Yao
e0cf0916dd Add a few targets to the Makefile to ease local development 2025-07-08 23:13:00 +08:00
240 changed files with 15705 additions and 5102 deletions

View File

@@ -28,3 +28,4 @@ LICENSE
CONTRIBUTING.md
dist
.git
config/

View File

@@ -38,3 +38,25 @@ updates:
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "gomod"
directory: "/install"
schedule:
interval: "daily"
groups:
dev-patch-updates:
dependency-type: "development"
update-types:
- "patch"
dev-minor-updates:
dependency-type: "development"
update-types:
- "minor"
prod-patch-updates:
dependency-type: "production"
update-types:
- "patch"
prod-minor-updates:
dependency-type: "production"
update-types:
- "minor"

View File

@@ -30,7 +30,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: 1.23.0
go-version: 1.24
- name: Update version in package.json
run: |

View File

@@ -23,7 +23,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
- name: Install dependencies
run: |

View File

@@ -15,7 +15,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
- name: Copy config file
run: cp config/config.example.yml config/config.yml
@@ -49,7 +49,7 @@ jobs:
exit 1
- name: Build Docker image sqlite
run: make build
run: make build-sqlite
- name: Build Docker image pg
run: make build-pg

2
.nvmrc
View File

@@ -1 +1 @@
20
22

View File

@@ -4,7 +4,7 @@ Contributions are welcome!
Please see the contribution and local development guide on the docs page before getting started:
https://docs.fossorial.io/development
https://docs.digpangolin.com/development/contributing
### Licensing Considerations
@@ -17,4 +17,4 @@ By creating this pull request, I grant the project maintainers an unlimited,
perpetual license to use, modify, and redistribute these contributions under any terms they
choose, including both the AGPLv3 and the Fossorial Commercial license terms. I
represent that I have the right to grant this license for all contributed content.
```
```

View File

@@ -1,4 +1,4 @@
FROM node:20-alpine
FROM node:22-alpine
WORKDIR /app

View File

@@ -1,4 +1,4 @@
FROM node:20-alpine AS builder
FROM node:22-alpine AS builder
WORKDIR /app
@@ -15,7 +15,7 @@ RUN npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema
RUN npm run build:pg
RUN npm run build:cli
FROM node:20-alpine AS runner
FROM node:22-alpine AS runner
WORKDIR /app

View File

@@ -1,4 +1,4 @@
FROM node:20-alpine AS builder
FROM node:22-alpine AS builder
WORKDIR /app
@@ -15,7 +15,7 @@ RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/sqlite/schema
RUN npm run build:sqlite
RUN npm run build:cli
FROM node:20-alpine AS runner
FROM node:22-alpine AS runner
WORKDIR /app

View File

@@ -16,11 +16,11 @@ _Pangolin tunnels your services to the internet so you can access anything from
<div align="center">
<h5>
<a href="https://fossorial.io">
<a href="https://digpangolin.com">
Website
</a>
<span> | </span>
<a href="https://docs.fossorial.io/Getting%20Started/quick-install">
<a href="https://docs.digpangolin.com/self-host/quick-install">
Install Guide
</a>
<span> | </span>
@@ -38,9 +38,8 @@ _Pangolin tunnels your services to the internet so you can access anything from
<p align="center">
<strong>
Start testing Pangolin at <a href="https://pangolin.fossorial.io/auth/signup">pangolin.fossorial.io</a>
<br/>
</strong>
Start testing Pangolin at <a href="https://pangolin.fossorial.io/auth/signup">pangolin.fossorial.io</a>
</strong>
</p>
Pangolin is a self-hosted tunneled reverse proxy server with identity and access control, designed to securely expose private resources on distributed networks. Acting as a central hub, it connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports.
@@ -105,13 +104,13 @@ Pangolin is a self-hosted tunneled reverse proxy server with identity and access
### Fully Self Hosted
Host the full application on your own server or on the cloud with a VPS. Take a look at the [documentation](https://docs.fossorial.io/Getting%20Started/quick-install) to get started.
Host the full application on your own server or on the cloud with a VPS. Take a look at the [documentation](https://docs.digpangolin.com/self-host/quick-install) to get started.
> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can get a [**VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**](https://my.racknerd.com/aff.php?aff=13788&pid=912). That's a great deal!
### Pangolin Cloud
Easy to use with simple [pay as you go pricing](https://fossorial.io/pricing). [Check it out here](https://pangolin.fossorial.io/auth/signup).
Easy to use with simple [pay as you go pricing](https://digpangolin.com/pricing). [Check it out here](https://pangolin.fossorial.io/auth/signup).
- Everything you get with self hosted Pangolin, but fully managed for you.
@@ -140,7 +139,7 @@ Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license.
## Contributions
Looking for something to contribute? Take a look at issues marked with [help wanted](https://github.com/fosrl/pangolin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22).
Looking for something to contribute? Take a look at issues marked with [help wanted](https://github.com/fosrl/pangolin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22). Also take a look through the freature requests in Discussions - any are available and some are marked as a good first issue.
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.

View File

@@ -0,0 +1,72 @@
import { CommandModule } from "yargs";
import { db, users, securityKeys } from "@server/db";
import { eq } from "drizzle-orm";
type ResetUserSecurityKeysArgs = {
email: string;
};
export const resetUserSecurityKeys: CommandModule<
{},
ResetUserSecurityKeysArgs
> = {
command: "reset-user-security-keys",
describe:
"Reset a user's security keys (passkeys) by deleting all their webauthn credentials",
builder: (yargs) => {
return yargs.option("email", {
type: "string",
demandOption: true,
describe: "User email address"
});
},
handler: async (argv: { email: string }) => {
try {
const { email } = argv;
console.log(`Looking for user with email: ${email}`);
// Find the user by email
const [user] = await db
.select()
.from(users)
.where(eq(users.email, email))
.limit(1);
if (!user) {
console.error(`User with email '${email}' not found`);
process.exit(1);
}
console.log(`Found user: ${user.email} (ID: ${user.userId})`);
// Check if user has any security keys
const userSecurityKeys = await db
.select()
.from(securityKeys)
.where(eq(securityKeys.userId, user.userId));
if (userSecurityKeys.length === 0) {
console.log(`User '${email}' has no security keys to reset`);
process.exit(0);
}
console.log(
`Found ${userSecurityKeys.length} security key(s) for user '${email}'`
);
// Delete all security keys for the user
await db
.delete(securityKeys)
.where(eq(securityKeys.userId, user.userId));
console.log(`Successfully reset security keys for user '${email}'`);
console.log(`Deleted ${userSecurityKeys.length} security key(s)`);
process.exit(0);
} catch (error) {
console.error("Error:", error);
process.exit(1);
}
}
};

View File

@@ -32,7 +32,8 @@ export const setAdminCredentials: CommandModule<{}, SetAdminCredentialsArgs> = {
},
handler: async (argv: { email: string; password: string }) => {
try {
const { email, password } = argv;
let { email, password } = argv;
email = email.trim().toLowerCase();
const parsed = passwordSchema.safeParse(password);

View File

@@ -3,9 +3,11 @@
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { setAdminCredentials } from "@cli/commands/setAdminCredentials";
import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys";
yargs(hideBin(process.argv))
.scriptName("pangctl")
.command(setAdminCredentials)
.command(resetUserSecurityKeys)
.demandCommand()
.help().argv;

View File

@@ -1,48 +1,28 @@
# To see all available options, please visit the docs:
# https://docs.fossorial.io/Pangolin/Configuration/config
# https://docs.digpangolin.com/self-host/advanced/config-file
app:
dashboard_url: "http://localhost:3002"
log_level: "info"
save_logs: false
dashboard_url: http://localhost:3002
log_level: debug
domains:
domain1:
base_domain: "example.com"
cert_resolver: "letsencrypt"
domain1:
base_domain: example.com
server:
external_port: 3000
internal_port: 3001
next_port: 3002
internal_hostname: "pangolin"
session_cookie_name: "p_session_token"
resource_access_token_param: "p_token"
secret: "your_secret_key_here"
resource_access_token_headers:
id: "P-Access-Token-Id"
token: "P-Access-Token"
resource_session_request_param: "p_session_request"
traefik:
http_entrypoint: "web"
https_entrypoint: "websecure"
secret: my_secret_key
gerbil:
start_port: 51820
base_endpoint: "localhost"
block_size: 24
site_block_size: 30
subnet_group: 100.89.137.0/20
use_subdomain: true
base_endpoint: example.com
rate_limits:
global:
window_minutes: 1
max_requests: 500
orgs:
block_size: 24
subnet_group: 100.90.137.0/20
flags:
require_email_verification: false
disable_signup_without_invite: true
disable_user_create_org: true
allow_raw_resources: true
require_email_verification: false
disable_signup_without_invite: true
disable_user_create_org: true
allow_raw_resources: true
enable_integration_api: true
enable_clients: true

BIN
config/db/db.sqlite.bak Normal file

Binary file not shown.

View File

@@ -31,11 +31,12 @@ services:
- SYS_MODULE
ports:
- 51820:51820/udp
- 21820:21820/udp
- 443:443 # Port for traefik because of the network_mode
- 80:80 # Port for traefik because of the network_mode
traefik:
image: traefik:v3.4.0
image: traefik:v3.5
container_name: traefik
restart: unless-stopped
network_mode: service:gerbil # Ports appear on the gerbil service
@@ -51,4 +52,5 @@ services:
networks:
default:
driver: bridge
name: pangolin
name: pangolin
enable_ipv6: true

View File

@@ -9,6 +9,7 @@ services:
- "3000:3000"
- "3001:3001"
- "3002:3002"
- "3003:3003"
environment:
- NODE_ENV=development
- ENVIRONMENT=dev
@@ -26,4 +27,4 @@ services:
- ./postcss.config.mjs:/app/postcss.config.mjs
- ./eslint.config.js:/app/eslint.config.js
- ./config:/app/config
restart: no
restart: no

View File

@@ -64,7 +64,7 @@ esbuild
}),
],
sourcemap: true,
target: "node20",
target: "node22",
})
.then(() => {
console.log("Build completed successfully");

View File

@@ -1,4 +1,5 @@
all: update-versions go-build-release put-back
dev-all: dev-update-versions dev-build dev-clean
go-build-release:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/installer_linux_amd64
@@ -11,6 +12,12 @@ clean:
update-versions:
@echo "Fetching latest versions..."
cp main.go main.go.bak && \
$(MAKE) dev-update-versions
put-back:
mv main.go.bak main.go
dev-update-versions:
PANGOLIN_VERSION=$$(curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name') && \
GERBIL_VERSION=$$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name') && \
BADGER_VERSION=$$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name') && \
@@ -20,5 +27,11 @@ update-versions:
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$$BADGER_VERSION\"/" main.go && \
echo "Updated main.go with latest versions"
put-back:
mv main.go.bak main.go
dev-build: go-build-release
dev-clean:
@echo "Restoring version values ..."
sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"replaceme\"/" main.go && \
sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"replaceme\"/" main.go && \
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"replaceme\"/" main.go
@echo "Restored version strings in main.go"

View File

@@ -1,5 +1,5 @@
# To see all available options, please visit the docs:
# https://docs.fossorial.io/Pangolin/Configuration/config
# https://docs.digpangolin.com/self-host/dns-and-networking
app:
dashboard_url: "https://{{.DashboardDomain}}"
@@ -22,10 +22,6 @@ gerbil:
start_port: 51820
base_endpoint: "{{.DashboardDomain}}"
orgs:
block_size: 24
subnet_group: 100.89.138.0/20
{{if .EnableEmail}}
email:
smtp_host: "{{.EmailSMTPHost}}"

View File

@@ -1,6 +1,6 @@
services:
crowdsec:
image: crowdsecurity/crowdsec:latest
image: docker.io/crowdsecurity/crowdsec:latest
container_name: crowdsec
environment:
GID: "1000"

View File

@@ -1,4 +1,4 @@
iame: captcha_remediation
name: captcha_remediation
filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() contains "http"
decisions:
@@ -22,4 +22,4 @@ filters:
decisions:
- type: ban
duration: 4h
on_success: break
on_success: break

View File

@@ -16,7 +16,7 @@ experimental:
version: "{{.BadgerVersion}}"
crowdsec: # CrowdSec plugin configuration added
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
version: "v1.4.2"
version: "v1.4.4"
log:
level: "INFO"

View File

@@ -1,7 +1,7 @@
name: pangolin
services:
pangolin:
image: fosrl/pangolin:{{.PangolinVersion}}
image: docker.io/fosrl/pangolin:{{.PangolinVersion}}
container_name: pangolin
restart: unless-stopped
volumes:
@@ -13,7 +13,7 @@ services:
retries: 15
{{if .InstallGerbil}}
gerbil:
image: fosrl/gerbil:{{.GerbilVersion}}
image: docker.io/fosrl/gerbil:{{.GerbilVersion}}
container_name: gerbil
restart: unless-stopped
depends_on:
@@ -31,11 +31,12 @@ services:
- SYS_MODULE
ports:
- 51820:51820/udp
- 21820:21820/udp
- 443:443 # Port for traefik because of the network_mode
- 80:80 # Port for traefik because of the network_mode
{{end}}
traefik:
image: traefik:v3.4.1
image: docker.io/traefik:v3.5
container_name: traefik
restart: unless-stopped
{{if .InstallGerbil}}
@@ -59,3 +60,4 @@ networks:
default:
driver: bridge
name: pangolin
{{if .EnableIPv6}} enable_ipv6: true{{end}}

View File

@@ -13,7 +13,7 @@ import (
func installCrowdsec(config Config) error {
if err := stopContainers(); err != nil {
if err := stopContainers(config.InstallationContainerType); err != nil {
return fmt.Errorf("failed to stop containers: %v", err)
}
@@ -72,12 +72,12 @@ func installCrowdsec(config Config) error {
os.Exit(1)
}
if err := startContainers(); err != nil {
if err := startContainers(config.InstallationContainerType); err != nil {
return fmt.Errorf("failed to start containers: %v", err)
}
// get API key
apiKey, err := GetCrowdSecAPIKey()
apiKey, err := GetCrowdSecAPIKey(config.InstallationContainerType)
if err != nil {
return fmt.Errorf("failed to get API key: %v", err)
}
@@ -87,7 +87,7 @@ func installCrowdsec(config Config) error {
return fmt.Errorf("failed to replace bouncer key: %v", err)
}
if err := restartContainer("traefik"); err != nil {
if err := restartContainer("traefik", config.InstallationContainerType); err != nil {
return fmt.Errorf("failed to restart containers: %v", err)
}
@@ -110,9 +110,9 @@ func checkIsCrowdsecInstalledInCompose() bool {
return bytes.Contains(content, []byte("crowdsec:"))
}
func GetCrowdSecAPIKey() (string, error) {
func GetCrowdSecAPIKey(containerType SupportedContainer) (string, error) {
// First, ensure the container is running
if err := waitForContainer("crowdsec"); err != nil {
if err := waitForContainer("crowdsec", containerType); err != nil {
return "", fmt.Errorf("waiting for container: %w", err)
}

View File

@@ -1,10 +1,10 @@
module installer
go 1.23.0
go 1.24
require (
golang.org/x/term v0.28.0
golang.org/x/term v0.33.0
gopkg.in/yaml.v3 v3.0.1
)
require golang.org/x/sys v0.29.0 // indirect
require golang.org/x/sys v0.34.0 // indirect

View File

@@ -1,7 +1,7 @@
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
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

@@ -1,5 +1,7 @@
docker
example.com
pangolin.example.com
yes
admin@example.com
yes
admin@example.com

View File

@@ -7,17 +7,18 @@ import (
"fmt"
"io"
"io/fs"
"math/rand"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"text/template"
"time"
"math/rand"
"strconv"
"net"
"golang.org/x/term"
)
@@ -33,43 +34,126 @@ func loadVersions(config *Config) {
var configFiles embed.FS
type Config struct {
PangolinVersion string
GerbilVersion string
BadgerVersion string
BaseDomain string
DashboardDomain string
LetsEncryptEmail string
EnableEmail bool
EmailSMTPHost string
EmailSMTPPort int
EmailSMTPUser string
EmailSMTPPass string
EmailNoReply string
InstallGerbil bool
TraefikBouncerKey string
DoCrowdsecInstall bool
Secret string
InstallationContainerType SupportedContainer
PangolinVersion string
GerbilVersion string
BadgerVersion string
BaseDomain string
DashboardDomain string
EnableIPv6 bool
LetsEncryptEmail string
EnableEmail bool
EmailSMTPHost string
EmailSMTPPort int
EmailSMTPUser string
EmailSMTPPass string
EmailNoReply string
InstallGerbil bool
TraefikBouncerKey string
DoCrowdsecInstall bool
Secret string
}
func main() {
reader := bufio.NewReader(os.Stdin)
type SupportedContainer string
// check if docker is not installed and the user is root
if !isDockerInstalled() {
if os.Geteuid() != 0 {
fmt.Println("Docker is not installed. Please install Docker manually or run this installer as root.")
os.Exit(1)
const (
Docker SupportedContainer = "docker"
Podman SupportedContainer = "podman"
)
func main() {
// print a banner about prerequisites - opening port 80, 443, 51820, and 21820 on the VPS and firewall and pointing your domain to the VPS IP with a records. Docs are at http://localhost:3000/Getting%20Started/dns-networking
fmt.Println("Welcome to the Pangolin installer!")
fmt.Println("This installer will help you set up Pangolin on your server.")
fmt.Println("")
fmt.Println("Please make sure you have the following prerequisites:")
fmt.Println("- Open TCP ports 80 and 443 and UDP ports 51820 and 21820 on your VPS and firewall.")
fmt.Println("- Point your domain to the VPS IP with A records.")
fmt.Println("")
fmt.Println("https://docs.digpangolin.com/self-host/dns-and-networking")
fmt.Println("")
fmt.Println("Lets get started!")
fmt.Println("")
if os.Geteuid() == 0 { // WE NEED TO BE SUDO TO CHECK THIS
for _, p := range []int{80, 443} {
if err := checkPortsAvailable(p); err != nil {
fmt.Fprintln(os.Stderr, err)
fmt.Printf("Please close any services on ports 80/443 in order to run the installation smoothly")
os.Exit(1)
}
}
}
// check if the user is in the docker group (linux only)
if !isUserInDockerGroup() {
fmt.Println("You are not in the docker group.")
fmt.Println("The installer will not be able to run docker commands without running it as root.")
reader := bufio.NewReader(os.Stdin)
inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker")
chosenContainer := Docker
if strings.EqualFold(inputContainer, "docker") {
chosenContainer = Docker
} else if strings.EqualFold(inputContainer, "podman") {
chosenContainer = Podman
} else {
fmt.Printf("Unrecognized container type: %s. Valid options are 'docker' or 'podman'.\n", inputContainer)
os.Exit(1)
}
if chosenContainer == Podman {
if !isPodmanInstalled() {
fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.")
os.Exit(1)
}
if err := exec.Command("bash", "-c", "cat /etc/sysctl.conf | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil {
fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.")
fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.")
approved := readBool(reader, "The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p\". Approve?", true)
if approved {
if os.Geteuid() != 0 {
fmt.Println("You need to run the installer as root for such a configuration.")
os.Exit(1)
}
// Podman containers are not able to listen on privileged ports. The official recommendation is to
// container low-range ports as unprivileged ports.
// Linux only.
if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p"); err != nil {
fmt.Sprintf("failed to configure unprivileged ports: %v.\n", err)
os.Exit(1)
}
} else {
fmt.Println("You need to configure port forwarding or adjust the listening ports before running pangolin.")
}
} else {
fmt.Println("Unprivileged ports have been configured.")
}
} else if chosenContainer == Docker {
// check if docker is not installed and the user is root
if !isDockerInstalled() {
if os.Geteuid() != 0 {
fmt.Println("Docker is not installed. Please install Docker manually or run this installer as root.")
os.Exit(1)
}
}
// check if the user is in the docker group (linux only)
if !isUserInDockerGroup() {
fmt.Println("You are not in the docker group.")
fmt.Println("The installer will not be able to run docker commands without running it as root.")
os.Exit(1)
}
} else {
// This shouldn't happen unless there's a third container runtime.
os.Exit(1)
}
var config Config
config.InstallationContainerType = chosenContainer
// check if there is already a config file
if _, err := os.Stat("config/config.yml"); err != nil {
@@ -86,7 +170,7 @@ func main() {
moveFile("config/docker-compose.yml", "docker-compose.yml")
if !isDockerInstalled() && runtime.GOOS == "linux" {
if !isDockerInstalled() && runtime.GOOS == "linux" && chosenContainer == Docker {
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
installDocker()
// try to start docker service but ignore errors
@@ -115,14 +199,15 @@ func main() {
fmt.Println("\n=== Starting installation ===")
if isDockerInstalled() {
if (isDockerInstalled() && chosenContainer == Docker) ||
(isPodmanInstalled() && chosenContainer == Podman) {
if readBool(reader, "Would you like to install and start the containers?", true) {
if err := pullContainers(); err != nil {
if err := pullContainers(chosenContainer); err != nil {
fmt.Println("Error: ", err)
return
}
if err := startContainers(); err != nil {
if err := startContainers(chosenContainer); err != nil {
fmt.Println("Error: ", err)
return
}
@@ -130,6 +215,28 @@ func main() {
}
} else {
fmt.Println("Looks like you already installed, so I am going to do the setup...")
// Read existing config to get DashboardDomain
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml")
if err != nil {
fmt.Printf("Warning: Could not read existing config: %v\n", err)
fmt.Println("You may need to manually enter your domain information.")
config = collectUserInput(reader)
} else {
config.DashboardDomain = traefikConfig.DashboardDomain
config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail
config.BadgerVersion = traefikConfig.BadgerVersion
// Show detected values and allow user to confirm or re-enter
fmt.Println("Detected existing configuration:")
fmt.Printf("Dashboard Domain: %s\n", config.DashboardDomain)
fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail)
fmt.Printf("Badger Version: %s\n", config.BadgerVersion)
if !readBool(reader, "Are these values correct?", true) {
config = collectUserInput(reader)
}
}
}
if !checkIsCrowdsecInstalledInCompose() {
@@ -137,6 +244,8 @@ func main() {
// check if crowdsec is installed
if readBool(reader, "Would you like to install CrowdSec?", false) {
fmt.Println("This installer constitutes a minimal viable CrowdSec deployment. CrowdSec will add extra complexity to your Pangolin installation and may not work to the best of its abilities out of the box. Users are expected to implement configuration adjustments on their own to achieve the best security posture. Consult the CrowdSec documentation for detailed configuration instructions.")
// BUG: crowdsec installation will be skipped if the user chooses to install on the first installation.
if readBool(reader, "Are you willing to manage CrowdSec?", false) {
if config.DashboardDomain == "" {
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml")
@@ -165,6 +274,23 @@ func main() {
}
}
// Setup Token Section
fmt.Println("\n=== Setup Token ===")
// Check if containers were started during this installation
containersStarted := false
if (isDockerInstalled() && chosenContainer == Docker) ||
(isPodmanInstalled() && chosenContainer == Podman) {
// Try to fetch and display the token if containers are running
containersStarted = true
printSetupToken(chosenContainer, config.DashboardDomain)
}
// If containers weren't started or token wasn't found, show instructions
if !containersStarted {
showSetupTokenInstructions(chosenContainer, config.DashboardDomain)
}
fmt.Println("Installation complete!")
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
}
@@ -228,9 +354,16 @@ func collectUserInput(reader *bufio.Reader) Config {
// Basic configuration
fmt.Println("\n=== Basic Configuration ===")
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", "pangolin."+config.BaseDomain)
// Set default dashboard domain after base domain is collected
defaultDashboardDomain := ""
if config.BaseDomain != "" {
defaultDashboardDomain = "pangolin." + config.BaseDomain
}
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
// Email configuration
fmt.Println("\n=== Email Configuration ===")
@@ -240,7 +373,7 @@ func collectUserInput(reader *bufio.Reader) Config {
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "")
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
}
@@ -330,7 +463,6 @@ func createConfigFiles(config Config) error {
return nil
})
if err != nil {
return fmt.Errorf("error walking config files: %v", err)
}
@@ -456,7 +588,15 @@ func startDockerService() error {
}
func isDockerInstalled() bool {
cmd := exec.Command("docker", "--version")
return isContainerInstalled("docker")
}
func isPodmanInstalled() bool {
return isContainerInstalled("podman") && isContainerInstalled("podman-compose")
}
func isContainerInstalled(container string) bool {
cmd := exec.Command(container, "--version")
if err := cmd.Run(); err != nil {
return false
}
@@ -527,52 +667,98 @@ func executeDockerComposeCommandWithArgs(args ...string) error {
cmd = exec.Command("docker-compose", args...)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// pullContainers pulls the containers using the appropriate command.
func pullContainers() error {
func pullContainers(containerType SupportedContainer) error {
fmt.Println("Pulling the container images...")
if containerType == Podman {
if err := run("podman-compose", "-f", "docker-compose.yml", "pull"); err != nil {
return fmt.Errorf("failed to pull the containers: %v", err)
}
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil {
return fmt.Errorf("failed to pull the containers: %v", err)
return nil
}
return nil
if containerType == Docker {
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil {
return fmt.Errorf("failed to pull the containers: %v", err)
}
return nil
}
return fmt.Errorf("Unsupported container type: %s", containerType)
}
// startContainers starts the containers using the appropriate command.
func startContainers() error {
func startContainers(containerType SupportedContainer) error {
fmt.Println("Starting containers...")
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil {
return fmt.Errorf("failed to start containers: %v", err)
if containerType == Podman {
if err := run("podman-compose", "-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil {
return fmt.Errorf("failed start containers: %v", err)
}
return nil
}
return nil
if containerType == Docker {
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil {
return fmt.Errorf("failed to start containers: %v", err)
}
return nil
}
return fmt.Errorf("Unsupported container type: %s", containerType)
}
// stopContainers stops the containers using the appropriate command.
func stopContainers() error {
func stopContainers(containerType SupportedContainer) error {
fmt.Println("Stopping containers...")
if containerType == Podman {
if err := run("podman-compose", "-f", "docker-compose.yml", "down"); err != nil {
return fmt.Errorf("failed to stop containers: %v", err)
}
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "down"); err != nil {
return fmt.Errorf("failed to stop containers: %v", err)
return nil
}
return nil
if containerType == Docker {
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "down"); err != nil {
return fmt.Errorf("failed to stop containers: %v", err)
}
return nil
}
return fmt.Errorf("Unsupported container type: %s", containerType)
}
// restartContainer restarts a specific container using the appropriate command.
func restartContainer(container string) error {
func restartContainer(container string, containerType SupportedContainer) error {
fmt.Println("Restarting containers...")
if containerType == Podman {
if err := run("podman-compose", "-f", "docker-compose.yml", "restart"); err != nil {
return fmt.Errorf("failed to stop the container \"%s\": %v", container, err)
}
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil {
return fmt.Errorf("failed to stop the container \"%s\": %v", container, err)
return nil
}
return nil
if containerType == Docker {
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil {
return fmt.Errorf("failed to stop the container \"%s\": %v", container, err)
}
return nil
}
return fmt.Errorf("Unsupported container type: %s", containerType)
}
func copyFile(src, dst string) error {
@@ -600,13 +786,13 @@ func moveFile(src, dst string) error {
return os.Remove(src)
}
func waitForContainer(containerName string) error {
func waitForContainer(containerName string, containerType SupportedContainer) error {
maxAttempts := 30
retryInterval := time.Second * 2
for attempt := 0; attempt < maxAttempts; attempt++ {
// Check if container is running
cmd := exec.Command("docker", "container", "inspect", "-f", "{{.State.Running}}", containerName)
cmd := exec.Command(string(containerType), "container", "inspect", "-f", "{{.State.Running}}", containerName)
var out bytes.Buffer
cmd.Stdout = &out
@@ -628,6 +814,91 @@ func waitForContainer(containerName string) error {
return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds()))
}
func printSetupToken(containerType SupportedContainer, dashboardDomain string) {
fmt.Println("Waiting for Pangolin to generate setup token...")
// Wait for Pangolin to be healthy
if err := waitForContainer("pangolin", containerType); err != nil {
fmt.Println("Warning: Pangolin container did not become healthy in time.")
return
}
// Give a moment for the setup token to be generated
time.Sleep(2 * time.Second)
// Fetch logs
var cmd *exec.Cmd
if containerType == Docker {
cmd = exec.Command("docker", "logs", "pangolin")
} else {
cmd = exec.Command("podman", "logs", "pangolin")
}
output, err := cmd.Output()
if err != nil {
fmt.Println("Warning: Could not fetch Pangolin logs to find setup token.")
return
}
// Parse for setup token
lines := strings.Split(string(output), "\n")
for i, line := range lines {
if strings.Contains(line, "=== SETUP TOKEN GENERATED ===") || strings.Contains(line, "=== SETUP TOKEN EXISTS ===") {
// Look for "Token: ..." in the next few lines
for j := i + 1; j < i+5 && j < len(lines); j++ {
trimmedLine := strings.TrimSpace(lines[j])
if strings.Contains(trimmedLine, "Token:") {
// Extract token after "Token:"
tokenStart := strings.Index(trimmedLine, "Token:")
if tokenStart != -1 {
token := strings.TrimSpace(trimmedLine[tokenStart+6:])
fmt.Printf("Setup token: %s\n", token)
fmt.Println("")
fmt.Println("This token is required to register the first admin account in the web UI at:")
fmt.Printf("https://%s/auth/initial-setup\n", dashboardDomain)
fmt.Println("")
fmt.Println("Save this token securely. It will be invalid after the first admin is created.")
return
}
}
}
}
}
fmt.Println("Warning: Could not find a setup token in Pangolin logs.")
}
func showSetupTokenInstructions(containerType SupportedContainer, dashboardDomain string) {
fmt.Println("\n=== Setup Token Instructions ===")
fmt.Println("To get your setup token, you need to:")
fmt.Println("")
fmt.Println("1. Start the containers:")
if containerType == Docker {
fmt.Println(" docker-compose up -d")
} else {
fmt.Println(" podman-compose up -d")
}
fmt.Println("")
fmt.Println("2. Wait for the Pangolin container to start and generate the token")
fmt.Println("")
fmt.Println("3. Check the container logs for the setup token:")
if containerType == Docker {
fmt.Println(" docker logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
} else {
fmt.Println(" podman logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
}
fmt.Println("")
fmt.Println("4. Look for output like:")
fmt.Println(" === SETUP TOKEN GENERATED ===")
fmt.Println(" Token: [your-token-here]")
fmt.Println(" Use this token on the initial setup page")
fmt.Println("")
fmt.Println("5. Use the token to complete initial setup at:")
fmt.Printf(" https://%s/auth/initial-setup\n", dashboardDomain)
fmt.Println("")
fmt.Println("The setup token is required to register the first admin account.")
fmt.Println("Save it securely - it will be invalid after the first admin is created.")
fmt.Println("================================")
}
func generateRandomSecretKey() string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
const length = 32
@@ -641,3 +912,29 @@ func generateRandomSecretKey() string {
}
return string(b)
}
// Run external commands with stdio/stderr attached.
func run(name string, args ...string) error {
cmd := exec.Command(name, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func checkPortsAvailable(port int) error {
addr := fmt.Sprintf(":%d", port)
ln, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf(
"ERROR: port %d is occupied or cannot be bound: %w\n\n",
port, err,
)
}
if closeErr := ln.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr,
"WARNING: failed to close test listener on port %d: %v\n",
port, closeErr,
)
}
return nil
}

1348
messages/bg-BG.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -59,7 +59,6 @@
"siteErrorCreate": "Chyba při vytváření lokality",
"siteErrorCreateKeyPair": "Nebyly nalezeny klíče nebo výchozí nastavení lokality",
"siteErrorCreateDefaults": "Výchozí nastavení lokality nenalezeno",
"siteNameDescription": "Toto je zobrazovaný název lokality.",
"method": "Způsob",
"siteMethodDescription": "Tímto způsobem budete vystavovat spojení.",
"siteLearnNewt": "Naučte se, jak nainstalovat Newt na svůj systém",
@@ -834,6 +833,24 @@
"pincodeRequirementsLength": "PIN must be exactly 6 digits",
"pincodeRequirementsChars": "PIN must only contain numbers",
"passwordRequirementsLength": "Password must be at least 1 character long",
"passwordRequirementsTitle": "Password requirements:",
"passwordRequirementLength": "At least 8 characters long",
"passwordRequirementUppercase": "At least one uppercase letter",
"passwordRequirementLowercase": "At least one lowercase letter",
"passwordRequirementNumber": "At least one number",
"passwordRequirementSpecial": "At least one special character",
"passwordRequirementsMet": "✓ Password meets all requirements",
"passwordStrength": "Password strength",
"passwordStrengthWeak": "Weak",
"passwordStrengthMedium": "Medium",
"passwordStrengthStrong": "Strong",
"passwordRequirements": "Requirements:",
"passwordRequirementLengthText": "8+ characters",
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
"passwordRequirementNumberText": "Number (0-9)",
"passwordRequirementSpecialText": "Special character (!@#$%...)",
"passwordsDoNotMatch": "Passwords do not match",
"otpEmailRequirementsLength": "OTP must be at least 1 character long",
"otpEmailSent": "OTP Sent",
"otpEmailSentDescription": "An OTP has been sent to your email",
@@ -968,6 +985,9 @@
"actionDeleteSite": "Delete Site",
"actionGetSite": "Get Site",
"actionListSites": "List Sites",
"setupToken": "Setup Token",
"setupTokenPlaceholder": "Enter the setup token from the server console",
"setupTokenRequired": "Setup token is required",
"actionUpdateSite": "Update Site",
"actionListSiteRoles": "List Allowed Site Roles",
"actionCreateResource": "Create Resource",
@@ -1023,6 +1043,11 @@
"actionDeleteIdpOrg": "Delete IDP Org Policy",
"actionListIdpOrgs": "List IDP Orgs",
"actionUpdateIdpOrg": "Update IDP Org",
"actionCreateClient": "Create Client",
"actionDeleteClient": "Delete Client",
"actionUpdateClient": "Update Client",
"actionListClients": "List Clients",
"actionGetClient": "Get Client",
"noneSelected": "None selected",
"orgNotFound2": "No organizations found.",
"searchProgress": "Search...",
@@ -1094,7 +1119,7 @@
"sidebarAllUsers": "All Users",
"sidebarIdentityProviders": "Identity Providers",
"sidebarLicense": "License",
"sidebarClients": "Clients",
"sidebarClients": "Clients (Beta)",
"sidebarDomains": "Domains",
"enableDockerSocket": "Enable Docker Socket",
"enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. Socket path must be provided to Newt.",
@@ -1162,7 +1187,7 @@
"selectDomainTypeCnameName": "Single Domain (CNAME)",
"selectDomainTypeCnameDescription": "Just this specific domain. Use this for individual subdomains or specific domain entries.",
"selectDomainTypeWildcardName": "Wildcard Domain",
"selectDomainTypeWildcardDescription": "This domain and its first level of subdomains.",
"selectDomainTypeWildcardDescription": "This domain and its subdomains.",
"domainDelegation": "Single Domain",
"selectType": "Select a type",
"actions": "Actions",
@@ -1196,7 +1221,7 @@
"sidebarExpand": "Expand",
"newtUpdateAvailable": "Update Available",
"newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.",
"domainPickerEnterDomain": "Enter your domain",
"domainPickerEnterDomain": "Domain",
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp",
"domainPickerDescription": "Enter the full domain of the resource to see available options.",
"domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options",
@@ -1206,7 +1231,7 @@
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Checking availability...",
"domainPickerNoMatchingDomains": "No matching domains found for \"{userInput}\". Try a different domain or check your organization's domain settings.",
"domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.",
"domainPickerOrganizationDomains": "Organization Domains",
"domainPickerProvidedDomains": "Provided Domains",
"domainPickerSubdomain": "Subdomain: {subdomain}",
@@ -1266,6 +1291,7 @@
"createDomainName": "Name:",
"createDomainValue": "Value:",
"createDomainCnameRecords": "CNAME Records",
"createDomainARecords": "A Records",
"createDomainRecordNumber": "Record {number}",
"createDomainTxtRecords": "TXT Records",
"createDomainSaveTheseRecords": "Save These Records",
@@ -1273,5 +1299,50 @@
"createDomainDnsPropagation": "DNS Propagation",
"createDomainDnsPropagationDescription": "DNS changes may take some time to propagate across the internet. This can take anywhere from a few minutes to 48 hours, depending on your DNS provider and TTL settings.",
"resourcePortRequired": "Port number is required for non-HTTP resources",
"resourcePortNotAllowed": "Port number should not be set for HTTP resources"
}
"resourcePortNotAllowed": "Port number should not be set for HTTP resources",
"signUpTerms": {
"IAgreeToThe": "I agree to the",
"termsOfService": "terms of service",
"and": "and",
"privacyPolicy": "privacy policy"
},
"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",
"seeAllClients": "See All Clients",
"clientInformation": "Client Information",
"clientNamePlaceholder": "Client name",
"address": "Address",
"subnetPlaceholder": "Subnet",
"addressDescription": "The address that this client will use for connectivity",
"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",
"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",
"clientUpdatedDescription": "The client has been updated.",
"clientUpdateFailed": "Failed to update client",
"clientUpdateError": "An error occurred while updating the client.",
"sitesFetchFailed": "Failed to fetch sites",
"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"
}

View File

@@ -1,5 +1,5 @@
{
"setupCreate": "Erstelle eine Organisation, Site und Ressourcen",
"setupCreate": "Erstelle eine Organisation, einen Standort und Ressourcen",
"setupNewOrg": "Neue Organisation",
"setupCreateOrg": "Organisation erstellen",
"setupCreateResources": "Ressource erstellen",
@@ -16,7 +16,7 @@
"componentsMember": "Du bist Mitglied von {count, plural, =0 {keiner Organisation} one {einer Organisation} other {# Organisationen}}.",
"componentsInvalidKey": "Ungültige oder abgelaufene Lizenzschlüssel erkannt. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.",
"dismiss": "Verwerfen",
"componentsLicenseViolation": "Lizenzverstoß: Dieser Server benutzt {usedSites} Sites, die das Lizenzlimit der {maxSites} Sites überschreiten. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.",
"componentsLicenseViolation": "Lizenzverstoß: Dieser Server benutzt {usedSites} Standorte, was das Lizenzlimit von {maxSites} Standorten überschreitet. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.",
"componentsSupporterMessage": "Vielen Dank für die Unterstützung von Pangolin als {tier}!",
"inviteErrorNotValid": "Es tut uns leid, aber es sieht so aus, als wäre die Einladung, auf die du zugreifen möchtest, entweder nicht angenommen worden oder nicht mehr gültig.",
"inviteErrorUser": "Es tut uns leid, aber es scheint, als sei die Einladung, auf die du zugreifen möchtest, nicht für diesen Benutzer bestimmt.",
@@ -38,28 +38,27 @@
"name": "Name",
"online": "Online",
"offline": "Offline",
"site": "Seite",
"site": "Standort",
"dataIn": "Daten eingehend",
"dataOut": "Daten ausgehend",
"connectionType": "Verbindungstyp",
"tunnelType": "Tunneltyp",
"local": "Lokal",
"edit": "Bearbeiten",
"siteConfirmDelete": "Site löschen bestätigen",
"siteDelete": "Site löschen",
"siteMessageRemove": "Sobald diese Seite entfernt ist, wird sie nicht mehr zugänglich sein. Alle Ressourcen und Ziele, die mit der Site verbunden sind, werden ebenfalls entfernt.",
"siteMessageConfirm": "Um zu bestätigen, gib den Namen der Site ein.",
"siteQuestionRemove": "Bist du sicher, dass Sie die Site {selectedSite} aus der Organisation entfernt werden soll?",
"siteManageSites": "Sites verwalten",
"siteConfirmDelete": "Standort löschen bestätigen",
"siteDelete": "Standort löschen",
"siteMessageRemove": "Sobald dieser Standort entfernt ist, wird er nicht mehr zugänglich sein. Alle Ressourcen und Ziele, die mit diesem Standort verbunden sind, werden ebenfalls entfernt.",
"siteMessageConfirm": "Um zu bestätigen, gib den Namen des Standortes unten ein.",
"siteQuestionRemove": "Bist du sicher, dass der Standort {selectedSite} aus der Organisation entfernt werden soll?",
"siteManageSites": "Standorte verwalten",
"siteDescription": "Verbindung zum Netzwerk durch sichere Tunnel erlauben",
"siteCreate": "Site erstellen",
"siteCreateDescription2": "Folge den nachfolgenden Schritten, um eine neue Site zu erstellen und zu verbinden",
"siteCreateDescription": "Erstelle eine neue Site, um Ressourcen zu verbinden",
"siteCreate": "Standort erstellen",
"siteCreateDescription2": "Folge den nachfolgenden Schritten, um einen neuen Standort zu erstellen und zu verbinden",
"siteCreateDescription": "Erstelle einen neuen Standort, um Ressourcen zu verbinden",
"close": "Schließen",
"siteErrorCreate": "Fehler beim Erstellen der Site",
"siteErrorCreate": "Fehler beim Erstellen des Standortes",
"siteErrorCreateKeyPair": "Schlüsselpaar oder Standardwerte nicht gefunden",
"siteErrorCreateDefaults": "Standardwerte der Site nicht gefunden",
"siteNameDescription": "Dies ist der Anzeigename für die Site.",
"method": "Methode",
"siteMethodDescription": "So werden Verbindungen freigegeben.",
"siteLearnNewt": "Wie du Newt auf deinem System installieren kannst",
@@ -71,8 +70,8 @@
"dockerRun": "Docker Run",
"siteLearnLocal": "Mehr Infos zu lokalen Sites",
"siteConfirmCopy": "Ich habe die Konfiguration kopiert",
"searchSitesProgress": "Sites durchsuchen...",
"siteAdd": "Site hinzufügen",
"searchSitesProgress": "Standorte durchsuchen...",
"siteAdd": "Standort hinzufügen",
"siteInstallNewt": "Newt installieren",
"siteInstallNewtDescription": "Installiere Newt auf deinem System.",
"WgConfiguration": "WireGuard Konfiguration",
@@ -83,26 +82,26 @@
"siteNewtDescription": "Nutze Newt für die beste Benutzererfahrung. Newt verwendet WireGuard as Basis und erlaubt Ihnen, Ihre privaten Ressourcen über ihre LAN-Adresse in Ihrem privaten Netzwerk aus dem Pangolin-Dashboard heraus zu adressieren.",
"siteRunsInDocker": "Läuft in Docker",
"siteRunsInShell": "Läuft in der Konsole auf macOS, Linux und Windows",
"siteErrorDelete": "Fehler beim Löschen der Site",
"siteErrorUpdate": "Fehler beim Aktualisieren der Site",
"siteErrorUpdateDescription": "Beim Aktualisieren der Site ist ein Fehler aufgetreten.",
"siteUpdated": "Site aktualisiert",
"siteUpdatedDescription": "Die Site wurde aktualisiert.",
"siteGeneralDescription": "Allgemeine Einstellungen für diese Site konfigurieren",
"siteSettingDescription": "Konfigurieren der Site Einstellungen",
"siteErrorDelete": "Fehler beim Löschen des Standortes",
"siteErrorUpdate": "Fehler beim Aktualisieren des Standortes",
"siteErrorUpdateDescription": "Beim Aktualisieren des Standortes ist ein Fehler aufgetreten.",
"siteUpdated": "Standort aktualisiert",
"siteUpdatedDescription": "Der Standort wurde aktualisiert.",
"siteGeneralDescription": "Allgemeine Einstellungen für diesen Standort konfigurieren",
"siteSettingDescription": "Konfigurieren der Standort Einstellungen",
"siteSetting": "{siteName} Einstellungen",
"siteNewtTunnel": "Newt-Tunnel (empfohlen)",
"siteNewtTunnelDescription": "Einfachster Weg, einen Zugriffspunkt zu deinem Netzwerk zu erstellen. Keine zusätzliche Einrichtung erforderlich.",
"siteWg": "Einfacher WireGuard Tunnel",
"siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.",
"siteLocalDescription": "Nur lokale Ressourcen. Kein Tunneling.",
"siteSeeAll": "Alle Sites anzeigen",
"siteTunnelDescription": "Lege fest, wie du dich mit deiner Site verbinden möchtest",
"siteSeeAll": "Alle Standorte anzeigen",
"siteTunnelDescription": "Lege fest, wie du dich mit deinem Standort verbinden möchtest",
"siteNewtCredentials": "Neue Newt Zugangsdaten",
"siteNewtCredentialsDescription": "So wird sich Newt mit dem Server authentifizieren",
"siteCredentialsSave": "Ihre Zugangsdaten speichern",
"siteCredentialsSaveDescription": "Du kannst das nur einmal sehen. Stelle sicher, dass du es an einen sicheren Ort kopierst.",
"siteInfo": "Site-Informationen",
"siteInfo": "Standort-Informationen",
"status": "Status",
"shareTitle": "Links zum Teilen verwalten",
"shareDescription": "Erstellen Sie teilbare Links, um temporären oder permanenten Zugriff auf Ihre Ressourcen zu gewähren",
@@ -164,10 +163,10 @@
"resourceSeeAll": "Alle Ressourcen anzeigen",
"resourceInfo": "Ressourcen-Informationen",
"resourceNameDescription": "Dies ist der Anzeigename für die Ressource.",
"siteSelect": "Site auswählen",
"siteSearch": "Website durchsuchen",
"siteNotFound": "Keine Site gefunden.",
"siteSelectionDescription": "Diese Seite wird die Verbindung zu der Ressource herstellen.",
"siteSelect": "Standort auswählen",
"siteSearch": "Standorte durchsuchen",
"siteNotFound": "Keinen Standort gefunden.",
"siteSelectionDescription": "Dieser Standort wird die Verbindung zu der Ressource herstellen.",
"resourceType": "Ressourcentyp",
"resourceTypeDescription": "Legen Sie fest, wie Sie auf Ihre Ressource zugreifen möchten",
"resourceHTTPSSettings": "HTTPS-Einstellungen",
@@ -303,7 +302,7 @@
"userQuestionRemove": "Sind Sie sicher, dass Sie {selectedUser} dauerhaft vom Server löschen möchten?",
"licenseKey": "Lizenzschlüssel",
"valid": "Gültig",
"numberOfSites": "Anzahl der Sites",
"numberOfSites": "Anzahl der Standorte",
"licenseKeySearch": "Lizenzschlüssel suchen...",
"licenseKeyAdd": "Lizenzschlüssel hinzufügen",
"type": "Typ",
@@ -343,16 +342,16 @@
"licensedNot": "Nicht lizenziert",
"hostId": "Host-ID",
"licenseReckeckAll": "Überprüfe alle Schlüssel",
"licenseSiteUsage": "Website-Nutzung",
"licenseSiteUsageDecsription": "Sehen Sie sich die Anzahl der Sites an, die diese Lizenz verwenden.",
"licenseNoSiteLimit": "Die Anzahl der Sites, die einen nicht lizenzierten Host verwenden, ist unbegrenzt.",
"licenseSiteUsage": "Standort-Nutzung",
"licenseSiteUsageDecsription": "Sehen Sie sich die Anzahl der Standorte an, die diese Lizenz verwenden.",
"licenseNoSiteLimit": "Die Anzahl der Standorte, die einen nicht lizenzierten Host verwenden, ist unbegrenzt.",
"licensePurchase": "Lizenz kaufen",
"licensePurchaseSites": "Zusätzliche Seiten kaufen",
"licenseSitesUsedMax": "{usedSites} der {maxSites} Seiten verwendet",
"licenseSitesUsed": "{count, plural, =0 {# Seiten} one {# Seite} other {# Seiten}} im System.",
"licensePurchaseSites": "Zusätzliche Standorte kaufen\n",
"licenseSitesUsedMax": "{usedSites} von {maxSites} Standorten verwendet",
"licenseSitesUsed": "{count, plural, =0 {# Standorte} one {# Standort} other {# Standorte}} im System.",
"licensePurchaseDescription": "Wähle aus, für wieviele Seiten du möchtest {selectedMode, select, license {kaufe eine Lizenz. Du kannst später immer weitere Seiten hinzufügen.} other {Füge zu deiner bestehenden Lizenz hinzu.}}",
"licenseFee": "Lizenzgebühr",
"licensePriceSite": "Preis pro Seite",
"licensePriceSite": "Preis pro Standort",
"total": "Gesamt",
"licenseContinuePayment": "Weiter zur Zahlung",
"pricingPage": "Preisseite",
@@ -468,7 +467,7 @@
"targetErrorDuplicate": "Doppeltes Ziel",
"targetErrorDuplicateDescription": "Ein Ziel mit diesen Einstellungen existiert bereits",
"targetWireGuardErrorInvalidIp": "Ungültige Ziel-IP",
"targetWireGuardErrorInvalidIpDescription": "Die Ziel-IP muss innerhalb des Site-Subnets liegen",
"targetWireGuardErrorInvalidIpDescription": "Die Ziel-IP muss innerhalb des Standort-Subnets liegen",
"targetsUpdated": "Ziele aktualisiert",
"targetsUpdatedDescription": "Ziele und Einstellungen erfolgreich aktualisiert",
"targetsErrorUpdate": "Fehler beim Aktualisieren der Ziele",
@@ -559,8 +558,8 @@
"resourceErrorCreateDescription": "Beim Erstellen der Ressource ist ein Fehler aufgetreten",
"resourceErrorCreateMessage": "Fehler beim Erstellen der Ressource:",
"resourceErrorCreateMessageDescription": "Ein unerwarteter Fehler ist aufgetreten",
"sitesErrorFetch": "Fehler beim Abrufen der Sites",
"sitesErrorFetchDescription": "Beim Abrufen der Sites ist ein Fehler aufgetreten",
"sitesErrorFetch": "Fehler beim Abrufen der Standorte",
"sitesErrorFetchDescription": "Beim Abrufen der Standorte ist ein Fehler aufgetreten",
"domainsErrorFetch": "Fehler beim Abrufen der Domains",
"domainsErrorFetchDescription": "Beim Abrufen der Domains ist ein Fehler aufgetreten",
"none": "Keine",
@@ -678,10 +677,10 @@
"resourceGeneralDescription": "Konfigurieren Sie die allgemeinen Einstellungen für diese Ressource",
"resourceEnable": "Ressource aktivieren",
"resourceTransfer": "Ressource übertragen",
"resourceTransferDescription": "Diese Ressource auf eine andere Site übertragen",
"resourceTransferDescription": "Diese Ressource auf einen anderen Standort übertragen",
"resourceTransferSubmit": "Ressource übertragen",
"siteDestination": "Zielsite",
"searchSites": "Sites durchsuchen",
"siteDestination": "Zielort",
"searchSites": "Standorte durchsuchen",
"accessRoleCreate": "Rolle erstellen",
"accessRoleCreateDescription": "Erstellen Sie eine neue Rolle, um Benutzer zu gruppieren und ihre Berechtigungen zu verwalten.",
"accessRoleCreateSubmit": "Rolle erstellen",
@@ -701,7 +700,7 @@
"accessRoleRemovedDescription": "Die Rolle wurde erfolgreich entfernt.",
"accessRoleRequiredRemove": "Bevor Sie diese Rolle löschen, wählen Sie bitte eine neue Rolle aus, zu der die bestehenden Mitglieder übertragen werden sollen.",
"manage": "Verwalten",
"sitesNotFound": "Keine Sites gefunden.",
"sitesNotFound": "Keine Standorte gefunden.",
"pangolinServerAdmin": "Server-Admin - Pangolin",
"licenseTierProfessional": "Professional Lizenz",
"licenseTierEnterprise": "Enterprise Lizenz",
@@ -709,10 +708,10 @@
"licensed": "Lizenziert",
"yes": "Ja",
"no": "Nein",
"sitesAdditional": "Zusätzliche Sites",
"sitesAdditional": "Zusätzliche Standorte",
"licenseKeys": "Lizenzschlüssel",
"sitestCountDecrease": "Anzahl der Sites verringern",
"sitestCountIncrease": "Anzahl der Sites erhöhen",
"sitestCountDecrease": "Anzahl der Standorte verringern",
"sitestCountIncrease": "Anzahl der Standorte erhöhen",
"idpManage": "Identitätsanbieter verwalten",
"idpManageDescription": "Identitätsanbieter im System anzeigen und verwalten",
"idpDeletedDescription": "Identitätsanbieter erfolgreich gelöscht",
@@ -834,6 +833,24 @@
"pincodeRequirementsLength": "PIN muss genau 6 Ziffern lang sein",
"pincodeRequirementsChars": "PIN darf nur Zahlen enthalten",
"passwordRequirementsLength": "Passwort muss mindestens 1 Zeichen lang sein",
"passwordRequirementsTitle": "Password requirements:",
"passwordRequirementLength": "At least 8 characters long",
"passwordRequirementUppercase": "At least one uppercase letter",
"passwordRequirementLowercase": "At least one lowercase letter",
"passwordRequirementNumber": "At least one number",
"passwordRequirementSpecial": "At least one special character",
"passwordRequirementsMet": "✓ Password meets all requirements",
"passwordStrength": "Password strength",
"passwordStrengthWeak": "Weak",
"passwordStrengthMedium": "Medium",
"passwordStrengthStrong": "Strong",
"passwordRequirements": "Requirements:",
"passwordRequirementLengthText": "8+ characters",
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
"passwordRequirementNumberText": "Number (0-9)",
"passwordRequirementSpecialText": "Special character (!@#$%...)",
"passwordsDoNotMatch": "Passwords do not match",
"otpEmailRequirementsLength": "OTP muss mindestens 1 Zeichen lang sein",
"otpEmailSent": "OTP gesendet",
"otpEmailSentDescription": "Ein OTP wurde an Ihre E-Mail gesendet",
@@ -964,12 +981,15 @@
"actionGetUser": "Benutzer abrufen",
"actionGetOrgUser": "Organisationsbenutzer abrufen",
"actionListOrgDomains": "Organisationsdomänen auflisten",
"actionCreateSite": "Site erstellen",
"actionDeleteSite": "Site löschen",
"actionGetSite": "Site abrufen",
"actionListSites": "Sites auflisten",
"actionUpdateSite": "Site aktualisieren",
"actionListSiteRoles": "Erlaubte Site-Rollen auflisten",
"actionCreateSite": "Standort erstellen",
"actionDeleteSite": "Standort löschen",
"actionGetSite": "Standort abrufen",
"actionListSites": "Standorte auflisten",
"setupToken": "Setup Token",
"setupTokenPlaceholder": "Enter the setup token from the server console",
"setupTokenRequired": "Setup token is required",
"actionUpdateSite": "Standorte aktualisieren",
"actionListSiteRoles": "Erlaubte Standort-Rollen auflisten",
"actionCreateResource": "Ressource erstellen",
"actionDeleteResource": "Ressource löschen",
"actionGetResource": "Ressource abrufen",
@@ -1023,6 +1043,11 @@
"actionDeleteIdpOrg": "IDP-Organisationsrichtlinie löschen",
"actionListIdpOrgs": "IDP-Organisationen auflisten",
"actionUpdateIdpOrg": "IDP-Organisation aktualisieren",
"actionCreateClient": "Kunde erstellen",
"actionDeleteClient": "Kunde löschen",
"actionUpdateClient": "Kunde aktualisieren",
"actionListClients": "Kunden auflisten",
"actionGetClient": "Kunde holen",
"noneSelected": "Keine ausgewählt",
"orgNotFound2": "Keine Organisationen gefunden.",
"searchProgress": "Suche...",
@@ -1074,7 +1099,7 @@
"language": "Sprache",
"verificationCodeRequired": "Code ist erforderlich",
"userErrorNoUpdate": "Kein Benutzer zum Aktualisieren",
"siteErrorNoUpdate": "Keine Site zum Aktualisieren",
"siteErrorNoUpdate": "Keine Standorte zum Aktualisieren",
"resourceErrorNoUpdate": "Keine Ressource zum Aktualisieren",
"authErrorNoUpdate": "Keine Auth-Informationen zum Aktualisieren",
"orgErrorNoUpdate": "Keine Organisation zum Aktualisieren",
@@ -1082,7 +1107,7 @@
"apiKeysErrorNoUpdate": "Kein API-Schlüssel zum Aktualisieren",
"sidebarOverview": "Übersicht",
"sidebarHome": "Zuhause",
"sidebarSites": "Seiten",
"sidebarSites": "Standorte",
"sidebarResources": "Ressourcen",
"sidebarAccessControl": "Zugriffskontrolle",
"sidebarUsers": "Benutzer",
@@ -1094,7 +1119,7 @@
"sidebarAllUsers": "Alle Benutzer",
"sidebarIdentityProviders": "Identitätsanbieter",
"sidebarLicense": "Lizenz",
"sidebarClients": "Kunden",
"sidebarClients": "Clients (Beta)",
"sidebarDomains": "Domains",
"enableDockerSocket": "Docker Socket aktivieren",
"enableDockerSocketDescription": "Docker Socket-Erkennung aktivieren, um Container-Informationen zu befüllen. Socket-Pfad muss Newt bereitgestellt werden.",
@@ -1162,7 +1187,7 @@
"selectDomainTypeCnameName": "Einzelne Domain (CNAME)",
"selectDomainTypeCnameDescription": "Nur diese spezifische Domain. Verwenden Sie dies für einzelne Subdomains oder spezifische Domaineinträge.",
"selectDomainTypeWildcardName": "Wildcard-Domain",
"selectDomainTypeWildcardDescription": "Diese Domain und ihre erste Ebene der Subdomains.",
"selectDomainTypeWildcardDescription": "Diese Domain und ihre Subdomains.",
"domainDelegation": "Einzelne Domain",
"selectType": "Typ auswählen",
"actions": "Aktionen",
@@ -1196,7 +1221,7 @@
"sidebarExpand": "Erweitern",
"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": "Geben Sie Ihre Domain ein",
"domainPickerEnterDomain": "Domain",
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, oder einfach myapp",
"domainPickerDescription": "Geben Sie die vollständige Domäne der Ressource ein, um verfügbare Optionen zu sehen.",
"domainPickerDescriptionSaas": "Geben Sie eine vollständige Domäne, Subdomäne oder einfach einen Namen ein, um verfügbare Optionen zu sehen",
@@ -1206,7 +1231,7 @@
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Verfügbarkeit prüfen...",
"domainPickerNoMatchingDomains": "Keine passenden Domains für \"{userInput}\" gefunden. Versuchen Sie es mit einer anderen Domain oder überprüfen Sie die Domain-Einstellungen Ihrer Organisation.",
"domainPickerNoMatchingDomains": "Keine passenden Domains gefunden. Versuchen Sie es mit einer anderen Domain oder überprüfen Sie die Domain-Einstellungen Ihrer Organisation.",
"domainPickerOrganizationDomains": "Organisations-Domains",
"domainPickerProvidedDomains": "Bereitgestellte Domains",
"domainPickerSubdomain": "Subdomain: {subdomain}",
@@ -1266,6 +1291,7 @@
"createDomainName": "Name:",
"createDomainValue": "Wert:",
"createDomainCnameRecords": "CNAME-Einträge",
"createDomainARecords": "A-Aufzeichnungen",
"createDomainRecordNumber": "Eintrag {number}",
"createDomainTxtRecords": "TXT-Einträge",
"createDomainSaveTheseRecords": "Diese Einträge speichern",
@@ -1273,5 +1299,50 @@
"createDomainDnsPropagation": "DNS-Verbreitung",
"createDomainDnsPropagationDescription": "Es kann einige Zeit dauern, bis DNS-Änderungen im Internet verbreitet werden. Dies kann je nach Ihrem DNS-Provider und den TTL-Einstellungen von einigen Minuten bis zu 48 Stunden dauern.",
"resourcePortRequired": "Portnummer ist für nicht-HTTP-Ressourcen erforderlich",
"resourcePortNotAllowed": "Portnummer sollte für HTTP-Ressourcen nicht gesetzt werden"
}
"resourcePortNotAllowed": "Portnummer sollte für HTTP-Ressourcen nicht gesetzt werden",
"signUpTerms": {
"IAgreeToThe": "Ich stimme den",
"termsOfService": "Nutzungsbedingungen zu",
"and": "und",
"privacyPolicy": "Datenschutzrichtlinie"
},
"siteRequired": "Standort ist erforderlich.",
"olmTunnel": "Olm Tunnel",
"olmTunnelDescription": "Nutzen Sie Olm für die Kundenverbindung",
"errorCreatingClient": "Fehler beim Erstellen des Clients",
"clientDefaultsNotFound": "Kundenvorgaben 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",
"address": "Adresse",
"subnetPlaceholder": "Subnetz",
"addressDescription": "Die Adresse, die dieser Client für die Verbindung verwenden wird.",
"selectSites": "Standorte auswählen",
"sitesDescription": "Der Client wird zu den ausgewählten Standorten eine Verbindung haben.",
"clientInstallOlm": "Olm installieren",
"clientInstallOlmDescription": "Olm auf Ihrem System zum Laufen bringen",
"clientOlmCredentials": "Olm-Zugangsdaten",
"clientOlmCredentialsDescription": "So authentifiziert sich Olm beim Server",
"olmEndpoint": "Olm-Endpunkt",
"olmId": "Olm-ID",
"olmSecretKey": "Olm-Geheimschlüssel",
"clientCredentialsSave": "Speichern Sie Ihre Zugangsdaten",
"clientCredentialsSaveDescription": "Sie können dies nur einmal sehen. Kopieren Sie es an einen sicheren Ort.",
"generalSettingsDescription": "Konfigurieren Sie die allgemeinen Einstellungen für diesen Client",
"clientUpdated": "Client aktualisiert",
"clientUpdatedDescription": "Der Client wurde aktualisiert.",
"clientUpdateFailed": "Fehler beim Aktualisieren des Clients",
"clientUpdateError": "Beim Aktualisieren des Clients ist ein Fehler aufgetreten.",
"sitesFetchFailed": "Fehler beim Abrufen von Standorten",
"sitesFetchError": "Beim Abrufen von Standorten ist ein Fehler aufgetreten.",
"olmErrorFetchReleases": "Beim Abrufen von Olm-Veröffentlichungen ist ein Fehler aufgetreten.",
"olmErrorFetchLatest": "Beim Abrufen der neuesten Olm-Veröffentlichung ist ein Fehler aufgetreten.",
"remoteSubnets": "Remote-Subnetze",
"enterCidrRange": "Geben Sie den CIDR-Bereich ein",
"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": "Öffentlichen Proxy aktivieren",
"resourceEnableProxyDescription": "Ermöglichen Sie öffentliches Proxieren zu dieser Ressource. Dies ermöglicht den Zugriff auf die Ressource von außerhalb des Netzwerks durch die Cloud über einen offenen Port. Erfordert Traefik-Config.",
"externalProxyEnabled": "Externer Proxy aktiviert"
}

View File

@@ -59,7 +59,6 @@
"siteErrorCreate": "Error creating site",
"siteErrorCreateKeyPair": "Key pair or site defaults not found",
"siteErrorCreateDefaults": "Site defaults not found",
"siteNameDescription": "This is the display name for the site.",
"method": "Method",
"siteMethodDescription": "This is how you will expose connections.",
"siteLearnNewt": "Learn how to install Newt on your system",
@@ -167,7 +166,7 @@
"siteSelect": "Select site",
"siteSearch": "Search site",
"siteNotFound": "No site found.",
"siteSelectionDescription": "This site will provide connectivity to the resource.",
"siteSelectionDescription": "This site will provide connectivity to the target.",
"resourceType": "Resource Type",
"resourceTypeDescription": "Determine how you want to access your resource",
"resourceHTTPSSettings": "HTTPS Settings",
@@ -198,6 +197,7 @@
"general": "General",
"generalSettings": "General Settings",
"proxy": "Proxy",
"internal": "Internal",
"rules": "Rules",
"resourceSettingDescription": "Configure the settings on your resource",
"resourceSetting": "{resourceName} Settings",
@@ -491,7 +491,7 @@
"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 services",
"targetsDescription": "Set up targets to route traffic to your backend services",
"targetStickySessions": "Enable Sticky Sessions",
"targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.",
"methodSelect": "Select method",
@@ -834,6 +834,24 @@
"pincodeRequirementsLength": "PIN must be exactly 6 digits",
"pincodeRequirementsChars": "PIN must only contain numbers",
"passwordRequirementsLength": "Password must be at least 1 character long",
"passwordRequirementsTitle": "Password requirements:",
"passwordRequirementLength": "At least 8 characters long",
"passwordRequirementUppercase": "At least one uppercase letter",
"passwordRequirementLowercase": "At least one lowercase letter",
"passwordRequirementNumber": "At least one number",
"passwordRequirementSpecial": "At least one special character",
"passwordRequirementsMet": "✓ Password meets all requirements",
"passwordStrength": "Password strength",
"passwordStrengthWeak": "Weak",
"passwordStrengthMedium": "Medium",
"passwordStrengthStrong": "Strong",
"passwordRequirements": "Requirements:",
"passwordRequirementLengthText": "8+ characters",
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
"passwordRequirementNumberText": "Number (0-9)",
"passwordRequirementSpecialText": "Special character (!@#$%...)",
"passwordsDoNotMatch": "Passwords do not match",
"otpEmailRequirementsLength": "OTP must be at least 1 character long",
"otpEmailSent": "OTP Sent",
"otpEmailSentDescription": "An OTP has been sent to your email",
@@ -968,6 +986,9 @@
"actionDeleteSite": "Delete Site",
"actionGetSite": "Get Site",
"actionListSites": "List Sites",
"setupToken": "Setup Token",
"setupTokenDescription": "Enter the setup token from the server console.",
"setupTokenRequired": "Setup token is required",
"actionUpdateSite": "Update Site",
"actionListSiteRoles": "List Allowed Site Roles",
"actionCreateResource": "Create Resource",
@@ -1023,6 +1044,11 @@
"actionDeleteIdpOrg": "Delete IDP Org Policy",
"actionListIdpOrgs": "List IDP Orgs",
"actionUpdateIdpOrg": "Update IDP Org",
"actionCreateClient": "Create Client",
"actionDeleteClient": "Delete Client",
"actionUpdateClient": "Update Client",
"actionListClients": "List Clients",
"actionGetClient": "Get Client",
"noneSelected": "None selected",
"orgNotFound2": "No organizations found.",
"searchProgress": "Search...",
@@ -1094,7 +1120,7 @@
"sidebarAllUsers": "All Users",
"sidebarIdentityProviders": "Identity Providers",
"sidebarLicense": "License",
"sidebarClients": "Clients",
"sidebarClients": "Clients (Beta)",
"sidebarDomains": "Domains",
"enableDockerSocket": "Enable Docker Socket",
"enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. Socket path must be provided to Newt.",
@@ -1162,7 +1188,7 @@
"selectDomainTypeCnameName": "Single Domain (CNAME)",
"selectDomainTypeCnameDescription": "Just this specific domain. Use this for individual subdomains or specific domain entries.",
"selectDomainTypeWildcardName": "Wildcard Domain",
"selectDomainTypeWildcardDescription": "This domain and its first level of subdomains.",
"selectDomainTypeWildcardDescription": "This domain and its subdomains.",
"domainDelegation": "Single Domain",
"selectType": "Select a type",
"actions": "Actions",
@@ -1196,7 +1222,7 @@
"sidebarExpand": "Expand",
"newtUpdateAvailable": "Update Available",
"newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.",
"domainPickerEnterDomain": "Enter your domain",
"domainPickerEnterDomain": "Domain",
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp",
"domainPickerDescription": "Enter the full domain of the resource to see available options.",
"domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options",
@@ -1206,7 +1232,7 @@
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Checking availability...",
"domainPickerNoMatchingDomains": "No matching domains found for \"{userInput}\". Try a different domain or check your organization's domain settings.",
"domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.",
"domainPickerOrganizationDomains": "Organization Domains",
"domainPickerProvidedDomains": "Provided Domains",
"domainPickerSubdomain": "Subdomain: {subdomain}",
@@ -1266,6 +1292,7 @@
"createDomainName": "Name:",
"createDomainValue": "Value:",
"createDomainCnameRecords": "CNAME Records",
"createDomainARecords": "A Records",
"createDomainRecordNumber": "Record {number}",
"createDomainTxtRecords": "TXT Records",
"createDomainSaveTheseRecords": "Save These Records",
@@ -1273,5 +1300,152 @@
"createDomainDnsPropagation": "DNS Propagation",
"createDomainDnsPropagationDescription": "DNS changes may take some time to propagate across the internet. This can take anywhere from a few minutes to 48 hours, depending on your DNS provider and TTL settings.",
"resourcePortRequired": "Port number is required for non-HTTP resources",
"resourcePortNotAllowed": "Port number should not be set for HTTP resources"
"resourcePortNotAllowed": "Port number should not be set for HTTP resources",
"signUpTerms": {
"IAgreeToThe": "I agree to the",
"termsOfService": "terms of service",
"and": "and",
"privacyPolicy": "privacy policy"
},
"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",
"seeAllClients": "See All Clients",
"clientInformation": "Client Information",
"clientNamePlaceholder": "Client name",
"address": "Address",
"subnetPlaceholder": "Subnet",
"addressDescription": "The address that this client will use for connectivity",
"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",
"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",
"clientUpdatedDescription": "The client has been updated.",
"clientUpdateFailed": "Failed to update client",
"clientUpdateError": "An error occurred while updating the client.",
"sitesFetchFailed": "Failed to fetch sites",
"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",
"addNewTarget": "Add New Target",
"targetsList": "Targets List",
"targetErrorDuplicateTargetFound": "Duplicate target found",
"httpMethod": "HTTP Method",
"selectHttpMethod": "Select HTTP method",
"domainPickerSubdomainLabel": "Subdomain",
"domainPickerBaseDomainLabel": "Base Domain",
"domainPickerSearchDomains": "Search domains...",
"domainPickerNoDomainsFound": "No domains found",
"domainPickerLoadingDomains": "Loading domains...",
"domainPickerSelectBaseDomain": "Select base domain...",
"domainPickerNotAvailableForCname": "Not available for CNAME domains",
"domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.",
"domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.",
"domainPickerFreeDomains": "Free Domains",
"domainPickerSearchForAvailableDomains": "Search for available domains",
"resourceDomain": "Domain",
"resourceEditDomain": "Edit Domain",
"siteName": "Site Name",
"proxyPort": "Port",
"resourcesTableProxyResources": "Proxy Resources",
"resourcesTableClientResources": "Client Resources",
"resourcesTableNoProxyResourcesFound": "No proxy resources found.",
"resourcesTableNoInternalResourcesFound": "No internal resources found.",
"resourcesTableDestination": "Destination",
"resourcesTableTheseResourcesForUseWith": "These resources are for use with",
"resourcesTableClients": "Clients",
"resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.",
"editInternalResourceDialogEditClientResource": "Edit Client Resource",
"editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.",
"editInternalResourceDialogResourceProperties": "Resource Properties",
"editInternalResourceDialogName": "Name",
"editInternalResourceDialogProtocol": "Protocol",
"editInternalResourceDialogSitePort": "Site Port",
"editInternalResourceDialogTargetConfiguration": "Target Configuration",
"editInternalResourceDialogDestinationIP": "Destination IP",
"editInternalResourceDialogDestinationPort": "Destination Port",
"editInternalResourceDialogCancel": "Cancel",
"editInternalResourceDialogSaveResource": "Save Resource",
"editInternalResourceDialogSuccess": "Success",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully",
"editInternalResourceDialogError": "Error",
"editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource",
"editInternalResourceDialogNameRequired": "Name is required",
"editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters",
"editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1",
"editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536",
"editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format",
"editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1",
"editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536",
"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.",
"createInternalResourceDialogResourceProperties": "Resource Properties",
"createInternalResourceDialogName": "Name",
"createInternalResourceDialogSite": "Site",
"createInternalResourceDialogSelectSite": "Select site...",
"createInternalResourceDialogSearchSites": "Search sites...",
"createInternalResourceDialogNoSitesFound": "No sites found.",
"createInternalResourceDialogProtocol": "Protocol",
"createInternalResourceDialogTcp": "TCP",
"createInternalResourceDialogUdp": "UDP",
"createInternalResourceDialogSitePort": "Site Port",
"createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.",
"createInternalResourceDialogTargetConfiguration": "Target Configuration",
"createInternalResourceDialogDestinationIP": "Destination IP",
"createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.",
"createInternalResourceDialogDestinationPort": "Destination Port",
"createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.",
"createInternalResourceDialogCancel": "Cancel",
"createInternalResourceDialogCreateResource": "Create Resource",
"createInternalResourceDialogSuccess": "Success",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully",
"createInternalResourceDialogError": "Error",
"createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource",
"createInternalResourceDialogNameRequired": "Name is required",
"createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters",
"createInternalResourceDialogPleaseSelectSite": "Please select a site",
"createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1",
"createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536",
"createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format",
"createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1",
"createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536",
"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.",
"autoLoginExternalIdp": "Auto Login with External IDP",
"autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.",
"selectIdp": "Select IDP",
"selectIdpPlaceholder": "Choose an IDP...",
"selectIdpRequired": "Please select an IDP when auto login is enabled.",
"autoLoginTitle": "Redirecting",
"autoLoginDescription": "Redirecting you to the external identity provider for authentication.",
"autoLoginProcessing": "Preparing authentication...",
"autoLoginRedirecting": "Redirecting to login...",
"autoLoginError": "Auto Login Error",
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL."
}

View File

@@ -59,7 +59,6 @@
"siteErrorCreate": "Error al crear el sitio",
"siteErrorCreateKeyPair": "Por defecto no se encuentra el par de claves o el sitio",
"siteErrorCreateDefaults": "Sitio por defecto no encontrado",
"siteNameDescription": "Este es el nombre para mostrar el sitio.",
"method": "Método",
"siteMethodDescription": "Así es como se expondrán las conexiones.",
"siteLearnNewt": "Aprende cómo instalar Newt en tu sistema",
@@ -834,6 +833,24 @@
"pincodeRequirementsLength": "El PIN debe tener exactamente 6 dígitos",
"pincodeRequirementsChars": "El PIN sólo debe contener números",
"passwordRequirementsLength": "La contraseña debe tener al menos 1 carácter",
"passwordRequirementsTitle": "Password requirements:",
"passwordRequirementLength": "At least 8 characters long",
"passwordRequirementUppercase": "At least one uppercase letter",
"passwordRequirementLowercase": "At least one lowercase letter",
"passwordRequirementNumber": "At least one number",
"passwordRequirementSpecial": "At least one special character",
"passwordRequirementsMet": "✓ Password meets all requirements",
"passwordStrength": "Password strength",
"passwordStrengthWeak": "Weak",
"passwordStrengthMedium": "Medium",
"passwordStrengthStrong": "Strong",
"passwordRequirements": "Requirements:",
"passwordRequirementLengthText": "8+ characters",
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
"passwordRequirementNumberText": "Number (0-9)",
"passwordRequirementSpecialText": "Special character (!@#$%...)",
"passwordsDoNotMatch": "Passwords do not match",
"otpEmailRequirementsLength": "OTP debe tener al menos 1 carácter",
"otpEmailSent": "OTP enviado",
"otpEmailSentDescription": "Un OTP ha sido enviado a tu correo electrónico",
@@ -968,6 +985,9 @@
"actionDeleteSite": "Eliminar sitio",
"actionGetSite": "Obtener sitio",
"actionListSites": "Listar sitios",
"setupToken": "Setup Token",
"setupTokenPlaceholder": "Enter the setup token from the server console",
"setupTokenRequired": "Setup token is required",
"actionUpdateSite": "Actualizar sitio",
"actionListSiteRoles": "Lista de roles permitidos del sitio",
"actionCreateResource": "Crear Recurso",
@@ -1023,6 +1043,11 @@
"actionDeleteIdpOrg": "Eliminar política de IDP Org",
"actionListIdpOrgs": "Listar Orgs IDP",
"actionUpdateIdpOrg": "Actualizar IDP Org",
"actionCreateClient": "Crear cliente",
"actionDeleteClient": "Eliminar cliente",
"actionUpdateClient": "Actualizar cliente",
"actionListClients": "Listar clientes",
"actionGetClient": "Obtener cliente",
"noneSelected": "Ninguno seleccionado",
"orgNotFound2": "No se encontraron organizaciones.",
"searchProgress": "Buscar...",
@@ -1094,7 +1119,7 @@
"sidebarAllUsers": "Todos los usuarios",
"sidebarIdentityProviders": "Proveedores de identidad",
"sidebarLicense": "Licencia",
"sidebarClients": "Clientes",
"sidebarClients": "Clientes (Beta)",
"sidebarDomains": "Dominios",
"enableDockerSocket": "Habilitar conector Docker",
"enableDockerSocketDescription": "Habilitar el descubrimiento de Docker Socket para completar la información del contenedor. La ruta del socket debe proporcionarse a Newt.",
@@ -1162,7 +1187,7 @@
"selectDomainTypeCnameName": "Dominio único (CNAME)",
"selectDomainTypeCnameDescription": "Solo este dominio específico. Úsalo para subdominios individuales o entradas específicas de dominio.",
"selectDomainTypeWildcardName": "Dominio comodín",
"selectDomainTypeWildcardDescription": "Este dominio y su primer nivel de subdominios.",
"selectDomainTypeWildcardDescription": "Este dominio y sus subdominios.",
"domainDelegation": "Dominio único",
"selectType": "Selecciona un tipo",
"actions": "Acciones",
@@ -1196,7 +1221,7 @@
"sidebarExpand": "Expandir",
"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": "Ingresa tu dominio",
"domainPickerEnterDomain": "Dominio",
"domainPickerPlaceholder": "myapp.example.com, api.v1.miDominio.com, o solo myapp",
"domainPickerDescription": "Ingresa el dominio completo del recurso para ver las opciones disponibles.",
"domainPickerDescriptionSaas": "Ingresa un dominio completo, subdominio o simplemente un nombre para ver las opciones disponibles",
@@ -1206,7 +1231,7 @@
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Comprobando disponibilidad...",
"domainPickerNoMatchingDomains": "No se encontraron dominios coincidentes para \"{userInput}\". Prueba con un dominio diferente o revisa la configuración de dominio de tu organización.",
"domainPickerNoMatchingDomains": "No se encontraron dominios que coincidan. Intente con un dominio diferente o verifique la configuración de dominios de su organización.",
"domainPickerOrganizationDomains": "Dominios de la organización",
"domainPickerProvidedDomains": "Dominios proporcionados",
"domainPickerSubdomain": "Subdominio: {subdomain}",
@@ -1266,6 +1291,7 @@
"createDomainName": "Nombre:",
"createDomainValue": "Valor:",
"createDomainCnameRecords": "Registros CNAME",
"createDomainARecords": "Registros A",
"createDomainRecordNumber": "Registro {number}",
"createDomainTxtRecords": "Registros TXT",
"createDomainSaveTheseRecords": "Guardar estos registros",
@@ -1273,5 +1299,50 @@
"createDomainDnsPropagation": "Propagación DNS",
"createDomainDnsPropagationDescription": "Los cambios de DNS pueden tardar un tiempo en propagarse a través de internet. Esto puede tardar desde unos pocos minutos hasta 48 horas, dependiendo de tu proveedor de DNS y la configuración de TTL.",
"resourcePortRequired": "Se requiere número de puerto para recursos no HTTP",
"resourcePortNotAllowed": "El número de puerto no debe establecerse para recursos HTTP"
}
"resourcePortNotAllowed": "El número de puerto no debe establecerse para recursos HTTP",
"signUpTerms": {
"IAgreeToThe": "Estoy de acuerdo con los",
"termsOfService": "términos del servicio",
"and": "y",
"privacyPolicy": "política de privacidad"
},
"siteRequired": "El sitio es requerido.",
"olmTunnel": "Túnel Olm",
"olmTunnelDescription": "Usar Olm para la conectividad del cliente",
"errorCreatingClient": "Error al crear el cliente",
"clientDefaultsNotFound": "Configuración predeterminada del cliente no encontrada",
"createClient": "Crear cliente",
"createClientDescription": "Crear un cliente nuevo para conectar a sus sitios",
"seeAllClients": "Ver todos los clientes",
"clientInformation": "Información del cliente",
"clientNamePlaceholder": "Nombre del cliente",
"address": "Dirección",
"subnetPlaceholder": "Subred",
"addressDescription": "La dirección que este cliente utilizará para la conectividad",
"selectSites": "Seleccionar sitios",
"sitesDescription": "El cliente tendrá conectividad con los sitios seleccionados",
"clientInstallOlm": "Instalar Olm",
"clientInstallOlmDescription": "Obtén Olm funcionando en tu sistema",
"clientOlmCredentials": "Credenciales Olm",
"clientOlmCredentialsDescription": "Así es como Olm se autentificará con el servidor",
"olmEndpoint": "Punto final Olm",
"olmId": "ID de Olm",
"olmSecretKey": "Clave secreta de Olm",
"clientCredentialsSave": "Guarda tus credenciales",
"clientCredentialsSaveDescription": "Sólo podrás verlo una vez. Asegúrate de copiarlo a un lugar seguro.",
"generalSettingsDescription": "Configura la configuración general para este cliente",
"clientUpdated": "Cliente actualizado",
"clientUpdatedDescription": "El cliente ha sido actualizado.",
"clientUpdateFailed": "Error al actualizar el cliente",
"clientUpdateError": "Se ha producido un error al actualizar el cliente.",
"sitesFetchFailed": "Error al obtener los sitios",
"sitesFetchError": "Se ha producido un error al recuperar los sitios.",
"olmErrorFetchReleases": "Se ha producido un error al recuperar las versiones de Olm.",
"olmErrorFetchLatest": "Se ha producido un error al recuperar la última versión de Olm.",
"remoteSubnets": "Subredes remotas",
"enterCidrRange": "Ingresa el rango CIDR",
"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": "Habilitar proxy público",
"resourceEnableProxyDescription": "Habilite el proxy público para este recurso. Esto permite el acceso al recurso desde fuera de la red a través de la nube en un puerto abierto. Requiere configuración de Traefik.",
"externalProxyEnabled": "Proxy externo habilitado"
}

View File

@@ -59,7 +59,6 @@
"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",
"siteNameDescription": "Ceci est le nom d'affichage du site.",
"method": "Méthode",
"siteMethodDescription": "C'est ainsi que vous exposerez les connexions.",
"siteLearnNewt": "Apprenez à installer Newt sur votre système",
@@ -834,6 +833,24 @@
"pincodeRequirementsLength": "Le code PIN doit comporter exactement 6 chiffres",
"pincodeRequirementsChars": "Le code PIN ne doit contenir que des chiffres",
"passwordRequirementsLength": "Le mot de passe doit comporter au moins 1 caractère",
"passwordRequirementsTitle": "Password requirements:",
"passwordRequirementLength": "At least 8 characters long",
"passwordRequirementUppercase": "At least one uppercase letter",
"passwordRequirementLowercase": "At least one lowercase letter",
"passwordRequirementNumber": "At least one number",
"passwordRequirementSpecial": "At least one special character",
"passwordRequirementsMet": "✓ Password meets all requirements",
"passwordStrength": "Password strength",
"passwordStrengthWeak": "Weak",
"passwordStrengthMedium": "Medium",
"passwordStrengthStrong": "Strong",
"passwordRequirements": "Requirements:",
"passwordRequirementLengthText": "8+ characters",
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
"passwordRequirementNumberText": "Number (0-9)",
"passwordRequirementSpecialText": "Special character (!@#$%...)",
"passwordsDoNotMatch": "Passwords do not match",
"otpEmailRequirementsLength": "L'OTP doit comporter au moins 1 caractère",
"otpEmailSent": "OTP envoyé",
"otpEmailSentDescription": "Un OTP a été envoyé à votre e-mail",
@@ -968,6 +985,9 @@
"actionDeleteSite": "Supprimer un site",
"actionGetSite": "Obtenir un site",
"actionListSites": "Lister les sites",
"setupToken": "Setup Token",
"setupTokenPlaceholder": "Enter the setup token from the server console",
"setupTokenRequired": "Setup token is required",
"actionUpdateSite": "Mettre à jour un site",
"actionListSiteRoles": "Lister les rôles autorisés du site",
"actionCreateResource": "Créer une ressource",
@@ -1023,6 +1043,11 @@
"actionDeleteIdpOrg": "Supprimer une politique d'organisation IDP",
"actionListIdpOrgs": "Lister les organisations IDP",
"actionUpdateIdpOrg": "Mettre à jour une organisation IDP",
"actionCreateClient": "Créer un client",
"actionDeleteClient": "Supprimer le client",
"actionUpdateClient": "Mettre à jour le client",
"actionListClients": "Liste des clients",
"actionGetClient": "Obtenir le client",
"noneSelected": "Aucune sélection",
"orgNotFound2": "Aucune organisation trouvée.",
"searchProgress": "Rechercher...",
@@ -1094,7 +1119,7 @@
"sidebarAllUsers": "Tous les utilisateurs",
"sidebarIdentityProviders": "Fournisseurs d'identité",
"sidebarLicense": "Licence",
"sidebarClients": "Clients",
"sidebarClients": "Clients (Bêta)",
"sidebarDomains": "Domaines",
"enableDockerSocket": "Activer Docker Socket",
"enableDockerSocketDescription": "Activer la découverte Docker Socket pour remplir les informations du conteneur. Le chemin du socket doit être fourni à Newt.",
@@ -1162,7 +1187,7 @@
"selectDomainTypeCnameName": "Domaine unique (CNAME)",
"selectDomainTypeCnameDescription": "Juste ce domaine spécifique. Utilisez ce paramètre pour des sous-domaines individuels ou des entrées de domaine spécifiques.",
"selectDomainTypeWildcardName": "Domaine Générique",
"selectDomainTypeWildcardDescription": "Ce domaine et son premier niveau de sous-domaines.",
"selectDomainTypeWildcardDescription": "Ce domaine et ses sous-domaines.",
"domainDelegation": "Domaine Unique",
"selectType": "Sélectionnez un type",
"actions": "Actions",
@@ -1196,7 +1221,7 @@
"sidebarExpand": "Développer",
"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": "Entrez votre domaine",
"domainPickerEnterDomain": "Domaine",
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, ou simplement myapp",
"domainPickerDescription": "Entrez le domaine complet de la ressource pour voir les options disponibles.",
"domainPickerDescriptionSaas": "Entrez un domaine complet, un sous-domaine ou juste un nom pour voir les options disponibles",
@@ -1206,7 +1231,7 @@
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Vérification de la disponibilité...",
"domainPickerNoMatchingDomains": "Aucun domaine correspondant trouvé pour \"{userInput}\". Essayez un autre domaine ou vérifiez les paramètres de domaine de votre organisation.",
"domainPickerNoMatchingDomains": "Aucun domaine correspondant trouvé. Essayez un autre domaine ou vérifiez les paramètres de domaine de votre organisation.",
"domainPickerOrganizationDomains": "Domaines de l'organisation",
"domainPickerProvidedDomains": "Domaines fournis",
"domainPickerSubdomain": "Sous-domaine : {subdomain}",
@@ -1266,6 +1291,7 @@
"createDomainName": "Nom :",
"createDomainValue": "Valeur :",
"createDomainCnameRecords": "Enregistrements CNAME",
"createDomainARecords": "Enregistrements A",
"createDomainRecordNumber": "Enregistrement {number}",
"createDomainTxtRecords": "Enregistrements TXT",
"createDomainSaveTheseRecords": "Enregistrez ces enregistrements",
@@ -1273,5 +1299,50 @@
"createDomainDnsPropagation": "Propagation DNS",
"createDomainDnsPropagationDescription": "Les modifications DNS peuvent mettre du temps à se propager sur internet. Cela peut prendre de quelques minutes à 48 heures selon votre fournisseur DNS et les réglages TTL.",
"resourcePortRequired": "Le numéro de port est requis pour les ressources non-HTTP",
"resourcePortNotAllowed": "Le numéro de port ne doit pas être défini pour les ressources HTTP"
}
"resourcePortNotAllowed": "Le numéro de port ne doit pas être défini pour les ressources HTTP",
"signUpTerms": {
"IAgreeToThe": "Je suis d'accord avec",
"termsOfService": "les conditions d'utilisation",
"and": "et",
"privacyPolicy": "la politique de confidentialité"
},
"siteRequired": "Le site est requis.",
"olmTunnel": "Tunnel Olm",
"olmTunnelDescription": "Utilisez Olm pour la connectivité client",
"errorCreatingClient": "Erreur lors de la création du client",
"clientDefaultsNotFound": "Les paramètres par défaut du client sont introuvables",
"createClient": "Créer un client",
"createClientDescription": "Créez un nouveau client pour vous connecter à vos sites",
"seeAllClients": "Voir tous les clients",
"clientInformation": "Informations client",
"clientNamePlaceholder": "Nom du client",
"address": "Adresse",
"subnetPlaceholder": "Sous-réseau",
"addressDescription": "L'adresse que ce client utilisera pour la connectivité",
"selectSites": "Sélectionner des sites",
"sitesDescription": "Le client aura une connectivité vers les sites sélectionnés",
"clientInstallOlm": "Installer Olm",
"clientInstallOlmDescription": "Faites fonctionner Olm sur votre système",
"clientOlmCredentials": "Identifiants Olm",
"clientOlmCredentialsDescription": "C'est ainsi qu'Olm s'authentifiera auprès du serveur",
"olmEndpoint": "Point de terminaison Olm",
"olmId": "ID Olm",
"olmSecretKey": "Clé secrète Olm",
"clientCredentialsSave": "Enregistrez vos identifiants",
"clientCredentialsSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Assurez-vous de la copier dans un endroit sécurisé.",
"generalSettingsDescription": "Configurez les paramètres généraux pour ce client",
"clientUpdated": "Client mis à jour",
"clientUpdatedDescription": "Le client a été mis à jour.",
"clientUpdateFailed": "Échec de la mise à jour du client",
"clientUpdateError": "Une erreur s'est produite lors de la mise à jour du client.",
"sitesFetchFailed": "Échec de la récupération des sites",
"sitesFetchError": "Une erreur s'est produite lors de la récupération des sites.",
"olmErrorFetchReleases": "Une erreur s'est produite lors de la récupération des versions d'Olm.",
"olmErrorFetchLatest": "Une erreur s'est produite lors de la récupération de la dernière version d'Olm.",
"remoteSubnets": "Sous-réseaux distants",
"enterCidrRange": "Entrez la plage CIDR",
"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": "Activer le proxy public",
"resourceEnableProxyDescription": "Activez le proxy public vers cette ressource. Cela permet d'accéder à la ressource depuis l'extérieur du réseau via le cloud sur un port ouvert. Nécessite la configuration de Traefik.",
"externalProxyEnabled": "Proxy externe activé"
}

View File

@@ -59,7 +59,6 @@
"siteErrorCreate": "Errore nella creazione del sito",
"siteErrorCreateKeyPair": "Coppia di chiavi o valori predefiniti del sito non trovati",
"siteErrorCreateDefaults": "Predefiniti del sito non trovati",
"siteNameDescription": "Questo è il nome visualizzato per il sito.",
"method": "Metodo",
"siteMethodDescription": "Questo è il modo in cui esporrete le connessioni.",
"siteLearnNewt": "Scopri come installare Newt sul tuo sistema",
@@ -834,6 +833,24 @@
"pincodeRequirementsLength": "Il PIN deve essere esattamente di 6 cifre",
"pincodeRequirementsChars": "Il PIN deve contenere solo numeri",
"passwordRequirementsLength": "La password deve essere lunga almeno 1 carattere",
"passwordRequirementsTitle": "Password requirements:",
"passwordRequirementLength": "At least 8 characters long",
"passwordRequirementUppercase": "At least one uppercase letter",
"passwordRequirementLowercase": "At least one lowercase letter",
"passwordRequirementNumber": "At least one number",
"passwordRequirementSpecial": "At least one special character",
"passwordRequirementsMet": "✓ Password meets all requirements",
"passwordStrength": "Password strength",
"passwordStrengthWeak": "Weak",
"passwordStrengthMedium": "Medium",
"passwordStrengthStrong": "Strong",
"passwordRequirements": "Requirements:",
"passwordRequirementLengthText": "8+ characters",
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
"passwordRequirementNumberText": "Number (0-9)",
"passwordRequirementSpecialText": "Special character (!@#$%...)",
"passwordsDoNotMatch": "Passwords do not match",
"otpEmailRequirementsLength": "L'OTP deve essere lungo almeno 1 carattere",
"otpEmailSent": "OTP Inviato",
"otpEmailSentDescription": "Un OTP è stato inviato alla tua email",
@@ -968,6 +985,9 @@
"actionDeleteSite": "Elimina Sito",
"actionGetSite": "Ottieni Sito",
"actionListSites": "Elenca Siti",
"setupToken": "Setup Token",
"setupTokenPlaceholder": "Enter the setup token from the server console",
"setupTokenRequired": "Setup token is required",
"actionUpdateSite": "Aggiorna Sito",
"actionListSiteRoles": "Elenca Ruoli Sito Consentiti",
"actionCreateResource": "Crea Risorsa",
@@ -1023,6 +1043,11 @@
"actionDeleteIdpOrg": "Elimina Politica Org IDP",
"actionListIdpOrgs": "Elenca Org IDP",
"actionUpdateIdpOrg": "Aggiorna Org IDP",
"actionCreateClient": "Crea Client",
"actionDeleteClient": "Elimina Client",
"actionUpdateClient": "Aggiorna Client",
"actionListClients": "Elenco Clienti",
"actionGetClient": "Ottieni Client",
"noneSelected": "Nessuna selezione",
"orgNotFound2": "Nessuna organizzazione trovata.",
"searchProgress": "Ricerca...",
@@ -1094,7 +1119,7 @@
"sidebarAllUsers": "Tutti Gli Utenti",
"sidebarIdentityProviders": "Fornitori Di Identità",
"sidebarLicense": "Licenza",
"sidebarClients": "Clienti",
"sidebarClients": "Clienti (Beta)",
"sidebarDomains": "Domini",
"enableDockerSocket": "Abilita Docker Socket",
"enableDockerSocketDescription": "Abilita il rilevamento Docker Socket per popolare le informazioni del contenitore. Il percorso del socket deve essere fornito a Newt.",
@@ -1162,7 +1187,7 @@
"selectDomainTypeCnameName": "Dominio Singolo (CNAME)",
"selectDomainTypeCnameDescription": "Solo questo dominio specifico. Usa questo per sottodomini individuali o specifiche voci di dominio.",
"selectDomainTypeWildcardName": "Dominio Jolly",
"selectDomainTypeWildcardDescription": "Questo dominio e il suo primo livello di sottodomini.",
"selectDomainTypeWildcardDescription": "Questo dominio e i suoi sottodomini.",
"domainDelegation": "Dominio Singolo",
"selectType": "Seleziona un tipo",
"actions": "Azioni",
@@ -1196,7 +1221,7 @@
"sidebarExpand": "Espandi",
"newtUpdateAvailable": "Aggiornamento Disponibile",
"newtUpdateAvailableInfo": "È disponibile una nuova versione di Newt. Si prega di aggiornare all'ultima versione per la migliore esperienza.",
"domainPickerEnterDomain": "Inserisci il tuo dominio",
"domainPickerEnterDomain": "Dominio",
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, o semplicemente myapp",
"domainPickerDescription": "Inserisci il dominio completo della risorsa per vedere le opzioni disponibili.",
"domainPickerDescriptionSaas": "Inserisci un dominio completo, un sottodominio o semplicemente un nome per vedere le opzioni disponibili",
@@ -1206,7 +1231,7 @@
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Controllando la disponibilità...",
"domainPickerNoMatchingDomains": "Nessun dominio corrispondente trovato per \"{userInput}\". Prova un altro dominio o controlla le impostazioni del dominio della tua organizzazione.",
"domainPickerNoMatchingDomains": "Nessun dominio corrispondente trovato. Prova un dominio diverso o verifica le impostazioni del dominio della tua organizzazione.",
"domainPickerOrganizationDomains": "Domini dell'Organizzazione",
"domainPickerProvidedDomains": "Domini Forniti",
"domainPickerSubdomain": "Sottodominio: {subdomain}",
@@ -1266,6 +1291,7 @@
"createDomainName": "Nome:",
"createDomainValue": "Valore:",
"createDomainCnameRecords": "Record CNAME",
"createDomainARecords": "Record A",
"createDomainRecordNumber": "Record {number}",
"createDomainTxtRecords": "Record TXT",
"createDomainSaveTheseRecords": "Salva Questi Record",
@@ -1273,5 +1299,50 @@
"createDomainDnsPropagation": "Propagazione DNS",
"createDomainDnsPropagationDescription": "Le modifiche DNS possono richiedere del tempo per propagarsi in Internet. Questo può richiedere da pochi minuti a 48 ore, a seconda del tuo provider DNS e delle impostazioni TTL.",
"resourcePortRequired": "Numero di porta richiesto per risorse non-HTTP",
"resourcePortNotAllowed": "Il numero di porta non deve essere impostato per risorse HTTP"
}
"resourcePortNotAllowed": "Il numero di porta non deve essere impostato per risorse HTTP",
"signUpTerms": {
"IAgreeToThe": "Accetto i",
"termsOfService": "termini di servizio",
"and": "e",
"privacyPolicy": "informativa sulla privacy"
},
"siteRequired": "Il sito è richiesto.",
"olmTunnel": "Olm Tunnel",
"olmTunnelDescription": "Usa Olm per la connettività client",
"errorCreatingClient": "Errore nella creazione del client",
"clientDefaultsNotFound": "Impostazioni predefinite del client non trovate",
"createClient": "Crea Cliente",
"createClientDescription": "Crea un nuovo cliente per connettersi ai tuoi siti",
"seeAllClients": "Vedi Tutti i Clienti",
"clientInformation": "Informazioni sul Cliente",
"clientNamePlaceholder": "Nome Cliente",
"address": "Indirizzo",
"subnetPlaceholder": "Sottorete",
"addressDescription": "L'indirizzo che questo cliente utilizzerà per la connettività",
"selectSites": "Seleziona siti",
"sitesDescription": "Il cliente avrà connettività ai siti selezionati",
"clientInstallOlm": "Installa Olm",
"clientInstallOlmDescription": "Avvia Olm sul tuo sistema",
"clientOlmCredentials": "Credenziali Olm",
"clientOlmCredentialsDescription": "Ecco come Olm si autenticherà con il server",
"olmEndpoint": "Endpoint Olm",
"olmId": "ID Olm",
"olmSecretKey": "Chiave Segreta Olm",
"clientCredentialsSave": "Salva le Tue Credenziali",
"clientCredentialsSaveDescription": "Potrai vederlo solo una volta. Assicurati di copiarlo in un luogo sicuro.",
"generalSettingsDescription": "Configura le impostazioni generali per questo cliente",
"clientUpdated": "Cliente aggiornato",
"clientUpdatedDescription": "Il cliente è stato aggiornato.",
"clientUpdateFailed": "Impossibile aggiornare il cliente",
"clientUpdateError": "Si è verificato un errore durante l'aggiornamento del cliente.",
"sitesFetchFailed": "Impossibile recuperare i siti",
"sitesFetchError": "Si è verificato un errore durante il recupero dei siti.",
"olmErrorFetchReleases": "Si è verificato un errore durante il recupero delle versioni di Olm.",
"olmErrorFetchLatest": "Si è verificato un errore durante il recupero dell'ultima versione di Olm.",
"remoteSubnets": "Sottoreti Remote",
"enterCidrRange": "Inserisci l'intervallo CIDR",
"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": "Abilita Proxy Pubblico",
"resourceEnableProxyDescription": "Abilita il proxy pubblico a questa risorsa. Consente l'accesso alla risorsa dall'esterno della rete tramite il cloud su una porta aperta. Richiede la configurazione di Traefik.",
"externalProxyEnabled": "Proxy Esterno Abilitato"
}

View File

@@ -59,7 +59,6 @@
"siteErrorCreate": "사이트 생성 오류",
"siteErrorCreateKeyPair": "키 쌍 또는 사이트 기본값을 찾을 수 없습니다",
"siteErrorCreateDefaults": "사이트 기본값을 찾을 수 없습니다",
"siteNameDescription": "이것은 사이트의 표시 이름입니다.",
"method": "방법",
"siteMethodDescription": "이것이 연결을 노출하는 방법입니다.",
"siteLearnNewt": "시스템에 Newt 설치하는 방법 배우기",
@@ -834,6 +833,24 @@
"pincodeRequirementsLength": "PIN은 정확히 6자리여야 합니다",
"pincodeRequirementsChars": "PIN은 숫자만 포함해야 합니다.",
"passwordRequirementsLength": "비밀번호는 최소 1자 이상이어야 합니다",
"passwordRequirementsTitle": "Password requirements:",
"passwordRequirementLength": "At least 8 characters long",
"passwordRequirementUppercase": "At least one uppercase letter",
"passwordRequirementLowercase": "At least one lowercase letter",
"passwordRequirementNumber": "At least one number",
"passwordRequirementSpecial": "At least one special character",
"passwordRequirementsMet": "✓ Password meets all requirements",
"passwordStrength": "Password strength",
"passwordStrengthWeak": "Weak",
"passwordStrengthMedium": "Medium",
"passwordStrengthStrong": "Strong",
"passwordRequirements": "Requirements:",
"passwordRequirementLengthText": "8+ characters",
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
"passwordRequirementNumberText": "Number (0-9)",
"passwordRequirementSpecialText": "Special character (!@#$%...)",
"passwordsDoNotMatch": "Passwords do not match",
"otpEmailRequirementsLength": "OTP는 최소 1자 이상이어야 합니다",
"otpEmailSent": "OTP 전송됨",
"otpEmailSentDescription": "OTP가 귀하의 이메일로 전송되었습니다.",
@@ -968,6 +985,9 @@
"actionDeleteSite": "사이트 삭제",
"actionGetSite": "사이트 가져오기",
"actionListSites": "사이트 목록",
"setupToken": "Setup Token",
"setupTokenPlaceholder": "Enter the setup token from the server console",
"setupTokenRequired": "Setup token is required",
"actionUpdateSite": "사이트 업데이트",
"actionListSiteRoles": "허용된 사이트 역할 목록",
"actionCreateResource": "리소스 생성",
@@ -1023,6 +1043,11 @@
"actionDeleteIdpOrg": "IDP 조직 정책 삭제",
"actionListIdpOrgs": "IDP 조직 목록",
"actionUpdateIdpOrg": "IDP 조직 업데이트",
"actionCreateClient": "Create Client",
"actionDeleteClient": "Delete Client",
"actionUpdateClient": "Update Client",
"actionListClients": "List Clients",
"actionGetClient": "Get Client",
"noneSelected": "선택된 항목 없음",
"orgNotFound2": "조직이 없습니다.",
"searchProgress": "검색...",
@@ -1094,7 +1119,7 @@
"sidebarAllUsers": "모든 사용자",
"sidebarIdentityProviders": "신원 공급자",
"sidebarLicense": "라이선스",
"sidebarClients": "클라이언트",
"sidebarClients": "Clients (Beta)",
"sidebarDomains": "도메인",
"enableDockerSocket": "Docker 소켓 활성화",
"enableDockerSocketDescription": "컨테이너 정보를 채우기 위해 Docker 소켓 검색을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.",
@@ -1162,7 +1187,7 @@
"selectDomainTypeCnameName": "단일 도메인 (CNAME)",
"selectDomainTypeCnameDescription": "단일 하위 도메인 또는 특정 도메인 항목에 사용됩니다.",
"selectDomainTypeWildcardName": "와일드카드 도메인",
"selectDomainTypeWildcardDescription": "이 도메인과 그 첫 번째 레벨의 하위 도메인입니다.",
"selectDomainTypeWildcardDescription": "This domain and its subdomains.",
"domainDelegation": "단일 도메인",
"selectType": "유형 선택",
"actions": "작업",
@@ -1196,7 +1221,7 @@
"sidebarExpand": "확장하기",
"newtUpdateAvailable": "업데이트 가능",
"newtUpdateAvailableInfo": "뉴트의 새 버전이 출시되었습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
"domainPickerEnterDomain": "도메인 입력",
"domainPickerEnterDomain": "Domain",
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, 또는 그냥 myapp",
"domainPickerDescription": "Enter the full domain of the resource to see available options.",
"domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options",
@@ -1206,7 +1231,7 @@
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "가용성을 확인 중...",
"domainPickerNoMatchingDomains": "\"{userInput}\"에 해당하는 도메인을 찾을 수 없습니다. 다른 도메인을 시도하거나 조직의 도메인 설정을 확인하세요.",
"domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.",
"domainPickerOrganizationDomains": "조직 도메인",
"domainPickerProvidedDomains": "제공된 도메인",
"domainPickerSubdomain": "서브도메인: {subdomain}",
@@ -1266,6 +1291,7 @@
"createDomainName": "이름:",
"createDomainValue": "값:",
"createDomainCnameRecords": "CNAME 레코드",
"createDomainARecords": "A Records",
"createDomainRecordNumber": "레코드 {number}",
"createDomainTxtRecords": "TXT 레코드",
"createDomainSaveTheseRecords": "이 레코드 저장",
@@ -1273,5 +1299,50 @@
"createDomainDnsPropagation": "DNS 전파",
"createDomainDnsPropagationDescription": "DNS 변경 사항은 인터넷 전체에 전파되는 데 시간이 걸립니다. DNS 제공자와 TTL 설정에 따라 몇 분에서 48시간까지 걸릴 수 있습니다.",
"resourcePortRequired": "HTTP 리소스가 아닌 경우 포트 번호가 필요합니다",
"resourcePortNotAllowed": "HTTP 리소스에 대해 포트 번호를 설정하지 마세요"
}
"resourcePortNotAllowed": "HTTP 리소스에 대해 포트 번호를 설정하지 마세요",
"signUpTerms": {
"IAgreeToThe": "I agree to the",
"termsOfService": "terms of service",
"and": "and",
"privacyPolicy": "privacy policy"
},
"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",
"seeAllClients": "See All Clients",
"clientInformation": "Client Information",
"clientNamePlaceholder": "Client name",
"address": "Address",
"subnetPlaceholder": "Subnet",
"addressDescription": "The address that this client will use for connectivity",
"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",
"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",
"clientUpdatedDescription": "The client has been updated.",
"clientUpdateFailed": "Failed to update client",
"clientUpdateError": "An error occurred while updating the client.",
"sitesFetchFailed": "Failed to fetch sites",
"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"
}

1348
messages/nb-NO.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -59,7 +59,6 @@
"siteErrorCreate": "Fout bij maken site",
"siteErrorCreateKeyPair": "Key pair of site standaard niet gevonden",
"siteErrorCreateDefaults": "Standaardinstellingen niet gevonden",
"siteNameDescription": "Dit is de weergavenaam van de site.",
"method": "Methode",
"siteMethodDescription": "Op deze manier legt u verbindingen bloot.",
"siteLearnNewt": "Leer hoe u Newt kunt installeren op uw systeem",
@@ -834,6 +833,24 @@
"pincodeRequirementsLength": "Pincode moet precies 6 cijfers zijn",
"pincodeRequirementsChars": "Pincode mag alleen cijfers bevatten",
"passwordRequirementsLength": "Wachtwoord moet ten minste 1 teken lang zijn",
"passwordRequirementsTitle": "Password requirements:",
"passwordRequirementLength": "At least 8 characters long",
"passwordRequirementUppercase": "At least one uppercase letter",
"passwordRequirementLowercase": "At least one lowercase letter",
"passwordRequirementNumber": "At least one number",
"passwordRequirementSpecial": "At least one special character",
"passwordRequirementsMet": "✓ Password meets all requirements",
"passwordStrength": "Password strength",
"passwordStrengthWeak": "Weak",
"passwordStrengthMedium": "Medium",
"passwordStrengthStrong": "Strong",
"passwordRequirements": "Requirements:",
"passwordRequirementLengthText": "8+ characters",
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
"passwordRequirementNumberText": "Number (0-9)",
"passwordRequirementSpecialText": "Special character (!@#$%...)",
"passwordsDoNotMatch": "Passwords do not match",
"otpEmailRequirementsLength": "OTP moet minstens 1 teken lang zijn",
"otpEmailSent": "OTP verzonden",
"otpEmailSentDescription": "Een OTP is naar uw e-mail verzonden",
@@ -968,6 +985,9 @@
"actionDeleteSite": "Site verwijderen",
"actionGetSite": "Site ophalen",
"actionListSites": "Sites weergeven",
"setupToken": "Setup Token",
"setupTokenPlaceholder": "Enter the setup token from the server console",
"setupTokenRequired": "Setup token is required",
"actionUpdateSite": "Site bijwerken",
"actionListSiteRoles": "Toon toegestane sitenollen",
"actionCreateResource": "Bron maken",
@@ -1023,6 +1043,11 @@
"actionDeleteIdpOrg": "Verwijder IDP Org Beleid",
"actionListIdpOrgs": "Toon IDP Orgs",
"actionUpdateIdpOrg": "IDP-org bijwerken",
"actionCreateClient": "Client aanmaken",
"actionDeleteClient": "Verwijder klant",
"actionUpdateClient": "Klant bijwerken",
"actionListClients": "Lijst klanten",
"actionGetClient": "Client ophalen",
"noneSelected": "Niet geselecteerd",
"orgNotFound2": "Geen organisaties gevonden.",
"searchProgress": "Zoeken...",
@@ -1094,7 +1119,7 @@
"sidebarAllUsers": "Alle gebruikers",
"sidebarIdentityProviders": "Identiteit aanbieders",
"sidebarLicense": "Licentie",
"sidebarClients": "Cliënten",
"sidebarClients": "Clients (Bèta)",
"sidebarDomains": "Domeinen",
"enableDockerSocket": "Docker Socket inschakelen",
"enableDockerSocketDescription": "Docker Socket-ontdekking inschakelen voor het invullen van containerinformatie. Socket-pad moet aan Newt worden verstrekt.",
@@ -1162,7 +1187,7 @@
"selectDomainTypeCnameName": "Enkel domein (CNAME)",
"selectDomainTypeCnameDescription": "Alleen dit specifieke domein. Gebruik dit voor individuele subdomeinen of specifieke domeinvermeldingen.",
"selectDomainTypeWildcardName": "Wildcard Domein",
"selectDomainTypeWildcardDescription": "Dit domein en zijn eerste niveau van subdomeinen.",
"selectDomainTypeWildcardDescription": "Dit domein en zijn subdomeinen.",
"domainDelegation": "Enkel domein",
"selectType": "Selecteer een type",
"actions": "acties",
@@ -1196,7 +1221,7 @@
"sidebarExpand": "Uitklappen",
"newtUpdateAvailable": "Update beschikbaar",
"newtUpdateAvailableInfo": "Er is een nieuwe versie van Newt beschikbaar. Update naar de nieuwste versie voor de beste ervaring.",
"domainPickerEnterDomain": "Voer je domein in",
"domainPickerEnterDomain": "Domein",
"domainPickerPlaceholder": "mijnapp.voorbeeld.com, api.v1.mijndomein.com, of gewoon mijnapp",
"domainPickerDescription": "Voer de volledige domein van de bron in om beschikbare opties te zien.",
"domainPickerDescriptionSaas": "Voer een volledig domein, subdomein of gewoon een naam in om beschikbare opties te zien",
@@ -1206,7 +1231,7 @@
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Beschikbaarheid controleren...",
"domainPickerNoMatchingDomains": "Geen overeenkomende domeinen gevonden voor \"{userInput}\". Probeer een ander domein of controleer de domeininstellingen van je organisatie.",
"domainPickerNoMatchingDomains": "Geen overeenkomende domeinen gevonden. Probeer een ander domein of controleer de domeininstellingen van uw organisatie.",
"domainPickerOrganizationDomains": "Organisatiedomeinen",
"domainPickerProvidedDomains": "Aangeboden domeinen",
"domainPickerSubdomain": "Subdomein: {subdomain}",
@@ -1266,6 +1291,7 @@
"createDomainName": "Naam:",
"createDomainValue": "Waarde:",
"createDomainCnameRecords": "CNAME-records",
"createDomainARecords": "A Records",
"createDomainRecordNumber": "Record {number}",
"createDomainTxtRecords": "TXT-records",
"createDomainSaveTheseRecords": "Deze records opslaan",
@@ -1273,5 +1299,50 @@
"createDomainDnsPropagation": "DNS-propagatie",
"createDomainDnsPropagationDescription": "DNS-wijzigingen kunnen enige tijd duren om over het internet te worden verspreid. Dit kan enkele minuten tot 48 uur duren, afhankelijk van je DNS-provider en TTL-instellingen.",
"resourcePortRequired": "Poortnummer is vereist voor niet-HTTP-bronnen",
"resourcePortNotAllowed": "Poortnummer mag niet worden ingesteld voor HTTP-bronnen"
}
"resourcePortNotAllowed": "Poortnummer mag niet worden ingesteld voor HTTP-bronnen",
"signUpTerms": {
"IAgreeToThe": "Ik ga akkoord met de",
"termsOfService": "servicevoorwaarden",
"and": "en",
"privacyPolicy": "privacybeleid"
},
"siteRequired": "Site is vereist.",
"olmTunnel": "Olm Tunnel",
"olmTunnelDescription": "Gebruik Olm voor clientconnectiviteit",
"errorCreatingClient": "Fout bij het aanmaken van de client",
"clientDefaultsNotFound": "Standaardinstellingen van klant niet gevonden",
"createClient": "Client aanmaken",
"createClientDescription": "Maak een nieuwe client aan om verbinding te maken met uw sites",
"seeAllClients": "Alle clients bekijken",
"clientInformation": "Klantinformatie",
"clientNamePlaceholder": "Clientnaam",
"address": "Adres",
"subnetPlaceholder": "Subnet",
"addressDescription": "Het adres dat deze client zal gebruiken voor connectiviteit",
"selectSites": "Selecteer sites",
"sitesDescription": "De client heeft connectiviteit met de geselecteerde sites",
"clientInstallOlm": "Installeer Olm",
"clientInstallOlmDescription": "Laat Olm draaien op uw systeem",
"clientOlmCredentials": "Olm inloggegevens",
"clientOlmCredentialsDescription": "Dit is hoe Olm zich bij de server zal verifiëren",
"olmEndpoint": "Olm Eindpunt",
"olmId": "Olm ID",
"olmSecretKey": "Olm Geheime Sleutel",
"clientCredentialsSave": "Uw referenties opslaan",
"clientCredentialsSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een beveiligde plek.",
"generalSettingsDescription": "Configureer de algemene instellingen voor deze client",
"clientUpdated": "Klant bijgewerkt ",
"clientUpdatedDescription": "De client is bijgewerkt.",
"clientUpdateFailed": "Het bijwerken van de client is mislukt",
"clientUpdateError": "Er is een fout opgetreden tijdens het bijwerken van de client.",
"sitesFetchFailed": "Het ophalen van sites is mislukt",
"sitesFetchError": "Er is een fout opgetreden bij het ophalen van sites.",
"olmErrorFetchReleases": "Er is een fout opgetreden bij het ophalen van Olm releases.",
"olmErrorFetchLatest": "Er is een fout opgetreden bij het ophalen van de nieuwste Olm release.",
"remoteSubnets": "Externe Subnets",
"enterCidrRange": "Voer CIDR-bereik in",
"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": "Openbare proxy inschakelen",
"resourceEnableProxyDescription": "Schakel publieke proxy in voor deze resource. Dit maakt toegang tot de resource mogelijk vanuit het netwerk via de cloud met een open poort. Vereist Traefik-configuratie.",
"externalProxyEnabled": "Externe Proxy Ingeschakeld"
}

View File

@@ -59,7 +59,6 @@
"siteErrorCreate": "Błąd podczas tworzenia witryny",
"siteErrorCreateKeyPair": "Nie znaleziono pary kluczy lub domyślnych ustawień witryny",
"siteErrorCreateDefaults": "Nie znaleziono domyślnych ustawień witryny",
"siteNameDescription": "To jest wyświetlana nazwa witryny.",
"method": "Metoda",
"siteMethodDescription": "W ten sposób ujawnisz połączenia.",
"siteLearnNewt": "Dowiedz się, jak zainstalować Newt w systemie",
@@ -834,6 +833,24 @@
"pincodeRequirementsLength": "PIN musi składać się dokładnie z 6 cyfr",
"pincodeRequirementsChars": "PIN może zawierać tylko cyfry",
"passwordRequirementsLength": "Hasło musi mieć co najmniej 1 znak",
"passwordRequirementsTitle": "Password requirements:",
"passwordRequirementLength": "At least 8 characters long",
"passwordRequirementUppercase": "At least one uppercase letter",
"passwordRequirementLowercase": "At least one lowercase letter",
"passwordRequirementNumber": "At least one number",
"passwordRequirementSpecial": "At least one special character",
"passwordRequirementsMet": "✓ Password meets all requirements",
"passwordStrength": "Password strength",
"passwordStrengthWeak": "Weak",
"passwordStrengthMedium": "Medium",
"passwordStrengthStrong": "Strong",
"passwordRequirements": "Requirements:",
"passwordRequirementLengthText": "8+ characters",
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
"passwordRequirementNumberText": "Number (0-9)",
"passwordRequirementSpecialText": "Special character (!@#$%...)",
"passwordsDoNotMatch": "Passwords do not match",
"otpEmailRequirementsLength": "Kod jednorazowy musi mieć co najmniej 1 znak",
"otpEmailSent": "Kod jednorazowy wysłany",
"otpEmailSentDescription": "Kod jednorazowy został wysłany na Twój e-mail",
@@ -968,6 +985,9 @@
"actionDeleteSite": "Usuń witrynę",
"actionGetSite": "Pobierz witrynę",
"actionListSites": "Lista witryn",
"setupToken": "Setup Token",
"setupTokenPlaceholder": "Enter the setup token from the server console",
"setupTokenRequired": "Setup token is required",
"actionUpdateSite": "Aktualizuj witrynę",
"actionListSiteRoles": "Lista dozwolonych ról witryny",
"actionCreateResource": "Utwórz zasób",
@@ -1023,6 +1043,11 @@
"actionDeleteIdpOrg": "Usuń politykę organizacji IDP",
"actionListIdpOrgs": "Lista organizacji IDP",
"actionUpdateIdpOrg": "Aktualizuj organizację IDP",
"actionCreateClient": "Utwórz klienta",
"actionDeleteClient": "Usuń klienta",
"actionUpdateClient": "Aktualizuj klienta",
"actionListClients": "Lista klientów",
"actionGetClient": "Pobierz klienta",
"noneSelected": "Nie wybrano",
"orgNotFound2": "Nie znaleziono organizacji.",
"searchProgress": "Szukaj...",
@@ -1094,7 +1119,7 @@
"sidebarAllUsers": "Wszyscy użytkownicy",
"sidebarIdentityProviders": "Dostawcy tożsamości",
"sidebarLicense": "Licencja",
"sidebarClients": "Klienci",
"sidebarClients": "Klienci (Beta)",
"sidebarDomains": "Domeny",
"enableDockerSocket": "Włącz gniazdo dokera",
"enableDockerSocketDescription": "Włącz wykrywanie Docker Socket w celu wypełnienia informacji o kontenerach. Ścieżka gniazda musi być dostarczona do Newt.",
@@ -1162,7 +1187,7 @@
"selectDomainTypeCnameName": "Pojedyncza domena (CNAME)",
"selectDomainTypeCnameDescription": "Tylko ta pojedyncza domena. Użyj tego dla poszczególnych subdomen lub wpisów specyficznych dla domeny.",
"selectDomainTypeWildcardName": "Domena wieloznaczna",
"selectDomainTypeWildcardDescription": "Ta domena i jej pierwsza warstwa subdomen.",
"selectDomainTypeWildcardDescription": "Ta domena i jej subdomeny.",
"domainDelegation": "Pojedyncza domena",
"selectType": "Wybierz typ",
"actions": "Akcje",
@@ -1196,7 +1221,7 @@
"sidebarExpand": "Rozwiń",
"newtUpdateAvailable": "Dostępna aktualizacja",
"newtUpdateAvailableInfo": "Nowa wersja Newt jest dostępna. Prosimy o aktualizację do najnowszej wersji dla najlepszej pracy.",
"domainPickerEnterDomain": "Wprowadź swoją domenę",
"domainPickerEnterDomain": "Domena",
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com lub po prostu myapp",
"domainPickerDescription": "Wpisz pełną domenę zasobu, aby zobaczyć dostępne opcje.",
"domainPickerDescriptionSaas": "Wprowadź pełną domenę, subdomenę lub po prostu nazwę, aby zobaczyć dostępne opcje",
@@ -1206,7 +1231,7 @@
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Sprawdzanie dostępności...",
"domainPickerNoMatchingDomains": "Nie znaleziono żadnych pasujących domen dla \"{userInput}\". Spróbuj innej domeny lub sprawdź ustawienia domeny swojej organizacji.",
"domainPickerNoMatchingDomains": "Nie znaleziono pasujących domen. Spróbuj innej domeny lub sprawdź ustawienia domeny swojej organizacji.",
"domainPickerOrganizationDomains": "Domeny organizacji",
"domainPickerProvidedDomains": "Dostarczone domeny",
"domainPickerSubdomain": "Subdomena: {subdomain}",
@@ -1266,6 +1291,7 @@
"createDomainName": "Nazwa:",
"createDomainValue": "Wartość:",
"createDomainCnameRecords": "Rekordy CNAME",
"createDomainARecords": "Rekordy A",
"createDomainRecordNumber": "Rekord {number}",
"createDomainTxtRecords": "Rekordy TXT",
"createDomainSaveTheseRecords": "Zapisz te rekordy",
@@ -1273,5 +1299,50 @@
"createDomainDnsPropagation": "Propagacja DNS",
"createDomainDnsPropagationDescription": "Zmiany DNS mogą zająć trochę czasu na rozpropagowanie się w Internecie. Może to potrwać od kilku minut do 48 godzin, w zależności od dostawcy DNS i ustawień TTL.",
"resourcePortRequired": "Numer portu jest wymagany dla zasobów non-HTTP",
"resourcePortNotAllowed": "Numer portu nie powinien być ustawiony dla zasobów HTTP"
}
"resourcePortNotAllowed": "Numer portu nie powinien być ustawiony dla zasobów HTTP",
"signUpTerms": {
"IAgreeToThe": "Zgadzam się z",
"termsOfService": "warunkami usługi",
"and": "oraz",
"privacyPolicy": "polityką prywatności"
},
"siteRequired": "Strona jest wymagana.",
"olmTunnel": "Tunel Olm",
"olmTunnelDescription": "Użyj Olm do łączności klienta",
"errorCreatingClient": "Błąd podczas tworzenia klienta",
"clientDefaultsNotFound": "Nie znaleziono domyślnych ustawień klienta",
"createClient": "Utwórz Klienta",
"createClientDescription": "Utwórz nowego klienta do łączenia się z Twoimi witrynami",
"seeAllClients": "Zobacz Wszystkich Klientów",
"clientInformation": "Informacje o Kliencie",
"clientNamePlaceholder": "Nazwa klienta",
"address": "Adres",
"subnetPlaceholder": "Podsieć",
"addressDescription": "Adres, którego ten klient będzie używać do łączności",
"selectSites": "Wybierz witryny",
"sitesDescription": "Klient będzie miał łączność z wybranymi witrynami",
"clientInstallOlm": "Zainstaluj Olm",
"clientInstallOlmDescription": "Uruchom Olm na swoim systemie",
"clientOlmCredentials": "Poświadczenia Olm",
"clientOlmCredentialsDescription": "To jest sposób, w jaki Olm będzie się uwierzytelniać z serwerem",
"olmEndpoint": "Punkt Końcowy Olm",
"olmId": "Identyfikator Olm",
"olmSecretKey": "Tajny Klucz Olm",
"clientCredentialsSave": "Zapisz swoje poświadczenia",
"clientCredentialsSaveDescription": "Będziesz mógł zobaczyć to tylko raz. Upewnij się, że skopiujesz go w bezpieczne miejsce.",
"generalSettingsDescription": "Skonfiguruj ogólne ustawienia dla tego klienta",
"clientUpdated": "Klient zaktualizowany",
"clientUpdatedDescription": "Klient został zaktualizowany.",
"clientUpdateFailed": "Nie udało się zaktualizować klienta",
"clientUpdateError": "Wystąpił błąd podczas aktualizacji klienta.",
"sitesFetchFailed": "Nie udało się pobrać witryn",
"sitesFetchError": "Wystąpił błąd podczas pobierania witryn.",
"olmErrorFetchReleases": "Wystąpił błąd podczas pobierania wydań Olm.",
"olmErrorFetchLatest": "Wystąpił błąd podczas pobierania najnowszego wydania Olm.",
"remoteSubnets": "Zdalne Podsieci",
"enterCidrRange": "Wprowadź zakres CIDR",
"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": "Włącz publiczny proxy",
"resourceEnableProxyDescription": "Włącz publiczne proxy dla tego zasobu. To umożliwia dostęp do zasobu spoza sieci przez chmurę na otwartym porcie. Wymaga konfiguracji Traefik.",
"externalProxyEnabled": "Zewnętrzny Proxy Włączony"
}

View File

@@ -59,7 +59,6 @@
"siteErrorCreate": "Erro ao criar site",
"siteErrorCreateKeyPair": "Par de chaves ou padrões do site não encontrados",
"siteErrorCreateDefaults": "Padrão do site não encontrado",
"siteNameDescription": "Este é o nome de exibição do site.",
"method": "Método",
"siteMethodDescription": "É assim que você irá expor as conexões.",
"siteLearnNewt": "Saiba como instalar o Newt no seu sistema",
@@ -834,6 +833,24 @@
"pincodeRequirementsLength": "O PIN deve ter exatamente 6 dígitos",
"pincodeRequirementsChars": "O PIN deve conter apenas números",
"passwordRequirementsLength": "A palavra-passe deve ter pelo menos 1 caractere",
"passwordRequirementsTitle": "Password requirements:",
"passwordRequirementLength": "At least 8 characters long",
"passwordRequirementUppercase": "At least one uppercase letter",
"passwordRequirementLowercase": "At least one lowercase letter",
"passwordRequirementNumber": "At least one number",
"passwordRequirementSpecial": "At least one special character",
"passwordRequirementsMet": "✓ Password meets all requirements",
"passwordStrength": "Password strength",
"passwordStrengthWeak": "Weak",
"passwordStrengthMedium": "Medium",
"passwordStrengthStrong": "Strong",
"passwordRequirements": "Requirements:",
"passwordRequirementLengthText": "8+ characters",
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
"passwordRequirementNumberText": "Number (0-9)",
"passwordRequirementSpecialText": "Special character (!@#$%...)",
"passwordsDoNotMatch": "Passwords do not match",
"otpEmailRequirementsLength": "O OTP deve ter pelo menos 1 caractere",
"otpEmailSent": "OTP Enviado",
"otpEmailSentDescription": "Um OTP foi enviado para o seu email",
@@ -968,6 +985,9 @@
"actionDeleteSite": "Eliminar Site",
"actionGetSite": "Obter Site",
"actionListSites": "Listar Sites",
"setupToken": "Setup Token",
"setupTokenPlaceholder": "Enter the setup token from the server console",
"setupTokenRequired": "Setup token is required",
"actionUpdateSite": "Atualizar Site",
"actionListSiteRoles": "Listar Funções Permitidas do Site",
"actionCreateResource": "Criar Recurso",
@@ -1023,6 +1043,11 @@
"actionDeleteIdpOrg": "Eliminar Política de Organização IDP",
"actionListIdpOrgs": "Listar Organizações IDP",
"actionUpdateIdpOrg": "Atualizar Organização IDP",
"actionCreateClient": "Criar Cliente",
"actionDeleteClient": "Excluir Cliente",
"actionUpdateClient": "Atualizar Cliente",
"actionListClients": "Listar Clientes",
"actionGetClient": "Obter Cliente",
"noneSelected": "Nenhum selecionado",
"orgNotFound2": "Nenhuma organização encontrada.",
"searchProgress": "Pesquisar...",
@@ -1094,7 +1119,7 @@
"sidebarAllUsers": "Todos os usuários",
"sidebarIdentityProviders": "Provedores de identidade",
"sidebarLicense": "Tipo:",
"sidebarClients": "Clientes",
"sidebarClients": "Clientes (Beta)",
"sidebarDomains": "Domínios",
"enableDockerSocket": "Habilitar Docker Socket",
"enableDockerSocketDescription": "Ativar a descoberta do Docker Socket para preencher informações do contêiner. O caminho do socket deve ser fornecido ao Newt.",
@@ -1162,7 +1187,7 @@
"selectDomainTypeCnameName": "Domínio Único (CNAME)",
"selectDomainTypeCnameDescription": "Apenas este domínio específico. Use isso para subdomínios individuais ou entradas de domínio específicas.",
"selectDomainTypeWildcardName": "Domínio Coringa",
"selectDomainTypeWildcardDescription": "Este domínio e seu primeiro nível de subdomínios.",
"selectDomainTypeWildcardDescription": "Este domínio e seus subdomínios.",
"domainDelegation": "Domínio Único",
"selectType": "Selecione um tipo",
"actions": "Ações",
@@ -1196,7 +1221,7 @@
"sidebarExpand": "Expandir",
"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": "Insira seu domínio",
"domainPickerEnterDomain": "Domínio",
"domainPickerPlaceholder": "meuapp.exemplo.com, api.v1.meudominio.com, ou apenas meuapp",
"domainPickerDescription": "Insira o domínio completo do recurso para ver as opções disponíveis.",
"domainPickerDescriptionSaas": "Insira um domínio completo, subdomínio ou apenas um nome para ver as opções disponíveis",
@@ -1206,7 +1231,7 @@
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Verificando disponibilidade...",
"domainPickerNoMatchingDomains": "Nenhum domínio correspondente encontrado para \"{userInput}\". Tente um domínio diferente ou verifique as configurações de domínio da sua organização.",
"domainPickerNoMatchingDomains": "Nenhum domínio correspondente encontrado. Tente um domínio diferente ou verifique as configurações do domínio da sua organização.",
"domainPickerOrganizationDomains": "Domínios da Organização",
"domainPickerProvidedDomains": "Domínios Fornecidos",
"domainPickerSubdomain": "Subdomínio: {subdomain}",
@@ -1266,6 +1291,7 @@
"createDomainName": "Nome:",
"createDomainValue": "Valor:",
"createDomainCnameRecords": "Registros CNAME",
"createDomainARecords": "Registros A",
"createDomainRecordNumber": "Registrar {number}",
"createDomainTxtRecords": "Registros TXT",
"createDomainSaveTheseRecords": "Salvar Esses Registros",
@@ -1273,5 +1299,50 @@
"createDomainDnsPropagation": "Propagação DNS",
"createDomainDnsPropagationDescription": "Alterações no DNS podem levar algum tempo para se propagar pela internet. Pode levar de alguns minutos a 48 horas, dependendo do seu provedor de DNS e das configurações de TTL.",
"resourcePortRequired": "Número da porta é obrigatório para recursos não-HTTP",
"resourcePortNotAllowed": "Número da porta não deve ser definido para recursos HTTP"
}
"resourcePortNotAllowed": "Número da porta não deve ser definido para recursos HTTP",
"signUpTerms": {
"IAgreeToThe": "Concordo com",
"termsOfService": "os termos de serviço",
"and": "e",
"privacyPolicy": "política de privacidade"
},
"siteRequired": "Site é obrigatório.",
"olmTunnel": "Olm Tunnel",
"olmTunnelDescription": "Use Olm para conectividade do cliente",
"errorCreatingClient": "Erro ao criar cliente",
"clientDefaultsNotFound": "Padrões do cliente não encontrados",
"createClient": "Criar Cliente",
"createClientDescription": "Crie um novo cliente para conectar aos seus sites",
"seeAllClients": "Ver Todos os Clientes",
"clientInformation": "Informações do Cliente",
"clientNamePlaceholder": "Nome do cliente",
"address": "Endereço",
"subnetPlaceholder": "Sub-rede",
"addressDescription": "O endereço que este cliente usará para conectividade",
"selectSites": "Selecionar sites",
"sitesDescription": "O cliente terá conectividade com os sites selecionados",
"clientInstallOlm": "Instalar Olm",
"clientInstallOlmDescription": "Execute o Olm em seu sistema",
"clientOlmCredentials": "Credenciais Olm",
"clientOlmCredentialsDescription": "É assim que Olm se autenticará com o servidor",
"olmEndpoint": "Endpoint Olm",
"olmId": "ID Olm",
"olmSecretKey": "Chave Secreta Olm",
"clientCredentialsSave": "Salve suas Credenciais",
"clientCredentialsSaveDescription": "Você só poderá ver isto uma vez. Certifique-se de copiá-las para um local seguro.",
"generalSettingsDescription": "Configure as configurações gerais para este cliente",
"clientUpdated": "Cliente atualizado",
"clientUpdatedDescription": "O cliente foi atualizado.",
"clientUpdateFailed": "Falha ao atualizar cliente",
"clientUpdateError": "Ocorreu um erro ao atualizar o cliente.",
"sitesFetchFailed": "Falha ao buscar sites",
"sitesFetchError": "Ocorreu um erro ao buscar sites.",
"olmErrorFetchReleases": "Ocorreu um erro ao buscar lançamentos do Olm.",
"olmErrorFetchLatest": "Ocorreu um erro ao buscar o lançamento mais recente do Olm.",
"remoteSubnets": "Sub-redes Remotas",
"enterCidrRange": "Insira o intervalo CIDR",
"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": "Ativar Proxy Público",
"resourceEnableProxyDescription": "Permite proxy público para este recurso. Isso permite o acesso ao recurso de fora da rede através da nuvem em uma porta aberta. Requer configuração do Traefik.",
"externalProxyEnabled": "Proxy Externo Habilitado"
}

View File

@@ -6,12 +6,12 @@
"setupOrgName": "Название организации",
"orgDisplayName": "Это отображаемое имя вашей организации.",
"orgId": "ID организации",
"setupIdentifierMessage": "Это уникальный идентификатор вашей организации. Он отличается от отображаемого имени.",
"setupIdentifierMessage": "Уникальный идентификатор вашей организации. Он задаётся отдельно от отображаемого имени.",
"setupErrorIdentifier": "ID организации уже занят. Выберите другой.",
"componentsErrorNoMemberCreate": "Вы пока не состоите ни в одной организации. Создайте организацию для начала работы.",
"componentsErrorNoMember": "Вы пока не состоите ни в одной организации.",
"welcome": "Welcome!",
"welcomeTo": "Welcome to",
"welcome": "Добро пожаловать!",
"welcomeTo": "Добро пожаловать в",
"componentsCreateOrg": "Создать организацию",
"componentsMember": "Вы состоите в {count, plural, =0 {0 организациях} one {# организации} few {# организациях} many {# организациях} other {# организациях}}.",
"componentsInvalidKey": "Обнаружены недействительные или просроченные лицензионные ключи. Соблюдайте условия лицензии для использования всех функций.",
@@ -59,7 +59,6 @@
"siteErrorCreate": "Ошибка при создании сайта",
"siteErrorCreateKeyPair": "Пара ключей или настройки сайта по умолчанию не найдены",
"siteErrorCreateDefaults": "Настройки сайта по умолчанию не найдены",
"siteNameDescription": "Отображаемое имя сайта.",
"method": "Метод",
"siteMethodDescription": "Это способ, которым вы будете открывать соединения.",
"siteLearnNewt": "Узнайте, как установить Newt в вашей системе",
@@ -207,7 +206,7 @@
"orgGeneralSettings": "Настройки организации",
"orgGeneralSettingsDescription": "Управляйте данными и конфигурацией вашей организации",
"saveGeneralSettings": "Сохранить общие настройки",
"saveSettings": "Save Settings",
"saveSettings": "Сохранить настройки",
"orgDangerZone": "Опасная зона",
"orgDangerZoneDescription": "Будьте осторожны: удалив организацию, вы не сможете восстановить её.",
"orgDelete": "Удалить организацию",
@@ -646,53 +645,53 @@
"resourcePincodeProtection": "Защита PIN-кодом {status}",
"resourcePincodeRemove": "PIN-код ресурса удалён",
"resourcePincodeRemoveDescription": "PIN-код ресурса был успешно удалён",
"resourcePincodeSetup": "Resource PIN code set",
"resourcePincodeSetupDescription": "The resource pincode has been set successfully",
"resourcePincodeSetupTitle": "Set Pincode",
"resourcePincodeSetupTitleDescription": "Set a pincode to protect this resource",
"resourceRoleDescription": "Admins can always access this resource.",
"resourceUsersRoles": "Users & Roles",
"resourceUsersRolesDescription": "Configure which users and roles can visit this resource",
"resourceUsersRolesSubmit": "Save Users & Roles",
"resourceWhitelistSave": "Saved successfully",
"resourceWhitelistSaveDescription": "Whitelist settings have been saved",
"ssoUse": "Use Platform SSO",
"resourcePincodeSetup": "PIN-код ресурса установлен",
"resourcePincodeSetupDescription": "PIN-код ресурса был успешно установлен",
"resourcePincodeSetupTitle": "Установить PIN-код",
"resourcePincodeSetupTitleDescription": "Установите PIN-код для защиты этого ресурса",
"resourceRoleDescription": "Администраторы всегда имеют доступ к этому ресурсу.",
"resourceUsersRoles": "Пользователи и роли",
"resourceUsersRolesDescription": "Выберите пользователей и роли с доступом к этому ресурсу",
"resourceUsersRolesSubmit": "Сохранить пользователей и роли",
"resourceWhitelistSave": "Успешно сохранено",
"resourceWhitelistSaveDescription": "Настройки белого списка были сохранены",
"ssoUse": "Использовать Platform SSO",
"ssoUseDescription": "Существующим пользователям нужно будет войти только один раз для всех ресурсов с включенной этой опцией.",
"proxyErrorInvalidPort": "Invalid port number",
"subdomainErrorInvalid": "Invalid subdomain",
"domainErrorFetch": "Error fetching domains",
"domainErrorFetchDescription": "An error occurred when fetching the domains",
"resourceErrorUpdate": "Failed to update resource",
"resourceErrorUpdateDescription": "An error occurred while updating the resource",
"resourceUpdated": "Resource updated",
"resourceUpdatedDescription": "The resource has been updated successfully",
"resourceErrorTransfer": "Failed to transfer resource",
"resourceErrorTransferDescription": "An error occurred while transferring the resource",
"resourceTransferred": "Resource transferred",
"resourceTransferredDescription": "The resource has been transferred successfully",
"resourceErrorToggle": "Failed to toggle resource",
"resourceErrorToggleDescription": "An error occurred while updating the resource",
"resourceVisibilityTitle": "Visibility",
"resourceVisibilityTitleDescription": "Completely enable or disable resource visibility",
"resourceGeneral": "General Settings",
"resourceGeneralDescription": "Configure the general settings for this resource",
"resourceEnable": "Enable Resource",
"resourceTransfer": "Transfer Resource",
"resourceTransferDescription": "Transfer this resource to a different site",
"resourceTransferSubmit": "Transfer Resource",
"siteDestination": "Destination Site",
"searchSites": "Search sites",
"accessRoleCreate": "Create Role",
"accessRoleCreateDescription": "Create a new role to group users and manage their permissions.",
"accessRoleCreateSubmit": "Create Role",
"accessRoleCreated": "Role created",
"accessRoleCreatedDescription": "The role has been successfully created.",
"accessRoleErrorCreate": "Failed to create role",
"accessRoleErrorCreateDescription": "An error occurred while creating the role.",
"accessRoleErrorNewRequired": "New role is required",
"accessRoleErrorRemove": "Failed to remove role",
"accessRoleErrorRemoveDescription": "An error occurred while removing the role.",
"accessRoleName": "Role Name",
"proxyErrorInvalidPort": "Неверный номер порта",
"subdomainErrorInvalid": "Неверный поддомен",
"domainErrorFetch": "Ошибка при получении доменов",
"domainErrorFetchDescription": "Произошла ошибка при получении доменов",
"resourceErrorUpdate": "Не удалось обновить ресурс",
"resourceErrorUpdateDescription": "Произошла ошибка при обновлении ресурса",
"resourceUpdated": "Ресурс обновлён",
"resourceUpdatedDescription": "Ресурс был успешно обновлён",
"resourceErrorTransfer": "Не удалось перенести ресурс",
"resourceErrorTransferDescription": "Произошла ошибка при переносе ресурса",
"resourceTransferred": "Ресурс перенесён",
"resourceTransferredDescription": "Ресурс был успешно перенесён",
"resourceErrorToggle": "Не удалось переключить ресурс",
"resourceErrorToggleDescription": "Произошла ошибка при обновлении ресурса",
"resourceVisibilityTitle": "Видимость",
"resourceVisibilityTitleDescription": "Включите или отключите видимость ресурса",
"resourceGeneral": "Общие настройки",
"resourceGeneralDescription": "Настройте общие параметры этого ресурса",
"resourceEnable": "Ресурс активен",
"resourceTransfer": "Перенести ресурс",
"resourceTransferDescription": "Перенесите этот ресурс на другой сайт",
"resourceTransferSubmit": "Перенести ресурс",
"siteDestination": "Новый сайт для ресурса",
"searchSites": "Поиск сайтов",
"accessRoleCreate": "Создание роли",
"accessRoleCreateDescription": "Создайте новую роль для группы пользователей и выдавайте им разрешения.",
"accessRoleCreateSubmit": "Создать роль",
"accessRoleCreated": "Роль создана",
"accessRoleCreatedDescription": "Роль была успешно создана.",
"accessRoleErrorCreate": "Не удалось создать роль",
"accessRoleErrorCreateDescription": "Произошла ошибка при создании роли.",
"accessRoleErrorNewRequired": "Новая роль обязательна",
"accessRoleErrorRemove": "Не удалось удалить роль",
"accessRoleErrorRemoveDescription": "Произошла ошибка при удалении роли.",
"accessRoleName": "Название роли",
"accessRoleQuestionRemove": "Вы собираетесь удалить роль {name}. Это действие нельзя отменить.",
"accessRoleRemove": "Удалить роль",
"accessRoleRemoveDescription": "Удалить роль из организации",
@@ -726,86 +725,86 @@
"idpSearch": "Поиск поставщиков удостоверений...",
"idpAdd": "Добавить поставщика удостоверений",
"idpClientIdRequired": "ID клиента обязателен.",
"idpClientSecretRequired": "Client Secret is required.",
"idpErrorAuthUrlInvalid": "Auth URL must be a valid URL.",
"idpErrorTokenUrlInvalid": "Token URL must be a valid URL.",
"idpPathRequired": "Identifier Path is required.",
"idpScopeRequired": "Scopes are required.",
"idpOidcDescription": "Configure an OpenID Connect identity provider",
"idpCreatedDescription": "Identity provider created successfully",
"idpCreate": "Create Identity Provider",
"idpCreateDescription": "Configure a new identity provider for user authentication",
"idpSeeAll": "See All Identity Providers",
"idpSettingsDescription": "Configure the basic information for your identity provider",
"idpDisplayName": "A display name for this identity provider",
"idpAutoProvisionUsers": "Auto Provision Users",
"idpClientSecretRequired": "Требуется секретный пароль клиента.",
"idpErrorAuthUrlInvalid": "URL авторизации должен быть корректным URL.",
"idpErrorTokenUrlInvalid": "URL токена должен быть корректным URL.",
"idpPathRequired": "Путь идентификатора обязателен.",
"idpScopeRequired": "Области действия обязательны.",
"idpOidcDescription": "Настройте поставщика удостоверений OpenID Connect",
"idpCreatedDescription": "Поставщик удостоверений успешно создан",
"idpCreate": "Создать поставщика удостоверений",
"idpCreateDescription": "Настройте нового поставщика удостоверений для аутентификации пользователей",
"idpSeeAll": "Посмотреть всех поставщиков удостоверений",
"idpSettingsDescription": "Настройте базовую информацию для вашего поставщика удостоверений",
"idpDisplayName": "Отображаемое имя для этого поставщика удостоверений",
"idpAutoProvisionUsers": "Автоматическое создание пользователей",
"idpAutoProvisionUsersDescription": "При включении пользователи будут автоматически создаваться в системе при первом входе с возможностью сопоставления пользователей с ролями и организациями.",
"licenseBadge": "Professional",
"idpType": "Provider Type",
"idpTypeDescription": "Select the type of identity provider you want to configure",
"idpOidcConfigure": "OAuth2/OIDC Configuration",
"licenseBadge": "Профессиональная",
"idpType": "Тип поставщика",
"idpTypeDescription": "Выберите тип поставщика удостоверений, который вы хотите настроить",
"idpOidcConfigure": "Конфигурация OAuth2/OIDC",
"idpOidcConfigureDescription": "Настройте конечные точки и учётные данные поставщика OAuth2/OIDC",
"idpClientId": "Client ID",
"idpClientIdDescription": "The OAuth2 client ID from your identity provider",
"idpClientSecret": "Client Secret",
"idpClientSecretDescription": "The OAuth2 client secret from your identity provider",
"idpAuthUrl": "Authorization URL",
"idpAuthUrlDescription": "The OAuth2 authorization endpoint URL",
"idpTokenUrl": "Token URL",
"idpTokenUrlDescription": "The OAuth2 token endpoint URL",
"idpOidcConfigureAlert": "Important Information",
"idpClientId": "ID клиента",
"idpClientIdDescription": "OAuth2 ID клиента от вашего поставщика удостоверений",
"idpClientSecret": "Секрет клиента",
"idpClientSecretDescription": "OAuth2 секрет клиента от вашего поставщика удостоверений",
"idpAuthUrl": "URL авторизации",
"idpAuthUrlDescription": "URL конечной точки авторизации OAuth2",
"idpTokenUrl": "URL токена",
"idpTokenUrlDescription": "URL конечной точки токена OAuth2",
"idpOidcConfigureAlert": "Важная информация",
"idpOidcConfigureAlertDescription": "После создания поставщика удостоверений вам нужно будет настроить URL обратного вызова в настройках вашего поставщика удостоверений. URL обратного вызова будет предоставлен после успешного создания.",
"idpToken": "Token Configuration",
"idpTokenDescription": "Configure how to extract user information from the ID token",
"idpJmespathAbout": "About JMESPath",
"idpJmespathAboutDescription": "The paths below use JMESPath syntax to extract values from the ID token.",
"idpJmespathAboutDescriptionLink": "Learn more about JMESPath",
"idpJmespathLabel": "Identifier Path",
"idpJmespathLabelDescription": "The path to the user identifier in the ID token",
"idpJmespathEmailPathOptional": "Email Path (Optional)",
"idpJmespathEmailPathOptionalDescription": "The path to the user's email in the ID token",
"idpJmespathNamePathOptional": "Name Path (Optional)",
"idpJmespathNamePathOptionalDescription": "The path to the user's name in the ID token",
"idpOidcConfigureScopes": "Scopes",
"idpOidcConfigureScopesDescription": "Space-separated list of OAuth2 scopes to request",
"idpSubmit": "Create Identity Provider",
"orgPolicies": "Organization Policies",
"idpToken": "Конфигурация токена",
"idpTokenDescription": "Настройте, как извлекать информацию о пользователе из ID токена",
"idpJmespathAbout": "О JMESPath",
"idpJmespathAboutDescription": "Пути ниже используют синтаксис JMESPath для извлечения значений из ID токена.",
"idpJmespathAboutDescriptionLink": "Узнать больше о JMESPath",
"idpJmespathLabel": "Путь идентификатора",
"idpJmespathLabelDescription": "Путь к идентификатору пользователя в ID токене",
"idpJmespathEmailPathOptional": "Путь к email (необязательно)",
"idpJmespathEmailPathOptionalDescription": "Путь к email пользователя в ID токене",
"idpJmespathNamePathOptional": "Путь к имени (необязательно)",
"idpJmespathNamePathOptionalDescription": "Путь к имени пользователя в ID токене",
"idpOidcConfigureScopes": "Области действия",
"idpOidcConfigureScopesDescription": "Список областей OAuth2, разделённых пробелами",
"idpSubmit": "Создать поставщика удостоверений",
"orgPolicies": "Политики организации",
"idpSettings": "Настройки {idpName}",
"idpCreateSettingsDescription": "Configure the settings for your identity provider",
"roleMapping": "Role Mapping",
"orgMapping": "Organization Mapping",
"orgPoliciesSearch": "Search organization policies...",
"orgPoliciesAdd": "Add Organization Policy",
"orgRequired": "Organization is required",
"error": "Error",
"success": "Success",
"orgPolicyAddedDescription": "Policy added successfully",
"orgPolicyUpdatedDescription": "Policy updated successfully",
"orgPolicyDeletedDescription": "Policy deleted successfully",
"defaultMappingsUpdatedDescription": "Default mappings updated successfully",
"orgPoliciesAbout": "About Organization Policies",
"idpCreateSettingsDescription": "Настройте параметры для вашего поставщика удостоверений",
"roleMapping": "Сопоставление ролей",
"orgMapping": "Сопоставление организаций",
"orgPoliciesSearch": "Поиск политик организации...",
"orgPoliciesAdd": "Добавить политику организации",
"orgRequired": "Организация обязательна",
"error": "Ошибка",
"success": "Успешно",
"orgPolicyAddedDescription": "Политика успешно добавлена",
"orgPolicyUpdatedDescription": "Политика успешно обновлена",
"orgPolicyDeletedDescription": "Политика успешно удалена",
"defaultMappingsUpdatedDescription": "Сопоставления по умолчанию успешно обновлены",
"orgPoliciesAbout": "О политиках организации",
"orgPoliciesAboutDescription": "Политики организации используются для контроля доступа к организациям на основе ID токена пользователя. Вы можете указать выражения JMESPath для извлечения информации о роли и организации из ID токена.",
"orgPoliciesAboutDescriptionLink": "See documentation, for more information.",
"defaultMappingsOptional": "Default Mappings (Optional)",
"orgPoliciesAboutDescriptionLink": "См. документацию для получения дополнительной информации.",
"defaultMappingsOptional": "Сопоставления по умолчанию (необязательно)",
"defaultMappingsOptionalDescription": "Сопоставления по умолчанию используются, когда для организации не определена политика организации. Здесь вы можете указать сопоставления ролей и организаций по умолчанию.",
"defaultMappingsRole": "Default Role Mapping",
"defaultMappingsRole": "Сопоставление ролей по умолчанию",
"defaultMappingsRoleDescription": "Результат этого выражения должен возвращать имя роли, как определено в организации, в виде строки.",
"defaultMappingsOrg": "Default Organization Mapping",
"defaultMappingsOrg": "Сопоставление организаций по умолчанию",
"defaultMappingsOrgDescription": "Это выражение должно возвращать ID организации или true для разрешения доступа пользователя к организации.",
"defaultMappingsSubmit": "Save Default Mappings",
"orgPoliciesEdit": "Edit Organization Policy",
"org": "Organization",
"orgSelect": "Select organization",
"orgSearch": "Search org",
"orgNotFound": "No org found.",
"roleMappingPathOptional": "Role Mapping Path (Optional)",
"orgMappingPathOptional": "Organization Mapping Path (Optional)",
"orgPolicyUpdate": "Update Policy",
"orgPolicyAdd": "Add Policy",
"orgPolicyConfig": "Configure access for an organization",
"idpUpdatedDescription": "Identity provider updated successfully",
"redirectUrl": "Redirect URL",
"redirectUrlAbout": "About Redirect URL",
"defaultMappingsSubmit": "Сохранить сопоставления по умолчанию",
"orgPoliciesEdit": "Редактировать политику организации",
"org": "Организация",
"orgSelect": "Выберите организацию",
"orgSearch": "Поиск организации",
"orgNotFound": "Организация не найдена.",
"roleMappingPathOptional": "Путь сопоставления ролей (необязательно)",
"orgMappingPathOptional": "Путь сопоставления организаций (необязательно)",
"orgPolicyUpdate": "Обновить политику",
"orgPolicyAdd": "Добавить политику",
"orgPolicyConfig": "Настроить доступ для организации",
"idpUpdatedDescription": "Поставщик удостоверений успешно обновлён",
"redirectUrl": "URL редиректа",
"redirectUrlAbout": "О редиректе URL",
"redirectUrlAboutDescription": "Это URL, на который пользователи будут перенаправлены после аутентификации. Вам нужно настроить этот URL в настройках вашего поставщика удостоверений.",
"pangolinAuth": "Аутентификация - Pangolin",
"verificationCodeLengthRequirements": "Ваш код подтверждения должен состоять из 8 символов.",
@@ -834,6 +833,24 @@
"pincodeRequirementsLength": "PIN должен состоять ровно из 6 цифр",
"pincodeRequirementsChars": "PIN должен содержать только цифры",
"passwordRequirementsLength": "Пароль должен быть не менее 1 символа",
"passwordRequirementsTitle": "Password requirements:",
"passwordRequirementLength": "At least 8 characters long",
"passwordRequirementUppercase": "At least one uppercase letter",
"passwordRequirementLowercase": "At least one lowercase letter",
"passwordRequirementNumber": "At least one number",
"passwordRequirementSpecial": "At least one special character",
"passwordRequirementsMet": "✓ Password meets all requirements",
"passwordStrength": "Password strength",
"passwordStrengthWeak": "Weak",
"passwordStrengthMedium": "Medium",
"passwordStrengthStrong": "Strong",
"passwordRequirements": "Requirements:",
"passwordRequirementLengthText": "8+ characters",
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
"passwordRequirementNumberText": "Number (0-9)",
"passwordRequirementSpecialText": "Special character (!@#$%...)",
"passwordsDoNotMatch": "Passwords do not match",
"otpEmailRequirementsLength": "OTP должен быть не менее 1 символа",
"otpEmailSent": "OTP отправлен",
"otpEmailSentDescription": "OTP был отправлен на ваш email",
@@ -859,162 +876,165 @@
"accessTokenError": "Ошибка проверки токена доступа",
"accessGranted": "Доступ предоставлен",
"accessUrlInvalid": "Неверный URL доступа",
"accessGrantedDescription": "You have been granted access to this resource. Redirecting you...",
"accessUrlInvalidDescription": "This shared access URL is invalid. Please contact the resource owner for a new URL.",
"tokenInvalid": "Invalid token",
"pincodeInvalid": "Invalid code",
"passwordErrorRequestReset": "Failed to request reset:",
"passwordErrorReset": "Failed to reset password:",
"passwordResetSuccess": "Password reset successfully! Back to log in...",
"passwordReset": "Reset Password",
"passwordResetDescription": "Follow the steps to reset your password",
"passwordResetSent": "We'll send a password reset code to this email address.",
"passwordResetCode": "Reset Code",
"passwordResetCodeDescription": "Check your email for the reset code.",
"passwordNew": "New Password",
"passwordNewConfirm": "Confirm New Password",
"pincodeAuth": "Authenticator Code",
"pincodeSubmit2": "Submit Code",
"passwordResetSubmit": "Request Reset",
"passwordBack": "Back to Password",
"loginBack": "Go back to log in",
"signup": "Sign up",
"loginStart": "Log in to get started",
"idpOidcTokenValidating": "Validating OIDC token",
"idpOidcTokenResponse": "Validate OIDC token response",
"idpErrorOidcTokenValidating": "Error validating OIDC token",
"accessGrantedDescription": "Вам был предоставлен доступ к этому ресурсу. Перенаправляем вас...",
"accessUrlInvalidDescription": "Этот общий URL доступа недействителен. Пожалуйста, свяжитесь с владельцем ресурса для получения нового URL.",
"tokenInvalid": "Неверный токен",
"pincodeInvalid": "Неверный код",
"passwordErrorRequestReset": "Не удалось запросить сброс:",
"passwordErrorReset": "Не удалось сбросить пароль:",
"passwordResetSuccess": "Пароль успешно сброшен! Вернуться к входу...",
"passwordReset": "Сброс пароля",
"passwordResetDescription": "Следуйте инструкциям для сброса вашего пароля",
"passwordResetSent": "Мы отправим код сброса пароля на этот email адрес.",
"passwordResetCode": "Код сброса пароля",
"passwordResetCodeDescription": "Проверьте вашу почту для получения кода сброса пароля.",
"passwordNew": "Новый пароль",
"passwordNewConfirm": "Подтвердите новый пароль",
"pincodeAuth": "Код аутентификатора",
"pincodeSubmit2": "Отправить код",
"passwordResetSubmit": "Запросить сброс",
"passwordBack": "Назад к паролю",
"loginBack": "Вернуться к входу",
"signup": "Регистрация",
"loginStart": "Войдите для начала работы",
"idpOidcTokenValidating": "Проверка OIDC токена",
"idpOidcTokenResponse": "Проверить ответ OIDC токена",
"idpErrorOidcTokenValidating": "Ошибка проверки OIDC токена",
"idpConnectingTo": "Подключение к {name}",
"idpConnectingToDescription": "Validating your identity",
"idpConnectingToProcess": "Connecting...",
"idpConnectingToFinished": "Connected",
"idpConnectingToDescription": "Проверка вашей личности",
"idpConnectingToProcess": "Подключение...",
"idpConnectingToFinished": "Подключено",
"idpErrorConnectingTo": "Возникла проблема при подключении к {name}. Пожалуйста, свяжитесь с вашим администратором.",
"idpErrorNotFound": "IdP not found",
"inviteInvalid": "Invalid Invite",
"inviteInvalidDescription": "The invite link is invalid.",
"inviteErrorWrongUser": "Invite is not for this user",
"inviteErrorUserNotExists": "User does not exist. Please create an account first.",
"inviteErrorLoginRequired": "You must be logged in to accept an invite",
"inviteErrorExpired": "The invite may have expired",
"inviteErrorRevoked": "The invite might have been revoked",
"inviteErrorTypo": "There could be a typo in the invite link",
"pangolinSetup": "Setup - Pangolin",
"orgNameRequired": "Organization name is required",
"orgIdRequired": "Organization ID is required",
"orgErrorCreate": "An error occurred while creating org",
"pageNotFound": "Page Not Found",
"pageNotFoundDescription": "Oops! The page you're looking for doesn't exist.",
"overview": "Overview",
"home": "Home",
"accessControl": "Access Control",
"settings": "Settings",
"usersAll": "All Users",
"license": "License",
"pangolinDashboard": "Dashboard - Pangolin",
"noResults": "No results found.",
"idpErrorNotFound": "IdP не найден",
"inviteInvalid": "Недействительное приглашение",
"inviteInvalidDescription": "Ссылка на приглашение недействительна.",
"inviteErrorWrongUser": "Приглашение не для этого пользователя",
"inviteErrorUserNotExists": "Пользователь не существует. Пожалуйста, сначала создайте учетную запись.",
"inviteErrorLoginRequired": "Вы должны войти, чтобы принять приглашение",
"inviteErrorExpired": "Срок действия приглашения истек",
"inviteErrorRevoked": "Возможно, приглашение было отозвано",
"inviteErrorTypo": "В пригласительной ссылке может быть опечатка",
"pangolinSetup": "Настройка - Pangolin",
"orgNameRequired": "Название организации обязательно",
"orgIdRequired": "ID организации обязателен",
"orgErrorCreate": "Произошла ошибка при создании организации",
"pageNotFound": "Страница не найдена",
"pageNotFoundDescription": "Упс! Страница, которую вы ищете, не существует.",
"overview": "Обзор",
"home": "Главная",
"accessControl": "Контроль доступа",
"settings": "Настройки",
"usersAll": "Все пользователи",
"license": "Лицензия",
"pangolinDashboard": "Дашборд - Pangolin",
"noResults": "Результаты не найдены.",
"terabytes": "{count} ТБ",
"gigabytes": "{count} ГБ",
"megabytes": "{count} МБ",
"tagsEntered": "Entered Tags",
"tagsEnteredDescription": "These are the tags you`ve entered.",
"tagsWarnCannotBeLessThanZero": "maxTags and minTags cannot be less than 0",
"tagsWarnNotAllowedAutocompleteOptions": "Tag not allowed as per autocomplete options",
"tagsWarnInvalid": "Invalid tag as per validateTag",
"tagWarnTooShort": "Tag {tagText} is too short",
"tagWarnTooLong": "Tag {tagText} is too long",
"tagsWarnReachedMaxNumber": "Reached the maximum number of tags allowed",
"tagWarnDuplicate": "Duplicate tag {tagText} not added",
"supportKeyInvalid": "Invalid Key",
"supportKeyInvalidDescription": "Your supporter key is invalid.",
"supportKeyValid": "Valid Key",
"supportKeyValidDescription": "Your supporter key has been validated. Thank you for your support!",
"supportKeyErrorValidationDescription": "Failed to validate supporter key.",
"supportKey": "Support Development and Adopt a Pangolin!",
"tagsEntered": "Введённые теги",
"tagsEnteredDescription": "Это теги, которые вы ввели.",
"tagsWarnCannotBeLessThanZero": "maxTags и minTags не могут быть меньше 0",
"tagsWarnNotAllowedAutocompleteOptions": "Тег не разрешён согласно опциям автозаполнения",
"tagsWarnInvalid": "Недействительный тег согласно validateTag",
"tagWarnTooShort": "Тег {tagText} слишком короткий",
"tagWarnTooLong": "Тег {tagText} слишком длинный",
"tagsWarnReachedMaxNumber": "Достигнуто максимальное количество разрешённых тегов",
"tagWarnDuplicate": "Дублирующий тег {tagText} не добавлен",
"supportKeyInvalid": "Недействительный ключ",
"supportKeyInvalidDescription": "Ваш ключ поддержки недействителен.",
"supportKeyValid": "Действительный ключ",
"supportKeyValidDescription": "Ваш ключ поддержки был проверен. Спасибо за поддержку!",
"supportKeyErrorValidationDescription": "Не удалось проверить ключ поддержки.",
"supportKey": "Поддержите разработку и усыновите Панголина!",
"supportKeyDescription": "Приобретите ключ поддержки, чтобы помочь нам продолжать разработку Pangolin для сообщества. Ваш вклад позволяет нам уделять больше времени поддержке и добавлению новых функций в приложение для всех. Мы никогда не будем использовать это для платного доступа к функциям. Это отдельно от любой коммерческой версии.",
"supportKeyPet": "You will also get to adopt and meet your very own pet Pangolin!",
"supportKeyPurchase": "Payments are processed via GitHub. Afterward, you can retrieve your key on",
"supportKeyPurchaseLink": "our website",
"supportKeyPurchase2": "and redeem it here.",
"supportKeyLearnMore": "Learn more.",
"supportKeyOptions": "Please select the option that best suits you.",
"supportKetOptionFull": "Full Supporter",
"forWholeServer": "For the whole server",
"lifetimePurchase": "Lifetime purchase",
"supporterStatus": "Supporter status",
"buy": "Buy",
"supportKeyOptionLimited": "Limited Supporter",
"forFiveUsers": "For 5 or less users",
"supportKeyRedeem": "Redeem Supporter Key",
"supportKeyHideSevenDays": "Hide for 7 days",
"supportKeyEnter": "Enter Supporter Key",
"supportKeyEnterDescription": "Meet your very own pet Pangolin!",
"githubUsername": "GitHub Username",
"supportKeyInput": "Supporter Key",
"supportKeyBuy": "Buy Supporter Key",
"logoutError": "Error logging out",
"signingAs": "Signed in as",
"serverAdmin": "Server Admin",
"otpEnable": "Enable Two-factor",
"otpDisable": "Disable Two-factor",
"logout": "Log Out",
"licenseTierProfessionalRequired": "Professional Edition Required",
"supportKeyPet": "Вы также сможете усыновить и встретить вашего собственного питомца Панголина!",
"supportKeyPurchase": "Платежи обрабатываются через GitHub. После этого вы сможете получить свой ключ на",
"supportKeyPurchaseLink": "нашем сайте",
"supportKeyPurchase2": "и активировать его здесь.",
"supportKeyLearnMore": "Узнать больше.",
"supportKeyOptions": "Пожалуйста, выберите подходящий вам вариант.",
"supportKetOptionFull": "Полная поддержка",
"forWholeServer": "За весь сервер",
"lifetimePurchase": "Пожизненная покупка",
"supporterStatus": "Статус поддержки",
"buy": "Купить",
"supportKeyOptionLimited": "Лимитированная поддержка",
"forFiveUsers": "За 5 или меньше пользователей",
"supportKeyRedeem": "Использовать ключ Поддержки",
"supportKeyHideSevenDays": "Скрыть на 7 дней",
"supportKeyEnter": "Введите ключ поддержки",
"supportKeyEnterDescription": "Встречайте своего питомца Панголина!",
"githubUsername": "Имя пользователя Github",
"supportKeyInput": "Ключ поддержки",
"supportKeyBuy": "Ключ поддержки",
"logoutError": "Ошибка при выходе",
"signingAs": "Вы вошли как",
"serverAdmin": "Администратор сервера",
"otpEnable": "Включить Двухфакторную Аутентификацию",
"otpDisable": "Отключить двухфакторную аутентификацию",
"logout": "Выйти",
"licenseTierProfessionalRequired": "Требуется профессиональная версия",
"licenseTierProfessionalRequiredDescription": "Эта функция доступна только в профессиональной версии.",
"actionGetOrg": "Get Organization",
"actionUpdateOrg": "Update Organization",
"actionUpdateUser": "Update User",
"actionGetUser": "Get User",
"actionGetOrgUser": "Get Organization User",
"actionListOrgDomains": "List Organization Domains",
"actionCreateSite": "Create Site",
"actionDeleteSite": "Delete Site",
"actionGetSite": "Get Site",
"actionListSites": "List Sites",
"actionUpdateSite": "Update Site",
"actionListSiteRoles": "List Allowed Site Roles",
"actionCreateResource": "Create Resource",
"actionDeleteResource": "Delete Resource",
"actionGetResource": "Get Resource",
"actionListResource": "List Resources",
"actionUpdateResource": "Update Resource",
"actionListResourceUsers": "List Resource Users",
"actionSetResourceUsers": "Set Resource Users",
"actionSetAllowedResourceRoles": "Set Allowed Resource Roles",
"actionListAllowedResourceRoles": "List Allowed Resource Roles",
"actionSetResourcePassword": "Set Resource Password",
"actionSetResourcePincode": "Set Resource Pincode",
"actionGetOrg": "Получить организацию",
"actionUpdateOrg": "Обновить организацию",
"actionUpdateUser": "Обновить пользователя",
"actionGetUser": "Получить пользователя",
"actionGetOrgUser": "Получить пользователя организации",
"actionListOrgDomains": "Список доменов организации",
"actionCreateSite": "Создать сайт",
"actionDeleteSite": "Удалить сайт",
"actionGetSite": "Получить сайт",
"actionListSites": "Список сайтов",
"setupToken": "Setup Token",
"setupTokenPlaceholder": "Enter the setup token from the server console",
"setupTokenRequired": "Setup token is required",
"actionUpdateSite": "Обновить сайт",
"actionListSiteRoles": "Список разрешенных ролей сайта",
"actionCreateResource": "Создать ресурс",
"actionDeleteResource": "Удалить ресурс",
"actionGetResource": "Получить ресурсы",
"actionListResource": "Список ресурсов",
"actionUpdateResource": "Обновить ресурс",
"actionListResourceUsers": "Список пользователей ресурсов",
"actionSetResourceUsers": "Список пользователей ресурсов",
"actionSetAllowedResourceRoles": "Набор разрешенных ролей ресурсов",
"actionListAllowedResourceRoles": "Список разрешенных ролей сайта",
"actionSetResourcePassword": "Задать пароль ресурса",
"actionSetResourcePincode": "Установить ПИН-код ресурса",
"actionSetResourceEmailWhitelist": "Set Resource Email Whitelist",
"actionGetResourceEmailWhitelist": "Get Resource Email Whitelist",
"actionCreateTarget": "Create Target",
"actionDeleteTarget": "Delete Target",
"actionGetTarget": "Get Target",
"actionListTargets": "List Targets",
"actionUpdateTarget": "Update Target",
"actionCreateRole": "Create Role",
"actionDeleteRole": "Delete Role",
"actionGetRole": "Get Role",
"actionListRole": "List Roles",
"actionUpdateRole": "Update Role",
"actionListAllowedRoleResources": "List Allowed Role Resources",
"actionInviteUser": "Invite User",
"actionRemoveUser": "Remove User",
"actionListUsers": "List Users",
"actionAddUserRole": "Add User Role",
"actionGenerateAccessToken": "Generate Access Token",
"actionDeleteAccessToken": "Delete Access Token",
"actionListAccessTokens": "List Access Tokens",
"actionCreateResourceRule": "Create Resource Rule",
"actionDeleteResourceRule": "Delete Resource Rule",
"actionListResourceRules": "List Resource Rules",
"actionUpdateResourceRule": "Update Resource Rule",
"actionListOrgs": "List Organizations",
"actionCheckOrgId": "Check ID",
"actionCreateOrg": "Create Organization",
"actionDeleteOrg": "Delete Organization",
"actionListApiKeys": "List API Keys",
"actionListApiKeyActions": "List API Key Actions",
"actionSetApiKeyActions": "Set API Key Allowed Actions",
"actionCreateApiKey": "Create API Key",
"actionDeleteApiKey": "Delete API Key",
"actionCreateIdp": "Create IDP",
"actionCreateTarget": "Создать цель",
"actionDeleteTarget": "Удалить цель",
"actionGetTarget": "Получить цель",
"actionListTargets": "Список целей",
"actionUpdateTarget": "Обновить цель",
"actionCreateRole": "Создать роль",
"actionDeleteRole": "Удалить роль",
"actionGetRole": "Получить Роль",
"actionListRole": "Список ролей",
"actionUpdateRole": "Обновить роль",
"actionListAllowedRoleResources": "Список разрешенных ролей сайта",
"actionInviteUser": "Пригласить пользователя",
"actionRemoveUser": "Удалить пользователя",
"actionListUsers": "Список пользователей",
"actionAddUserRole": "Добавить роль пользователя",
"actionGenerateAccessToken": "Сгенерировать токен доступа",
"actionDeleteAccessToken": "Удалить токен доступа",
"actionListAccessTokens": "Список токенов доступа",
"actionCreateResourceRule": "Создать правило ресурса",
"actionDeleteResourceRule": "Удалить правило ресурса",
"actionListResourceRules": "Список правил ресурса",
"actionUpdateResourceRule": "Обновить правило ресурса",
"actionListOrgs": "Список организаций",
"actionCheckOrgId": "Проверить ID",
"actionCreateOrg": "Создать организацию",
"actionDeleteOrg": "Удалить организацию",
"actionListApiKeys": "Список API ключей",
"actionListApiKeyActions": "Список действий API ключа",
"actionSetApiKeyActions": "Установить разрешённые действия API ключа",
"actionCreateApiKey": "Создать API ключ",
"actionDeleteApiKey": "Удалить API ключ",
"actionCreateIdp": "Создать IDP",
"actionUpdateIdp": "Обновить IDP",
"actionDeleteIdp": "Удалить IDP",
"actionListIdps": "Список IDP",
@@ -1023,6 +1043,11 @@
"actionDeleteIdpOrg": "Удалить политику IDP организации",
"actionListIdpOrgs": "Список организаций IDP",
"actionUpdateIdpOrg": "Обновить организацию IDP",
"actionCreateClient": "Создать Клиента",
"actionDeleteClient": "Удалить Клиента",
"actionUpdateClient": "Обновить Клиента",
"actionListClients": "Список Клиентов",
"actionGetClient": "Получить Клиента",
"noneSelected": "Ничего не выбрано",
"orgNotFound2": "Организации не найдены.",
"searchProgress": "Поиск...",
@@ -1053,19 +1078,19 @@
"otpErrorDisableDescription": "Произошла ошибка при отключении 2FA",
"otpRemove": "Отключить двухфакторную аутентификацию",
"otpRemoveDescription": "Отключить двухфакторную аутентификацию для вашей учётной записи",
"otpRemoveSuccess": "Two-Factor Authentication Disabled",
"otpRemoveSuccess": "Двухфакторная аутентификация отключена",
"otpRemoveSuccessMessage": "Двухфакторная аутентификация была отключена для вашей учётной записи. Вы можете включить её снова в любое время.",
"otpRemoveSubmit": "Disable 2FA",
"paginator": "Page {current} of {last}",
"paginatorToFirst": "Go to first page",
"paginatorToPrevious": "Go to previous page",
"paginatorToNext": "Go to next page",
"paginatorToLast": "Go to last page",
"copyText": "Copy text",
"copyTextFailed": "Failed to copy text: ",
"copyTextClipboard": "Copy to clipboard",
"inviteErrorInvalidConfirmation": "Invalid confirmation",
"passwordRequired": "Password is required",
"otpRemoveSubmit": "Отключить 2FA",
"paginator": "Страница {current} из {last}",
"paginatorToFirst": "Перейти на первую страницу",
"paginatorToPrevious": "Перейти на предыдущую страницу",
"paginatorToNext": "Перейти на следующую страницу",
"paginatorToLast": "Перейти на последнюю страницу",
"copyText": "Скопировать текст",
"copyTextFailed": "Не удалось скопировать текст: ",
"copyTextClipboard": "Копировать в буфер обмена",
"inviteErrorInvalidConfirmation": "Неверное подтверждение",
"passwordRequired": "Пароль обязателен",
"allowAll": "Разрешить всё",
"permissionsAllowAll": "Разрешить все разрешения",
"githubUsernameRequired": "Имя пользователя GitHub обязательно",
@@ -1094,8 +1119,8 @@
"sidebarAllUsers": "Все пользователи",
"sidebarIdentityProviders": "Поставщики удостоверений",
"sidebarLicense": "Лицензия",
"sidebarClients": "Clients",
"sidebarDomains": "Domains",
"sidebarClients": "Клиенты (бета)",
"sidebarDomains": "Домены",
"enableDockerSocket": "Включить Docker Socket",
"enableDockerSocketDescription": "Включить обнаружение Docker Socket для заполнения информации о контейнерах. Путь к сокету должен быть предоставлен Newt.",
"enableDockerSocketLink": "Узнать больше",
@@ -1135,36 +1160,36 @@
"dark": "тёмная",
"system": "системная",
"theme": "Тема",
"subnetRequired": "Subnet is required",
"subnetRequired": "Требуется подсеть",
"initialSetupTitle": "Начальная настройка сервера",
"initialSetupDescription": "Создайте первоначальную учётную запись администратора сервера. Может существовать только один администратор сервера. Вы всегда можете изменить эти учётные данные позже.",
"createAdminAccount": "Создать учётную запись администратора",
"setupErrorCreateAdmin": "Произошла ошибка при создании учётной записи администратора сервера.",
"certificateStatus": "Certificate Status",
"loading": "Loading",
"restart": "Restart",
"domains": "Domains",
"domainsDescription": "Manage domains for your organization",
"domainsSearch": "Search domains...",
"domainAdd": "Add Domain",
"domainAddDescription": "Register a new domain with your organization",
"domainCreate": "Create Domain",
"domainCreatedDescription": "Domain created successfully",
"domainDeletedDescription": "Domain deleted successfully",
"domainQuestionRemove": "Are you sure you want to remove the domain {domain} from your account?",
"domainMessageRemove": "Once removed, the domain will no longer be associated with your account.",
"domainMessageConfirm": "To confirm, please type the domain name below.",
"domainConfirmDelete": "Confirm Delete Domain",
"domainDelete": "Delete Domain",
"domain": "Domain",
"selectDomainTypeNsName": "Domain Delegation (NS)",
"selectDomainTypeNsDescription": "This domain and all its subdomains. Use this when you want to control an entire domain zone.",
"selectDomainTypeCnameName": "Single Domain (CNAME)",
"selectDomainTypeCnameDescription": "Just this specific domain. Use this for individual subdomains or specific domain entries.",
"certificateStatus": "Статус сертификата",
"loading": "Загрузка",
"restart": "Перезагрузка",
"domains": "Домены",
"domainsDescription": "Управление доменами для вашей организации",
"domainsSearch": "Поиск доменов...",
"domainAdd": "Добавить Домен",
"domainAddDescription": "Зарегистрировать новый домен в вашей организации",
"domainCreate": "Создать Домен",
"domainCreatedDescription": "Домен успешно создан",
"domainDeletedDescription": "Домен успешно удален",
"domainQuestionRemove": "Вы уверены, что хотите удалить домен {domain} из вашего аккаунта?",
"domainMessageRemove": "После удаления домен больше не будет связан с вашей учетной записью.",
"domainMessageConfirm": "Для подтверждения введите ниже имя домена.",
"domainConfirmDelete": "Подтвердить удаление домена",
"domainDelete": "Удалить Домен",
"domain": "Домен",
"selectDomainTypeNsName": "Делегация домена (NS)",
"selectDomainTypeNsDescription": "Этот домен и все его субдомены. Используйте это, когда вы хотите управлять всей доменной зоной.",
"selectDomainTypeCnameName": "Одиночный домен (CNAME)",
"selectDomainTypeCnameDescription": "Только этот конкретный домен. Используйте это для отдельных субдоменов или отдельных записей домена.",
"selectDomainTypeWildcardName": "Wildcard Domain",
"selectDomainTypeWildcardDescription": "This domain and its first level of subdomains.",
"domainDelegation": "Single Domain",
"selectType": "Select a type",
"selectDomainTypeWildcardDescription": "Этот домен и его субдомены.",
"domainDelegation": "Единый домен",
"selectType": "Выберите тип",
"actions": "Actions",
"refresh": "Refresh",
"refreshError": "Failed to refresh data",
@@ -1196,7 +1221,7 @@
"sidebarExpand": "Expand",
"newtUpdateAvailable": "Update Available",
"newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.",
"domainPickerEnterDomain": "Enter your domain",
"domainPickerEnterDomain": "Domain",
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp",
"domainPickerDescription": "Enter the full domain of the resource to see available options.",
"domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options",
@@ -1206,7 +1231,7 @@
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Checking availability...",
"domainPickerNoMatchingDomains": "No matching domains found for \"{userInput}\". Try a different domain or check your organization's domain settings.",
"domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.",
"domainPickerOrganizationDomains": "Organization Domains",
"domainPickerProvidedDomains": "Provided Domains",
"domainPickerSubdomain": "Subdomain: {subdomain}",
@@ -1266,12 +1291,58 @@
"createDomainName": "Name:",
"createDomainValue": "Value:",
"createDomainCnameRecords": "CNAME Records",
"createDomainARecords": "A Records",
"createDomainRecordNumber": "Record {number}",
"createDomainTxtRecords": "TXT Records",
"createDomainSaveTheseRecords": "Save These Records",
"createDomainSaveTheseRecordsDescription": "Make sure to save these DNS records as you will not see them again.",
"createDomainDnsPropagation": "DNS Propagation",
"createDomainDnsPropagationDescription": "DNS changes may take some time to propagate across the internet. This can take anywhere from a few minutes to 48 hours, depending on your DNS provider and TTL settings.",
"resourcePortRequired": "Port number is required for non-HTTP resources",
"resourcePortNotAllowed": "Port number should not be set for HTTP resources"
}
"createDomainSaveTheseRecords": "Сохранить эти записи",
"createDomainSaveTheseRecordsDescription": "Обязательно сохраните эти DNS записи, так как вы их больше не увидите.",
"createDomainDnsPropagation": "Распространение DNS",
"createDomainDnsPropagationDescription": "Изменения DNS могут занять некоторое время для распространения через интернет. Это может занять от нескольких минут до 48 часов в зависимости от вашего DNS провайдера и настроек TTL.",
"resourcePortRequired": "Номер порта необходим для не-HTTP ресурсов",
"resourcePortNotAllowed": "Номер порта не должен быть установлен для HTTP ресурсов",
"signUpTerms": {
"IAgreeToThe": "Я согласен с",
"termsOfService": "условия использования",
"and": "и",
"privacyPolicy": "политика конфиденциальности"
},
"siteRequired": "Необходимо указать сайт.",
"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",
"seeAllClients": "See All Clients",
"clientInformation": "Client Information",
"clientNamePlaceholder": "Client name",
"address": "Address",
"subnetPlaceholder": "Subnet",
"addressDescription": "The address that this client will use for connectivity",
"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",
"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",
"clientUpdatedDescription": "The client has been updated.",
"clientUpdateFailed": "Failed to update client",
"clientUpdateError": "An error occurred while updating the client.",
"sitesFetchFailed": "Failed to fetch sites",
"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"
}

View File

@@ -59,7 +59,6 @@
"siteErrorCreate": "Site oluşturulurken hata",
"siteErrorCreateKeyPair": "Anahtar çifti veya site varsayılanları bulunamadı",
"siteErrorCreateDefaults": "Site varsayılanları bulunamadı",
"siteNameDescription": "Bu, site için görünen addır.",
"method": "Yöntem",
"siteMethodDescription": "Bağlantıları nasıl açığa çıkaracağınız budur.",
"siteLearnNewt": "Newt'i sisteminize nasıl kuracağınızı öğrenin",
@@ -834,6 +833,24 @@
"pincodeRequirementsLength": "PIN kesinlikle 6 haneli olmalıdır",
"pincodeRequirementsChars": "PIN sadece numaralardan oluşmalıdır",
"passwordRequirementsLength": "Şifre en az 1 karakter uzunluğunda olmalıdır",
"passwordRequirementsTitle": "Password requirements:",
"passwordRequirementLength": "At least 8 characters long",
"passwordRequirementUppercase": "At least one uppercase letter",
"passwordRequirementLowercase": "At least one lowercase letter",
"passwordRequirementNumber": "At least one number",
"passwordRequirementSpecial": "At least one special character",
"passwordRequirementsMet": "✓ Password meets all requirements",
"passwordStrength": "Password strength",
"passwordStrengthWeak": "Weak",
"passwordStrengthMedium": "Medium",
"passwordStrengthStrong": "Strong",
"passwordRequirements": "Requirements:",
"passwordRequirementLengthText": "8+ characters",
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
"passwordRequirementNumberText": "Number (0-9)",
"passwordRequirementSpecialText": "Special character (!@#$%...)",
"passwordsDoNotMatch": "Passwords do not match",
"otpEmailRequirementsLength": "OTP en az 1 karakter uzunluğunda olmalıdır",
"otpEmailSent": "OTP Gönderildi",
"otpEmailSentDescription": "E-posta adresinize bir OTP gönderildi",
@@ -968,6 +985,9 @@
"actionDeleteSite": "Siteyi Sil",
"actionGetSite": "Siteyi Al",
"actionListSites": "Siteleri Listele",
"setupToken": "Setup Token",
"setupTokenPlaceholder": "Enter the setup token from the server console",
"setupTokenRequired": "Setup token is required",
"actionUpdateSite": "Siteyi Güncelle",
"actionListSiteRoles": "İzin Verilen Site Rolleri Listele",
"actionCreateResource": "Kaynak Oluştur",
@@ -1023,6 +1043,11 @@
"actionDeleteIdpOrg": "Kimlik Sağlayıcı Organizasyon Politikasını Sil",
"actionListIdpOrgs": "Kimlik Sağlayıcı Organizasyonları Listele",
"actionUpdateIdpOrg": "Kimlik Sağlayıcı Organizasyonu Güncelle",
"actionCreateClient": "Müşteri Oluştur",
"actionDeleteClient": "Müşteri Sil",
"actionUpdateClient": "Müşteri Güncelle",
"actionListClients": "Müşterileri Listele",
"actionGetClient": "Müşteriyi Al",
"noneSelected": "Hiçbiri seçili değil",
"orgNotFound2": "Hiçbir organizasyon bulunamadı.",
"searchProgress": "Ara...",
@@ -1094,7 +1119,7 @@
"sidebarAllUsers": "Tüm Kullanıcılar",
"sidebarIdentityProviders": "Kimlik Sağlayıcılar",
"sidebarLicense": "Lisans",
"sidebarClients": "Müşteriler",
"sidebarClients": "Müşteriler (Beta)",
"sidebarDomains": "Alan Adları",
"enableDockerSocket": "Docker Soketi Etkinleştir",
"enableDockerSocketDescription": "Konteyner bilgilerini doldurmak için Docker Socket keşfini etkinleştirin. Socket yolu Newt'e sağlanmalıdır.",
@@ -1162,7 +1187,7 @@
"selectDomainTypeCnameName": "Tekil Alan Adı (CNAME)",
"selectDomainTypeCnameDescription": "Sadece bu belirli alan adı. Bireysel alt alan adları veya belirli alan adı girişleri için bunu kullanın.",
"selectDomainTypeWildcardName": "Wildcard Alan Adı",
"selectDomainTypeWildcardDescription": "Bu alan adı ve onun ilk alt alan düzeyi.",
"selectDomainTypeWildcardDescription": "Bu domain ve alt alan adları.",
"domainDelegation": "Tekil Alan Adı",
"selectType": "Bir tür seçin",
"actions": "İşlemler",
@@ -1196,7 +1221,7 @@
"sidebarExpand": "Genişlet",
"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ınızı girin",
"domainPickerEnterDomain": "Domain",
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com veya sadece myapp",
"domainPickerDescription": "Mevcut seçenekleri görmek için kaynağın tam etki alanını girin.",
"domainPickerDescriptionSaas": "Mevcut seçenekleri görmek için tam etki alanı, alt etki alanı veya sadece bir isim girin",
@@ -1206,7 +1231,7 @@
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Kullanılabilirlik kontrol ediliyor...",
"domainPickerNoMatchingDomains": "\"{userInput}\" için uygun alan adı bulunamadı. Farklı bir alan adı deneyin veya organizasyonunuzun alan adı ayarlarını kontrol edin.",
"domainPickerNoMatchingDomains": "Eşleşen domain bulunamadı. Farklı bir domain deneyin veya organizasyonunuzun domain ayarlarını kontrol edin.",
"domainPickerOrganizationDomains": "Organizasyon Alan Adları",
"domainPickerProvidedDomains": "Sağlanan Alan Adları",
"domainPickerSubdomain": "Alt Alan: {subdomain}",
@@ -1266,6 +1291,7 @@
"createDomainName": "Ad:",
"createDomainValue": "Değer:",
"createDomainCnameRecords": "CNAME Kayıtları",
"createDomainARecords": "A Kayıtları",
"createDomainRecordNumber": "Kayıt {number}",
"createDomainTxtRecords": "TXT Kayıtları",
"createDomainSaveTheseRecords": "Bu Kayıtları Kaydet",
@@ -1273,5 +1299,50 @@
"createDomainDnsPropagation": "DNS Yayılması",
"createDomainDnsPropagationDescription": "DNS değişikliklerinin internet genelinde yayılması zaman alabilir. DNS sağlayıcınız ve TTL ayarlarına bağlı olarak bu birkaç dakika ile 48 saat arasında değişebilir.",
"resourcePortRequired": "HTTP dışı kaynaklar için bağlantı noktası numarası gereklidir",
"resourcePortNotAllowed": "HTTP kaynakları için bağlantı noktası numarası ayarlanmamalı"
}
"resourcePortNotAllowed": "HTTP kaynakları için bağlantı noktası numarası ayarlanmamalı",
"signUpTerms": {
"IAgreeToThe": "Kabul ediyorum",
"termsOfService": "hizmet şartları",
"and": "ve",
"privacyPolicy": "gizlilik politikası"
},
"siteRequired": "Site gerekli.",
"olmTunnel": "Olm Tüneli",
"olmTunnelDescription": "Müşteri bağlantıları için Olm kullanın",
"errorCreatingClient": "Müşteri oluşturulurken hata oluştu",
"clientDefaultsNotFound": "Müşteri varsayılanları bulunamadı",
"createClient": "Müşteri Oluştur",
"createClientDescription": "Sitelerinize bağlanmak için yeni bir müşteri oluşturun",
"seeAllClients": "Tüm Müşterileri Gör",
"clientInformation": "Müşteri Bilgileri",
"clientNamePlaceholder": "Müşteri adı",
"address": "Adres",
"subnetPlaceholder": "Alt ağ",
"addressDescription": "Bu müşteri için bağlantıda kullanılacak adres",
"selectSites": "Siteleri seçin",
"sitesDescription": "Müşteri seçilen sitelere bağlantı kuracaktır",
"clientInstallOlm": "Olm Yükle",
"clientInstallOlmDescription": "Sisteminizde Olm çalıştırın",
"clientOlmCredentials": "Olm Kimlik Bilgileri",
"clientOlmCredentialsDescription": "Bu, Olm'in sunucu ile kimlik doğrulaması yapacağı yöntemdir",
"olmEndpoint": "Olm Uç Noktası",
"olmId": "Olm Kimliği",
"olmSecretKey": "Olm Gizli Anahtarı",
"clientCredentialsSave": "Kimlik Bilgilerinizi Kaydedin",
"clientCredentialsSaveDescription": "Bunu yalnızca bir kez görebileceksiniz. Güvenli bir yere kopyaladığınızdan emin olun.",
"generalSettingsDescription": "Bu müşteri için genel ayarları yapılandırın",
"clientUpdated": "Müşteri güncellendi",
"clientUpdatedDescription": "Müşteri güncellenmiştir.",
"clientUpdateFailed": "Müşteri güncellenemedi",
"clientUpdateError": "Müşteri güncellenirken bir hata oluştu.",
"sitesFetchFailed": "Siteler alınamadı",
"sitesFetchError": "Siteler alınırken bir hata oluştu.",
"olmErrorFetchReleases": "Olm yayınları alınırken bir hata oluştu.",
"olmErrorFetchLatest": "En son Olm yayını alınırken bir hata oluştu.",
"remoteSubnets": "Uzak Alt Ağlar",
"enterCidrRange": "CIDR aralığını girin",
"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": "Genel Proxy'i Etkinleştir",
"resourceEnableProxyDescription": "Bu kaynağa genel proxy erişimini etkinleştirin. Bu sayede ağ dışından açık bir port üzerinden kaynağa bulut aracılığıyla erişim sağlanır. Traefik yapılandırması gereklidir.",
"externalProxyEnabled": "Dış Proxy Etkinleştirildi"
}

View File

@@ -59,7 +59,6 @@
"siteErrorCreate": "创建站点出错",
"siteErrorCreateKeyPair": "找不到密钥对或站点默认值",
"siteErrorCreateDefaults": "未找到站点默认值",
"siteNameDescription": "这是站点的显示名称。",
"method": "方法",
"siteMethodDescription": "这是您将如何显示连接。",
"siteLearnNewt": "学习如何在您的系统上安装 Newt",
@@ -834,6 +833,24 @@
"pincodeRequirementsLength": "PIN码必须是6位数字",
"pincodeRequirementsChars": "PIN 必须只包含数字",
"passwordRequirementsLength": "密码必须至少 1 个字符长",
"passwordRequirementsTitle": "Password requirements:",
"passwordRequirementLength": "At least 8 characters long",
"passwordRequirementUppercase": "At least one uppercase letter",
"passwordRequirementLowercase": "At least one lowercase letter",
"passwordRequirementNumber": "At least one number",
"passwordRequirementSpecial": "At least one special character",
"passwordRequirementsMet": "✓ Password meets all requirements",
"passwordStrength": "Password strength",
"passwordStrengthWeak": "Weak",
"passwordStrengthMedium": "Medium",
"passwordStrengthStrong": "Strong",
"passwordRequirements": "Requirements:",
"passwordRequirementLengthText": "8+ characters",
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
"passwordRequirementNumberText": "Number (0-9)",
"passwordRequirementSpecialText": "Special character (!@#$%...)",
"passwordsDoNotMatch": "Passwords do not match",
"otpEmailRequirementsLength": "OTP 必须至少 1 个字符长",
"otpEmailSent": "OTP 已发送",
"otpEmailSentDescription": "OTP 已经发送到您的电子邮件",
@@ -968,6 +985,9 @@
"actionDeleteSite": "删除站点",
"actionGetSite": "获取站点",
"actionListSites": "站点列表",
"setupToken": "Setup Token",
"setupTokenPlaceholder": "Enter the setup token from the server console",
"setupTokenRequired": "Setup token is required",
"actionUpdateSite": "更新站点",
"actionListSiteRoles": "允许站点角色列表",
"actionCreateResource": "创建资源",
@@ -1023,6 +1043,11 @@
"actionDeleteIdpOrg": "删除 IDP组织策略",
"actionListIdpOrgs": "列出 IDP组织",
"actionUpdateIdpOrg": "更新 IDP组织",
"actionCreateClient": "创建客户端",
"actionDeleteClient": "删除客户端",
"actionUpdateClient": "更新客户端",
"actionListClients": "列出客户端",
"actionGetClient": "获取客户端",
"noneSelected": "未选择",
"orgNotFound2": "未找到组织。",
"searchProgress": "搜索中...",
@@ -1094,7 +1119,7 @@
"sidebarAllUsers": "所有用户",
"sidebarIdentityProviders": "身份提供商",
"sidebarLicense": "证书",
"sidebarClients": "客户",
"sidebarClients": "客户端(测试版)",
"sidebarDomains": "域",
"enableDockerSocket": "启用停靠套接字",
"enableDockerSocketDescription": "启用 Docker Socket 发现以填充容器信息。必须向 Newt 提供 Socket 路径。",
@@ -1162,7 +1187,7 @@
"selectDomainTypeCnameName": "单个域CNAME",
"selectDomainTypeCnameDescription": "仅此特定域。用于单个子域或特定域条目。",
"selectDomainTypeWildcardName": "通配符域",
"selectDomainTypeWildcardDescription": "此域及其第一级子域。",
"selectDomainTypeWildcardDescription": "此域及其子域。",
"domainDelegation": "单个域",
"selectType": "选择一个类型",
"actions": "操作",
@@ -1196,7 +1221,7 @@
"sidebarExpand": "展开",
"newtUpdateAvailable": "更新可用",
"newtUpdateAvailableInfo": "新版本的 Newt 已可用。请更新到最新版本以获得最佳体验。",
"domainPickerEnterDomain": "输入您的域",
"domainPickerEnterDomain": "域",
"domainPickerPlaceholder": "myapp.example.com、api.v1.mydomain.com 或仅 myapp",
"domainPickerDescription": "输入资源的完整域名以查看可用选项。",
"domainPickerDescriptionSaas": "输入完整域名、子域或名称以查看可用选项。",
@@ -1206,7 +1231,7 @@
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "检查可用性...",
"domainPickerNoMatchingDomains": "未找到 \"{userInput}\" 的匹配域。尝试其他域或检查您组织的域设置。",
"domainPickerNoMatchingDomains": "未找到匹配的域名。尝试不同的域名或检查您组织的域设置。",
"domainPickerOrganizationDomains": "组织域",
"domainPickerProvidedDomains": "提供的域",
"domainPickerSubdomain": "子域:{subdomain}",
@@ -1266,6 +1291,7 @@
"createDomainName": "名称:",
"createDomainValue": "值:",
"createDomainCnameRecords": "CNAME 记录",
"createDomainARecords": "A记录",
"createDomainRecordNumber": "记录 {number}",
"createDomainTxtRecords": "TXT 记录",
"createDomainSaveTheseRecords": "保存这些记录",
@@ -1273,5 +1299,50 @@
"createDomainDnsPropagation": "DNS 传播",
"createDomainDnsPropagationDescription": "DNS 更改可能需要一些时间才能在互联网上传播。这可能需要从几分钟到 48 小时,具体取决于您的 DNS 提供商和 TTL 设置。",
"resourcePortRequired": "非 HTTP 资源必须输入端口号",
"resourcePortNotAllowed": "HTTP 资源不应设置端口号"
}
"resourcePortNotAllowed": "HTTP 资源不应设置端口号",
"signUpTerms": {
"IAgreeToThe": "我同意",
"termsOfService": "服务条款",
"and": "和",
"privacyPolicy": "隐私政策"
},
"siteRequired": "需要站点。",
"olmTunnel": "Olm 隧道",
"olmTunnelDescription": "使用 Olm 进行客户端连接",
"errorCreatingClient": "创建客户端出错",
"clientDefaultsNotFound": "未找到客户端默认值",
"createClient": "创建客户端",
"createClientDescription": "创建一个新客户端来连接您的站点",
"seeAllClients": "查看所有客户端",
"clientInformation": "客户端信息",
"clientNamePlaceholder": "客户端名称",
"address": "地址",
"subnetPlaceholder": "子网",
"addressDescription": "此客户端将用于连接的地址",
"selectSites": "选择站点",
"sitesDescription": "客户端将与所选站点进行连接",
"clientInstallOlm": "安装 Olm",
"clientInstallOlmDescription": "在您的系统上运行 Olm",
"clientOlmCredentials": "Olm 凭据",
"clientOlmCredentialsDescription": "这是 Olm 服务器的身份验证方式",
"olmEndpoint": "Olm 端点",
"olmId": "Olm ID",
"olmSecretKey": "Olm 私钥",
"clientCredentialsSave": "保存您的凭据",
"clientCredentialsSaveDescription": "该信息仅会显示一次,请确保将其复制到安全位置。",
"generalSettingsDescription": "配置此客户端的常规设置",
"clientUpdated": "客户端已更新",
"clientUpdatedDescription": "客户端已更新。",
"clientUpdateFailed": "更新客户端失败",
"clientUpdateError": "更新客户端时出错。",
"sitesFetchFailed": "获取站点失败",
"sitesFetchError": "获取站点时出错。",
"olmErrorFetchReleases": "获取 Olm 发布版本时出错。",
"olmErrorFetchLatest": "获取最新 Olm 发布版本时出错。",
"remoteSubnets": "远程子网",
"enterCidrRange": "输入 CIDR 范围",
"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": "启用公共代理",
"resourceEnableProxyDescription": "启用到此资源的公共代理。这允许外部网络通过开放端口访问资源。需要 Traefik 配置。",
"externalProxyEnabled": "外部代理已启用"
}

1687
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -50,15 +50,15 @@
"@radix-ui/react-tabs": "1.1.12",
"@radix-ui/react-toast": "1.2.14",
"@radix-ui/react-tooltip": "^1.2.7",
"@react-email/components": "0.3.1",
"@react-email/render": "^1.1.2",
"@react-email/components": "0.5.0",
"@react-email/render": "^1.2.0",
"@react-email/tailwind": "1.2.2",
"@simplewebauthn/browser": "^13.1.0",
"@simplewebauthn/server": "^9.0.3",
"@react-email/tailwind": "1.2.1",
"@tailwindcss/forms": "^0.5.10",
"@tanstack/react-table": "8.21.3",
"arctic": "^3.7.0",
"axios": "1.10.0",
"axios": "1.11.0",
"better-sqlite3": "11.7.0",
"canvas-confetti": "1.9.3",
"class-variance-authority": "^0.7.1",
@@ -69,10 +69,10 @@
"cookies": "^0.9.1",
"cors": "2.8.5",
"crypto-js": "^4.2.0",
"drizzle-orm": "0.44.2",
"eslint": "9.31.0",
"eslint-config-next": "15.3.5",
"express": "4.21.2",
"drizzle-orm": "0.44.4",
"eslint": "9.33.0",
"eslint-config-next": "15.4.6",
"express": "5.1.0",
"express-rate-limit": "7.5.1",
"glob": "11.0.3",
"helmet": "8.1.0",
@@ -82,69 +82,70 @@
"jmespath": "^0.16.0",
"js-yaml": "4.1.0",
"jsonwebtoken": "^9.0.2",
"lucide-react": "0.525.0",
"lucide-react": "0.539.0",
"moment": "2.30.1",
"next": "15.3.5",
"next": "15.4.6",
"next-intl": "^4.3.4",
"next-themes": "0.4.6",
"node-cache": "5.1.2",
"node-fetch": "3.3.2",
"nodemailer": "7.0.5",
"npm": "^11.4.2",
"npm": "^11.5.2",
"oslo": "1.2.1",
"pg": "^8.16.2",
"posthog-node": "^5.7.0",
"qrcode.react": "4.2.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-easy-sort": "^1.6.0",
"react-hook-form": "7.60.0",
"react-hook-form": "7.62.0",
"react-icons": "^5.5.0",
"rebuild": "0.1.2",
"semver": "^7.7.2",
"swagger-ui-express": "^5.0.1",
"tailwind-merge": "3.3.1",
"tw-animate-css": "^1.3.5",
"tw-animate-css": "^1.3.6",
"uuid": "^11.1.0",
"vaul": "1.1.2",
"winston": "3.17.0",
"winston-daily-rotate-file": "5.0.0",
"ws": "8.18.3",
"yargs": "18.0.0",
"zod": "3.25.76",
"zod-validation-error": "3.5.2",
"yargs": "18.0.0"
"zod-validation-error": "3.5.2"
},
"devDependencies": {
"@dotenvx/dotenvx": "1.47.6",
"@dotenvx/dotenvx": "1.48.4",
"@esbuild-plugins/tsconfig-paths": "0.1.2",
"@tailwindcss/postcss": "^4.1.10",
"@types/better-sqlite3": "7.6.12",
"@types/cookie-parser": "1.4.9",
"@types/cors": "2.8.19",
"@types/crypto-js": "^4.2.2",
"@types/express": "5.0.0",
"@types/express": "5.0.3",
"@types/express-session": "^1.18.2",
"@types/jmespath": "^0.15.2",
"@types/js-yaml": "4.0.9",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^24",
"@types/nodemailer": "6.4.17",
"@types/pg": "8.15.4",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@types/pg": "8.15.5",
"@types/react": "19.1.10",
"@types/react-dom": "19.1.7",
"@types/semver": "^7.7.0",
"@types/swagger-ui-express": "^4.1.8",
"@types/ws": "8.18.1",
"@types/yargs": "17.0.33",
"drizzle-kit": "0.31.4",
"esbuild": "0.25.6",
"esbuild": "0.25.9",
"esbuild-node-externals": "1.18.0",
"postcss": "^8",
"react-email": "4.1.0",
"react-email": "4.2.8",
"tailwindcss": "^4.1.4",
"tsc-alias": "1.8.16",
"tsx": "4.20.3",
"tsx": "4.20.4",
"typescript": "^5",
"typescript-eslint": "^8.36.0"
"typescript-eslint": "^8.39.1"
},
"overrides": {
"emblor": {

View File

@@ -69,6 +69,11 @@ export enum ActionsEnum {
deleteResourceRule = "deleteResourceRule",
listResourceRules = "listResourceRules",
updateResourceRule = "updateResourceRule",
createSiteResource = "createSiteResource",
deleteSiteResource = "deleteSiteResource",
getSiteResource = "getSiteResource",
listSiteResources = "listSiteResources",
updateSiteResource = "updateSiteResource",
createClient = "createClient",
deleteClient = "deleteClient",
updateClient = "updateClient",

View File

@@ -5,7 +5,8 @@ import {
boolean,
integer,
bigint,
real
real,
text
} from "drizzle-orm/pg-core";
import { InferSelectModel } from "drizzle-orm";
@@ -22,7 +23,8 @@ export const domains = pgTable("domains", {
export const orgs = pgTable("orgs", {
orgId: varchar("orgId").primaryKey(),
name: varchar("name").notNull(),
subnet: varchar("subnet")
subnet: varchar("subnet"),
createdAt: text("createdAt")
});
export const orgDomains = pgTable("orgDomains", {
@@ -58,16 +60,12 @@ export const sites = pgTable("sites", {
publicKey: varchar("publicKey"),
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
listenPort: integer("listenPort"),
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true)
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access
});
export const resources = pgTable("resources", {
resourceId: serial("resourceId").primaryKey(),
siteId: integer("siteId")
.references(() => sites.siteId, {
onDelete: "cascade"
})
.notNull(),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
@@ -92,7 +90,11 @@ export const resources = pgTable("resources", {
enabled: boolean("enabled").notNull().default(true),
stickySession: boolean("stickySession").notNull().default(false),
tlsServerName: varchar("tlsServerName"),
setHostHeader: varchar("setHostHeader")
setHostHeader: varchar("setHostHeader"),
enableProxy: boolean("enableProxy").default(true),
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
onDelete: "cascade"
}),
});
export const targets = pgTable("targets", {
@@ -102,6 +104,11 @@ export const targets = pgTable("targets", {
onDelete: "cascade"
})
.notNull(),
siteId: integer("siteId")
.references(() => sites.siteId, {
onDelete: "cascade"
})
.notNull(),
ip: varchar("ip").notNull(),
method: varchar("method"),
port: integer("port").notNull(),
@@ -120,6 +127,22 @@ export const exitNodes = pgTable("exitNodes", {
maxConnections: integer("maxConnections")
});
export const siteResources = pgTable("siteResources", { // this is for the clients
siteResourceId: serial("siteResourceId").primaryKey(),
siteId: integer("siteId")
.notNull()
.references(() => sites.siteId, { onDelete: "cascade" }),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
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),
});
export const users = pgTable("user", {
userId: varchar("id").primaryKey(),
email: varchar("email"),
@@ -135,6 +158,8 @@ export const users = pgTable("user", {
twoFactorSecret: varchar("twoFactorSecret"),
emailVerified: boolean("emailVerified").notNull().default(false),
dateCreated: varchar("dateCreated").notNull(),
termsAcceptedTimestamp: varchar("termsAcceptedTimestamp"),
termsVersion: varchar("termsVersion"),
serverAdmin: boolean("serverAdmin").notNull().default(false)
});
@@ -504,13 +529,13 @@ export const clients = pgTable("clients", {
name: varchar("name").notNull(),
pubKey: varchar("pubKey"),
subnet: varchar("subnet").notNull(),
megabytesIn: integer("bytesIn"),
megabytesOut: integer("bytesOut"),
megabytesIn: real("bytesIn"),
megabytesOut: real("bytesOut"),
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
lastPing: varchar("lastPing"),
lastPing: integer("lastPing"),
type: varchar("type").notNull(), // "olm"
online: boolean("online").notNull().default(false),
endpoint: varchar("endpoint"),
// endpoint: varchar("endpoint"),
lastHolePunch: integer("lastHolePunch"),
maxConnections: integer("maxConnections")
});
@@ -522,13 +547,15 @@ export const clientSites = pgTable("clientSites", {
siteId: integer("siteId")
.notNull()
.references(() => sites.siteId, { onDelete: "cascade" }),
isRelayed: boolean("isRelayed").notNull().default(false)
isRelayed: boolean("isRelayed").notNull().default(false),
endpoint: varchar("endpoint")
});
export const olms = pgTable("olms", {
olmId: varchar("id").primaryKey(),
secretHash: varchar("secretHash").notNull(),
dateCreated: varchar("dateCreated").notNull(),
version: text("version"),
clientId: integer("clientId").references(() => clients.clientId, {
onDelete: "cascade"
})
@@ -539,7 +566,7 @@ export const olmSessions = pgTable("clientSession", {
olmId: varchar("olmId")
.notNull()
.references(() => olms.olmId, { onDelete: "cascade" }),
expiresAt: integer("expiresAt").notNull()
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
});
export const userClients = pgTable("userClients", {
@@ -562,9 +589,11 @@ export const roleClients = pgTable("roleClients", {
export const securityKeys = pgTable("webauthnCredentials", {
credentialId: varchar("credentialId").primaryKey(),
userId: varchar("userId").notNull().references(() => users.userId, {
onDelete: "cascade"
}),
userId: varchar("userId")
.notNull()
.references(() => users.userId, {
onDelete: "cascade"
}),
publicKey: varchar("publicKey").notNull(),
signCount: integer("signCount").notNull(),
transports: varchar("transports"),
@@ -584,6 +613,14 @@ export const webauthnChallenge = pgTable("webauthnChallenge", {
expiresAt: bigint("expiresAt", { mode: "number" }).notNull() // Unix timestamp
});
export const setupTokens = pgTable("setupTokens", {
tokenId: varchar("tokenId").primaryKey(),
token: varchar("token").notNull(),
used: boolean("used").notNull().default(false),
dateCreated: varchar("dateCreated").notNull(),
dateUsed: varchar("dateUsed")
});
export type Org = InferSelectModel<typeof orgs>;
export type User = InferSelectModel<typeof users>;
export type Site = InferSelectModel<typeof sites>;
@@ -629,3 +666,5 @@ export type OlmSession = InferSelectModel<typeof olmSessions>;
export type UserClient = InferSelectModel<typeof userClients>;
export type RoleClient = InferSelectModel<typeof roleClients>;
export type OrgDomains = InferSelectModel<typeof orgDomains>;
export type SiteResource = InferSelectModel<typeof siteResources>;
export type SetupToken = InferSelectModel<typeof setupTokens>;

View File

@@ -16,7 +16,8 @@ export const domains = sqliteTable("domains", {
export const orgs = sqliteTable("orgs", {
orgId: text("orgId").primaryKey(),
name: text("name").notNull(),
subnet: text("subnet")
subnet: text("subnet"),
createdAt: text("createdAt")
});
export const userDomains = sqliteTable("userDomains", {
@@ -65,16 +66,12 @@ export const sites = sqliteTable("sites", {
listenPort: integer("listenPort"),
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
.notNull()
.default(true)
.default(true),
remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access
});
export const resources = sqliteTable("resources", {
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
siteId: integer("siteId")
.references(() => sites.siteId, {
onDelete: "cascade"
})
.notNull(),
orgId: text("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
@@ -105,7 +102,11 @@ export const resources = sqliteTable("resources", {
.notNull()
.default(false),
tlsServerName: text("tlsServerName"),
setHostHeader: text("setHostHeader")
setHostHeader: text("setHostHeader"),
enableProxy: integer("enableProxy", { mode: "boolean" }).default(true),
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
onDelete: "cascade"
}),
});
export const targets = sqliteTable("targets", {
@@ -115,6 +116,11 @@ export const targets = sqliteTable("targets", {
onDelete: "cascade"
})
.notNull(),
siteId: integer("siteId")
.references(() => sites.siteId, {
onDelete: "cascade"
})
.notNull(),
ip: text("ip").notNull(),
method: text("method"),
port: integer("port").notNull(),
@@ -133,6 +139,22 @@ export const exitNodes = sqliteTable("exitNodes", {
maxConnections: integer("maxConnections")
});
export const siteResources = sqliteTable("siteResources", { // this is for the clients
siteResourceId: integer("siteResourceId").primaryKey({ autoIncrement: true }),
siteId: integer("siteId")
.notNull()
.references(() => sites.siteId, { onDelete: "cascade" }),
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
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),
});
export const users = sqliteTable("user", {
userId: text("id").primaryKey(),
email: text("email"),
@@ -154,6 +176,8 @@ export const users = sqliteTable("user", {
.notNull()
.default(false),
dateCreated: text("dateCreated").notNull(),
termsAcceptedTimestamp: text("termsAcceptedTimestamp"),
termsVersion: text("termsVersion"),
serverAdmin: integer("serverAdmin", { mode: "boolean" })
.notNull()
.default(false)
@@ -161,9 +185,11 @@ export const users = sqliteTable("user", {
export const securityKeys = sqliteTable("webauthnCredentials", {
credentialId: text("credentialId").primaryKey(),
userId: text("userId").notNull().references(() => users.userId, {
onDelete: "cascade"
}),
userId: text("userId")
.notNull()
.references(() => users.userId, {
onDelete: "cascade"
}),
publicKey: text("publicKey").notNull(),
signCount: integer("signCount").notNull(),
transports: text("transports"),
@@ -182,6 +208,14 @@ export const webauthnChallenge = sqliteTable("webauthnChallenge", {
expiresAt: integer("expiresAt").notNull() // Unix timestamp
});
export const setupTokens = sqliteTable("setupTokens", {
tokenId: text("tokenId").primaryKey(),
token: text("token").notNull(),
used: integer("used", { mode: "boolean" }).notNull().default(false),
dateCreated: text("dateCreated").notNull(),
dateUsed: text("dateUsed")
});
export const newts = sqliteTable("newt", {
newtId: text("id").primaryKey(),
secretHash: text("secretHash").notNull(),
@@ -208,10 +242,10 @@ export const clients = sqliteTable("clients", {
megabytesIn: integer("bytesIn"),
megabytesOut: integer("bytesOut"),
lastBandwidthUpdate: text("lastBandwidthUpdate"),
lastPing: text("lastPing"),
lastPing: integer("lastPing"),
type: text("type").notNull(), // "olm"
online: integer("online", { mode: "boolean" }).notNull().default(false),
endpoint: text("endpoint"),
// endpoint: text("endpoint"),
lastHolePunch: integer("lastHolePunch")
});
@@ -222,13 +256,15 @@ export const clientSites = sqliteTable("clientSites", {
siteId: integer("siteId")
.notNull()
.references(() => sites.siteId, { onDelete: "cascade" }),
isRelayed: integer("isRelayed", { mode: "boolean" }).notNull().default(false)
isRelayed: integer("isRelayed", { mode: "boolean" }).notNull().default(false),
endpoint: text("endpoint")
});
export const olms = sqliteTable("olms", {
olmId: text("id").primaryKey(),
secretHash: text("secretHash").notNull(),
dateCreated: text("dateCreated").notNull(),
version: text("version"),
clientId: integer("clientId").references(() => clients.clientId, {
onDelete: "cascade"
})
@@ -673,4 +709,7 @@ export type Idp = InferSelectModel<typeof idp>;
export type ApiKey = InferSelectModel<typeof apiKeys>;
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
export type SiteResource = InferSelectModel<typeof siteResources>;
export type OrgDomains = InferSelectModel<typeof orgDomains>;
export type SetupToken = InferSelectModel<typeof setupTokens>;
export type HostMeta = InferSelectModel<typeof hostMeta>;

View File

@@ -18,10 +18,10 @@ function createEmailClient() {
host: emailConfig.smtp_host,
port: emailConfig.smtp_port,
secure: emailConfig.smtp_secure || false,
auth: {
auth: (emailConfig.smtp_user && emailConfig.smtp_pass) ? {
user: emailConfig.smtp_user,
pass: emailConfig.smtp_pass
}
} : null
} as SMTPTransport.Options;
if (emailConfig.smtp_tls_reject_unauthorized !== undefined) {

View File

@@ -88,7 +88,7 @@ export const WelcomeQuickStart = ({
To learn how to use Newt, including more
installation methods, visit the{" "}
<a
href="https://docs.fossorial.io"
href="https://docs.digpangolin.com/manage/sites/install-site"
className="underline"
>
docs

View File

@@ -1,3 +1,4 @@
#! /usr/bin/env node
import "./extendZod.ts";
import { runSetupFunctions } from "./setup";
@@ -7,11 +8,17 @@ import { createInternalServer } from "./internalServer";
import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "@server/db";
import { createIntegrationApiServer } from "./integrationApiServer";
import config from "@server/lib/config";
import { setHostMeta } from "@server/lib/hostMeta";
import { initTelemetryClient } from "./lib/telemetry.js";
async function startServers() {
await setHostMeta();
await config.initServer();
await runSetupFunctions();
initTelemetryClient();
// Start all servers
const apiServer = createApiServer();
const internalServer = createInternalServer();

View File

@@ -30,12 +30,6 @@ export class Config {
throw new Error(`Invalid configuration file: ${errors}`);
}
if (process.env.APP_BASE_DOMAIN) {
console.log(
"WARNING: You're using deprecated environment variables. Transition to the configuration file. https://docs.fossorial.io/"
);
}
if (
// @ts-ignore
parsedConfig.users ||

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.7.0";
export const APP_VERSION = "1.8.0";
export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME);

View File

@@ -1,7 +1,9 @@
import { db } from "@server/db";
import { db, HostMeta } from "@server/db";
import { hostMeta } from "@server/db";
import { v4 as uuidv4 } from "uuid";
let gotHostMeta: HostMeta | undefined;
export async function setHostMeta() {
const [existing] = await db.select().from(hostMeta).limit(1);
@@ -15,3 +17,12 @@ export async function setHostMeta() {
.insert(hostMeta)
.values({ hostMetaId: id, createdAt: new Date().getTime() });
}
export async function getHostMeta() {
if (gotHostMeta) {
return gotHostMeta;
}
const [meta] = await db.select().from(hostMeta).limit(1);
gotHostMeta = meta;
return meta;
}

View File

@@ -271,7 +271,7 @@ export async function getNextAvailableClientSubnet(
)
].filter((address) => address !== null) as string[];
let subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
if (!subnet) {
throw new Error("No available subnets remaining in space");
}
@@ -289,7 +289,7 @@ export async function getNextAvailableOrgSubnet(): Promise<string> {
const addresses = existingAddresses.map((org) => org.subnet!);
let subnet = findNextAvailableCidr(
const subnet = findNextAvailableCidr(
addresses,
config.getRawConfig().orgs.block_size,
config.getRawConfig().orgs.subnet_group

View File

@@ -1,6 +1,6 @@
import { MemoryStore, Store } from "express-rate-limit";
export function createStore(): Store {
let rateLimitStore: Store = new MemoryStore();
const rateLimitStore: Store = new MemoryStore();
return rateLimitStore;
}

View File

@@ -3,7 +3,6 @@ import yaml from "js-yaml";
import { configFilePath1, configFilePath2 } from "./consts";
import { z } from "zod";
import stoi from "./stoi";
import { build } from "@server/build";
const portSchema = z.number().positive().gt(0).lte(65535);
@@ -25,7 +24,13 @@ export const configSchema = z
.optional()
.default("info"),
save_logs: z.boolean().optional().default(false),
log_failed_attempts: z.boolean().optional().default(false)
log_failed_attempts: z.boolean().optional().default(false),
telmetry: z
.object({
anonymous_usage: z.boolean().optional().default(true)
})
.optional()
.default({})
}),
domains: z
.record(
@@ -128,7 +133,9 @@ export const configSchema = z
.object({
http_entrypoint: z.string().optional().default("web"),
https_entrypoint: z.string().optional().default("websecure"),
additional_middlewares: z.array(z.string()).optional()
additional_middlewares: z.array(z.string()).optional(),
cert_resolver: z.string().optional().default("letsencrypt"),
prefer_wildcard_cert: z.boolean().optional().default(false)
})
.optional()
.default({}),
@@ -157,13 +164,16 @@ export const configSchema = z
})
.optional()
.default({}),
orgs: z.object({
block_size: z.number().positive().gt(0).optional().default(24),
subnet_group: z.string().optional().default("100.90.128.0/24")
}).optional().default({
block_size: 24,
subnet_group: "100.90.128.0/24"
}),
orgs: z
.object({
block_size: z.number().positive().gt(0).optional().default(24),
subnet_group: z.string().optional().default("100.90.128.0/24")
})
.optional()
.default({
block_size: 24,
subnet_group: "100.90.128.0/24"
}),
rate_limits: z
.object({
global: z
@@ -208,7 +218,10 @@ export const configSchema = z
smtp_host: z.string().optional(),
smtp_port: portSchema.optional(),
smtp_user: z.string().optional(),
smtp_pass: z.string().optional(),
smtp_pass: z
.string()
.optional()
.transform(getEnvOrYaml("EMAIL_SMTP_PASS")),
smtp_secure: z.boolean().optional(),
smtp_tls_reject_unauthorized: z.boolean().optional(),
no_reply: z.string().email().optional()
@@ -224,9 +237,22 @@ export const configSchema = z
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()
enable_clients: z.boolean().optional().default(true)
})
.optional(),
dns: z
.object({
nameservers: z
.array(z.string().optional().optional())
.optional()
.default(["ns1.fossorial.io", "ns2.fossorial.io"]),
cname_extension: z.string().optional().default("fossorial.io")
})
.optional()
.default({
nameservers: ["ns1.fossorial.io", "ns2.fossorial.io"],
cname_extension: "fossorial.io"
})
})
.refine(
(data) => {
@@ -242,7 +268,7 @@ export const configSchema = z
{
message: "At least one domain must be defined"
}
)
);
export function readConfigFile() {
const loadConfig = (configPath: string) => {
@@ -269,7 +295,7 @@ export function readConfigFile() {
if (!environment) {
throw new Error(
"No configuration file found. Please create one. https://docs.fossorial.io/"
"No configuration file found. Please create one. https://docs.digpangolin.com/self-host/advanced/config-file"
);
}

295
server/lib/telemetry.ts Normal file
View File

@@ -0,0 +1,295 @@
import { PostHog } from "posthog-node";
import config from "./config";
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 { APP_VERSION } from "./consts";
import crypto from "crypto";
import { UserType } from "@server/types/UserTypes";
class TelemetryClient {
private client: PostHog | null = null;
private enabled: boolean;
private intervalId: NodeJS.Timeout | null = null;
constructor() {
const enabled = config.getRawConfig().app.telmetry.anonymous_usage;
this.enabled = enabled;
const dev = process.env.ENVIRONMENT !== "prod";
if (this.enabled && !dev) {
this.client = new PostHog(
"phc_QYuATSSZt6onzssWcYJbXLzQwnunIpdGGDTYhzK3VjX",
{
host: "https://digpangolin.com/relay-O7yI"
}
);
process.on("exit", () => {
this.client?.shutdown();
});
this.sendStartupEvents().catch((err) => {
logger.error("Failed to send startup telemetry:", err);
});
this.startAnalyticsInterval();
logger.info(
"Pangolin now gathers anonymous usage data to help us better understand how the software is used and guide future improvements and feature development. You can find more details, including instructions for opting out of this anonymous data collection, at: https://docs.digpangolin.com/telemetry"
);
} else if (!this.enabled && !dev) {
logger.info(
"Analytics usage statistics collection is disabled. If you enable this, you can help us make Pangolin better for everyone. Learn more at: https://docs.digpangolin.com/telemetry"
);
}
}
private startAnalyticsInterval() {
this.intervalId = setInterval(
() => {
this.collectAndSendAnalytics().catch((err) => {
logger.error("Failed to collect analytics:", err);
});
},
6 * 60 * 60 * 1000
);
this.collectAndSendAnalytics().catch((err) => {
logger.error("Failed to collect initial analytics:", err);
});
}
private anon(value: string): string {
return crypto
.createHash("sha256")
.update(value.toLowerCase())
.digest("hex");
}
private async getSystemStats() {
try {
const [sitesCount] = await db
.select({ count: count() })
.from(sites);
const [usersCount] = await db
.select({ count: count() })
.from(users);
const [usersInternalCount] = await db
.select({ count: count() })
.from(users)
.where(eq(users.type, UserType.Internal));
const [usersOidcCount] = await db
.select({ count: count() })
.from(users)
.where(eq(users.type, UserType.OIDC));
const [orgsCount] = await db.select({ count: count() }).from(orgs);
const [resourcesCount] = await db
.select({ count: count() })
.from(resources);
const [clientsCount] = await db
.select({ count: count() })
.from(clients);
const [idpCount] = await db.select({ count: count() }).from(idp);
const [onlineSitesCount] = await db
.select({ count: count() })
.from(sites)
.where(eq(sites.online, true));
const [numApiKeys] = await db
.select({ count: count() })
.from(apiKeys);
const [customRoles] = await db
.select({ count: count() })
.from(roles)
.where(notInArray(roles.name, ["Admin", "Member"]));
const adminUsers = await db
.select({ email: users.email })
.from(users)
.where(eq(users.serverAdmin, true));
const resourceDetails = await db
.select({
name: resources.name,
sso: resources.sso,
protocol: resources.protocol,
http: resources.http
})
.from(resources);
const siteDetails = await db
.select({
siteName: sites.name,
megabytesIn: sites.megabytesIn,
megabytesOut: sites.megabytesOut,
type: sites.type,
online: sites.online
})
.from(sites);
const supporterKey = config.getSupporterData();
return {
numSites: sitesCount.count,
numUsers: usersCount.count,
numUsersInternal: usersInternalCount.count,
numUsersOidc: usersOidcCount.count,
numOrganizations: orgsCount.count,
numResources: resourcesCount.count,
numClients: clientsCount.count,
numIdentityProviders: idpCount.count,
numSitesOnline: onlineSitesCount.count,
resources: resourceDetails,
adminUsers: adminUsers.map((u) => u.email),
sites: siteDetails,
appVersion: APP_VERSION,
numApiKeys: numApiKeys.count,
numCustomRoles: customRoles.count,
supporterStatus: {
valid: supporterKey?.valid || false,
tier: supporterKey?.tier || "None",
githubUsername: supporterKey?.githubUsername || null
}
};
} catch (error) {
logger.error("Failed to collect system stats:", error);
throw error;
}
}
private async sendStartupEvents() {
if (!this.enabled || !this.client) return;
const hostMeta = await getHostMeta();
if (!hostMeta) return;
const stats = await this.getSystemStats();
this.client.capture({
distinctId: hostMeta.hostMetaId,
event: "supporter_status",
properties: {
valid: stats.supporterStatus.valid,
tier: stats.supporterStatus.tier,
github_username: stats.supporterStatus.githubUsername
? this.anon(stats.supporterStatus.githubUsername)
: "None"
}
});
this.client.capture({
distinctId: hostMeta.hostMetaId,
event: "host_startup",
properties: {
host_id: hostMeta.hostMetaId,
app_version: stats.appVersion,
install_timestamp: hostMeta.createdAt
}
});
for (const email of stats.adminUsers) {
// There should only be on admin user, but just in case
if (email) {
this.client.capture({
distinctId: this.anon(email),
event: "admin_user",
properties: {
host_id: hostMeta.hostMetaId,
app_version: stats.appVersion,
hashed_email: this.anon(email)
}
});
}
}
}
private async collectAndSendAnalytics() {
if (!this.enabled || !this.client) return;
try {
const hostMeta = await getHostMeta();
if (!hostMeta) {
logger.warn(
"Telemetry: Host meta not found, skipping analytics"
);
return;
}
const stats = await this.getSystemStats();
this.client.capture({
distinctId: hostMeta.hostMetaId,
event: "system_analytics",
properties: {
app_version: stats.appVersion,
num_sites: stats.numSites,
num_users: stats.numUsers,
num_users_internal: stats.numUsersInternal,
num_users_oidc: stats.numUsersOidc,
num_organizations: stats.numOrganizations,
num_resources: stats.numResources,
num_clients: stats.numClients,
num_identity_providers: stats.numIdentityProviders,
num_sites_online: stats.numSitesOnline,
resources: stats.resources.map((r) => ({
name: this.anon(r.name),
sso_enabled: r.sso,
protocol: r.protocol,
http_enabled: r.http
})),
sites: stats.sites.map((s) => ({
site_name: this.anon(s.siteName),
megabytes_in: s.megabytesIn,
megabytes_out: s.megabytesOut,
type: s.type,
online: s.online
})),
num_api_keys: stats.numApiKeys,
num_custom_roles: stats.numCustomRoles
}
});
} catch (error) {
logger.error("Failed to send analytics:", error);
}
}
async sendTelemetry(eventName: string, properties: Record<string, any>) {
if (!this.enabled || !this.client) return;
const hostMeta = await getHostMeta();
if (!hostMeta) {
logger.warn("Telemetry: Host meta not found, skipping telemetry");
return;
}
this.client.groupIdentify({
groupType: "host_id",
groupKey: hostMeta.hostMetaId,
properties
});
}
shutdown() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
if (this.enabled && this.client) {
this.client.shutdown();
}
}
}
let telemetryClient!: TelemetryClient;
export function initTelemetryClient() {
if (!telemetryClient) {
telemetryClient = new TelemetryClient();
}
return telemetryClient;
}
export default telemetryClient;

View File

@@ -5,7 +5,7 @@ import NodeCache from "node-cache";
import { validateJWT } from "./licenseJwt";
import { count, eq } from "drizzle-orm";
import moment from "moment";
import { setHostMeta } from "@server/setup/setHostMeta";
import { setHostMeta } from "@server/lib/hostMeta";
import { encrypt, decrypt } from "@server/lib/crypto";
const keyTypes = ["HOST", "SITES"] as const;

View File

@@ -3,6 +3,7 @@ import config from "@server/lib/config";
import * as winston from "winston";
import path from "path";
import { APP_PATH } from "./lib/consts";
import telemetryClient from "./lib/telemetry";
const hformat = winston.format.printf(
({ level, label, message, timestamp, stack, ...metadata }) => {

View File

@@ -27,3 +27,4 @@ export * from "./verifyApiKeyAccess";
export * from "./verifyDomainAccess";
export * from "./verifyClientsEnabled";
export * from "./verifyUserIsOrgOwner";
export * from "./verifySiteResourceAccess";

View File

@@ -10,3 +10,4 @@ export * from "./verifyApiKeySetResourceUsers";
export * from "./verifyAccessTokenAccess";
export * from "./verifyApiKeyIsRoot";
export * from "./verifyApiKeyApiKeyAccess";
export * from "./verifyApiKeyClientAccess";

View File

@@ -35,6 +35,11 @@ export async function verifyApiKeyApiKeyAccess(
);
}
if (callerApiKey.isRoot) {
// Root keys can access any key in any org
return next();
}
const [callerApiKeyOrg] = await db
.select()
.from(apiKeyOrg)

View File

@@ -0,0 +1,91 @@
import { Request, Response, NextFunction } from "express";
import { clients, db } from "@server/db";
import { apiKeyOrg } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyApiKeyClientAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
const apiKey = req.apiKey;
const clientId = parseInt(
req.params.clientId || req.body.clientId || req.query.clientId
);
if (!apiKey) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
);
}
if (isNaN(clientId)) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid client ID")
);
}
if (apiKey.isRoot) {
// Root keys can access any key in any org
return next();
}
const client = await db
.select()
.from(clients)
.where(eq(clients.clientId, clientId))
.limit(1);
if (client.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Client with ID ${clientId} not found`
)
);
}
if (!client[0].orgId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`Client with ID ${clientId} does not have an organization ID`
)
);
}
if (!req.apiKeyOrg) {
const apiKeyOrgRes = await db
.select()
.from(apiKeyOrg)
.where(
and(
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
eq(apiKeyOrg.orgId, client[0].orgId)
)
);
req.apiKeyOrg = apiKeyOrgRes[0];
}
if (!req.apiKeyOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have access to this organization"
)
);
}
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying site access"
)
);
}
}

View File

@@ -27,6 +27,11 @@ export async function verifyApiKeyOrgAccess(
);
}
if (req.apiKey?.isRoot) {
// Root keys can access any key in any org
return next();
}
if (!req.apiKeyOrg) {
const apiKeyOrgRes = await db
.select()

View File

@@ -37,6 +37,11 @@ export async function verifyApiKeyResourceAccess(
);
}
if (apiKey.isRoot) {
// Root keys can access any key in any org
return next();
}
if (!resource.orgId) {
return next(
createHttpError(

View File

@@ -45,6 +45,11 @@ export async function verifyApiKeyRoleAccess(
);
}
if (apiKey.isRoot) {
// Root keys can access any key in any org
return next();
}
const orgIds = new Set(rolesData.map((role) => role.orgId));
for (const role of rolesData) {

View File

@@ -32,6 +32,11 @@ export async function verifyApiKeySetResourceUsers(
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs"));
}
if (apiKey.isRoot) {
// Root keys can access any key in any org
return next();
}
if (userIds.length === 0) {
return next();
}

View File

@@ -1,9 +1,6 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import {
sites,
apiKeyOrg
} from "@server/db";
import { sites, apiKeyOrg } from "@server/db";
import { and, eq, or } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
@@ -31,6 +28,11 @@ export async function verifyApiKeySiteAccess(
);
}
if (apiKey.isRoot) {
// Root keys can access any key in any org
return next();
}
const site = await db
.select()
.from(sites)

View File

@@ -66,6 +66,11 @@ export async function verifyApiKeyTargetAccess(
);
}
if (apiKey.isRoot) {
// Root keys can access any key in any org
return next();
}
if (!resource.orgId) {
return next(
createHttpError(

View File

@@ -27,6 +27,11 @@ export async function verifyApiKeyUserAccess(
);
}
if (apiKey.isRoot) {
// Root keys can access any key in any org
return next();
}
if (!req.apiKeyOrg || !req.apiKeyOrg.orgId) {
return next(
createHttpError(

View File

@@ -0,0 +1,62 @@
import { Request, Response, NextFunction } from "express";
import { db } 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";
export async function verifySiteResourceAccess(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const siteResourceId = parseInt(req.params.siteResourceId);
const siteId = parseInt(req.params.siteId);
const orgId = req.params.orgId;
if (!siteResourceId || !siteId || !orgId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Missing required parameters"
)
);
}
// 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)
))
.limit(1);
if (!siteResource) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Site resource not found"
)
);
}
// Attach the siteResource to the request for use in the next middleware/route
// @ts-ignore - Extending Request type
req.siteResource = siteResource;
next();
} catch (error) {
logger.error("Error verifying site resource access:", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying site resource access"
)
);
}
}

View File

@@ -15,7 +15,7 @@ export async function createNextServer() {
const nextServer = express();
nextServer.all("*", (req, res) => {
nextServer.all("/{*splat}", (req, res) => {
const parsedUrl = parse(req.url!, true);
return handle(req, res, parsedUrl);
});

View File

@@ -222,7 +222,7 @@ export async function listAccessTokens(
(resource) => resource.resourceId
);
let countQuery: any = db
const countQuery: any = db
.select({ count: count() })
.from(resources)
.where(inArray(resources.resourceId, accessibleResourceIds));

View File

@@ -63,15 +63,6 @@ export async function createRootApiKey(
lastChars,
isRoot: true
});
const allOrgs = await trx.select().from(orgs);
for (const org of allOrgs) {
await trx.insert(apiKeyOrg).values({
apiKeyId,
orgId: org.orgId
});
}
});
try {

View File

@@ -10,6 +10,7 @@ export * from "./resetPassword";
export * from "./requestPasswordReset";
export * from "./setServerAdmin";
export * from "./initialSetupComplete";
export * from "./validateSetupToken";
export * from "./changePassword";
export * from "./checkResourceSession";
export * from "./securityKey";

View File

@@ -106,21 +106,21 @@ export async function login(
);
}
// Check if user has security keys registered
const userSecurityKeys = await db
.select()
.from(securityKeys)
.where(eq(securityKeys.userId, existingUser.userId));
if (userSecurityKeys.length > 0) {
return response<LoginResponse>(res, {
data: { useSecurityKey: true },
success: true,
error: false,
message: "Security key authentication required",
status: HttpCode.OK
});
}
// // Check if user has security keys registered
// const userSecurityKeys = await db
// .select()
// .from(securityKeys)
// .where(eq(securityKeys.userId, existingUser.userId));
//
// if (userSecurityKeys.length > 0) {
// return response<LoginResponse>(res, {
// data: { useSecurityKey: true },
// success: true,
// error: false,
// message: "Security key authentication required",
// status: HttpCode.OK
// });
// }
if (
existingUser.twoFactorSetupRequested &&

View File

@@ -8,14 +8,15 @@ import logger from "@server/logger";
import { hashPassword } from "@server/auth/password";
import { passwordSchema } from "@server/auth/passwordSchema";
import { response } from "@server/lib";
import { db, users } from "@server/db";
import { eq } from "drizzle-orm";
import { db, users, setupTokens } from "@server/db";
import { eq, and } from "drizzle-orm";
import { UserType } from "@server/types/UserTypes";
import moment from "moment";
export const bodySchema = z.object({
email: z.string().toLowerCase().email(),
password: passwordSchema
password: passwordSchema,
setupToken: z.string().min(1, "Setup token is required")
});
export type SetServerAdminBody = z.infer<typeof bodySchema>;
@@ -39,7 +40,27 @@ export async function setServerAdmin(
);
}
const { email, password } = parsedBody.data;
const { email, password, setupToken } = parsedBody.data;
// Validate setup token
const [validToken] = await db
.select()
.from(setupTokens)
.where(
and(
eq(setupTokens.token, setupToken),
eq(setupTokens.used, false)
)
);
if (!validToken) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid or expired setup token"
)
);
}
const [existing] = await db
.select()
@@ -58,15 +79,27 @@ export async function setServerAdmin(
const passwordHash = await hashPassword(password);
const userId = generateId(15);
await db.insert(users).values({
userId: userId,
email: email,
type: UserType.Internal,
username: email,
passwordHash,
dateCreated: moment().toISOString(),
serverAdmin: true,
emailVerified: true
await db.transaction(async (trx) => {
// Mark the token as used
await trx
.update(setupTokens)
.set({
used: true,
dateUsed: moment().toISOString()
})
.where(eq(setupTokens.tokenId, validToken.tokenId));
// Create the server admin user
await trx.insert(users).values({
userId: userId,
email: email,
type: UserType.Internal,
username: email,
passwordHash,
dateCreated: moment().toISOString(),
serverAdmin: true,
emailVerified: true
});
});
return response<SetServerAdminResponse>(res, {

View File

@@ -21,15 +21,14 @@ import { hashPassword } from "@server/auth/password";
import { checkValidInvite } from "@server/auth/checkValidInvite";
import { passwordSchema } from "@server/auth/passwordSchema";
import { UserType } from "@server/types/UserTypes";
import { build } from "@server/build";
export const signupBodySchema = z.object({
email: z
.string()
.toLowerCase()
.email(),
email: z.string().toLowerCase().email(),
password: passwordSchema,
inviteToken: z.string().optional(),
inviteId: z.string().optional()
inviteId: z.string().optional(),
termsAcceptedTimestamp: z.string().nullable().optional()
});
export type SignUpBody = z.infer<typeof signupBodySchema>;
@@ -54,7 +53,8 @@ export async function signup(
);
}
const { email, password, inviteToken, inviteId } = parsedBody.data;
const { email, password, inviteToken, inviteId, termsAcceptedTimestamp } =
parsedBody.data;
const passwordHash = await hashPassword(password);
const userId = generateId(15);
@@ -161,13 +161,24 @@ export async function signup(
}
}
if (build === "saas" && !termsAcceptedTimestamp) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"You must accept the terms of service and privacy policy"
)
);
}
await db.insert(users).values({
userId: userId,
type: UserType.Internal,
username: email,
email: email,
passwordHash,
dateCreated: moment().toISOString()
dateCreated: moment().toISOString(),
termsAcceptedTimestamp: termsAcceptedTimestamp || null,
termsVersion: "1"
});
// give the user their default permissions:

View File

@@ -0,0 +1,84 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, setupTokens } from "@server/db";
import { eq, and } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
const validateSetupTokenSchema = z
.object({
token: z.string().min(1, "Token is required")
})
.strict();
export type ValidateSetupTokenResponse = {
valid: boolean;
message: string;
};
export async function validateSetupToken(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = validateSetupTokenSchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { token } = parsedBody.data;
// Find the token in the database
const [setupToken] = await db
.select()
.from(setupTokens)
.where(
and(
eq(setupTokens.token, token),
eq(setupTokens.used, false)
)
);
if (!setupToken) {
return response<ValidateSetupTokenResponse>(res, {
data: {
valid: false,
message: "Invalid or expired setup token"
},
success: true,
error: false,
message: "Token validation completed",
status: HttpCode.OK
});
}
return response<ValidateSetupTokenResponse>(res, {
data: {
valid: true,
message: "Setup token is valid"
},
success: true,
error: false,
message: "Token validation completed",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to validate setup token"
)
);
}
}

View File

@@ -52,20 +52,26 @@ export async function exchangeSession(
try {
const { requestToken, host, requestIp } = parsedBody.data;
let cleanHost = host;
// if the host ends with :port
if (cleanHost.match(/:[0-9]{1,5}$/)) {
let matched = ''+cleanHost.match(/:[0-9]{1,5}$/);
cleanHost = cleanHost.slice(0, -1*matched.length);
}
const clientIp = requestIp?.split(":")[0];
const [resource] = await db
.select()
.from(resources)
.where(eq(resources.fullDomain, host))
.where(eq(resources.fullDomain, cleanHost))
.limit(1);
if (!resource) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Resource with host ${host} not found`
`Resource with host ${cleanHost} not found`
)
);
}

View File

@@ -121,11 +121,10 @@ export async function verifyResourceSession(
logger.debug("Client IP:", { clientIp });
let cleanHost = host;
// if the host ends with :443 or :80 remove it
if (cleanHost.endsWith(":443")) {
cleanHost = cleanHost.slice(0, -4);
} else if (cleanHost.endsWith(":80")) {
cleanHost = cleanHost.slice(0, -3);
// if the host ends with :port, strip it
if (cleanHost.match(/:[0-9]{1,5}$/)) {
let matched = ''+cleanHost.match(/:[0-9]{1,5}$/);
cleanHost = cleanHost.slice(0, -1*matched.length);
}
const resourceCacheKey = `resource:${cleanHost}`;

View File

@@ -144,14 +144,16 @@ export async function createClient(
const subnetExistsClients = await db
.select()
.from(clients)
.where(eq(clients.subnet, updatedSubnet))
.where(
and(eq(clients.subnet, updatedSubnet), eq(clients.orgId, orgId))
)
.limit(1);
if (subnetExistsClients.length > 0) {
return next(
createHttpError(
HttpCode.CONFLICT,
`Subnet ${subnet} already exists`
`Subnet ${updatedSubnet} already exists in clients`
)
);
}
@@ -159,14 +161,16 @@ export async function createClient(
const subnetExistsSites = await db
.select()
.from(sites)
.where(eq(sites.address, updatedSubnet))
.where(
and(eq(sites.address, updatedSubnet), eq(sites.orgId, orgId))
)
.limit(1);
if (subnetExistsSites.length > 0) {
return next(
createHttpError(
HttpCode.CONFLICT,
`Subnet ${subnet} already exists`
`Subnet ${updatedSubnet} already exists in sites`
)
);
}

View File

@@ -14,32 +14,32 @@ import { OpenAPITags, registry } from "@server/openApi";
const getClientSchema = z
.object({
clientId: z.string().transform(stoi).pipe(z.number().int().positive()),
orgId: z.string().optional()
orgId: z.string()
})
.strict();
async function query(clientId: number) {
async function query(clientId: number, orgId: string) {
// Get the client
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, clientId))
.where(and(eq(clients.clientId, clientId), eq(clients.orgId, orgId)))
.limit(1);
if (!client) {
return null;
}
// Get the siteIds associated with this client
const sites = await db
.select({ siteId: clientSites.siteId })
.from(clientSites)
.where(eq(clientSites.clientId, clientId));
// Add the siteIds to the client object
return {
...client,
siteIds: sites.map(site => site.siteId)
siteIds: sites.map((site) => site.siteId)
};
}
@@ -75,9 +75,9 @@ export async function getClient(
);
}
const { clientId } = parsedParams.data;
const { clientId, orgId } = parsedParams.data;
const client = await query(clientId);
const client = await query(clientId, orgId);
if (!client) {
return next(
@@ -98,4 +98,4 @@ export async function getClient(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
}

View File

@@ -23,7 +23,7 @@ const pickClientDefaultsSchema = z
registry.registerPath({
method: "get",
path: "/site/{siteId}/pick-client-defaults",
path: "/org/{orgId}/pick-client-defaults",
description: "Return pre-requisite data for creating a client.",
tags: [OpenAPITags.Client, OpenAPITags.Site],
request: {

View File

@@ -0,0 +1,39 @@
import { sendToClient } from "../ws";
export async function addTargets(
newtId: string,
destinationIp: string,
destinationPort: number,
protocol: string,
port: number | null = null
) {
const target = `${port ? port + ":" : ""}${
destinationIp
}:${destinationPort}`;
await sendToClient(newtId, {
type: `newt/wg/${protocol}/add`,
data: {
targets: [target] // We can only use one target for WireGuard right now
}
});
}
export async function removeTargets(
newtId: string,
destinationIp: string,
destinationPort: number,
protocol: string,
port: number | null = null
) {
const target = `${port ? port + ":" : ""}${
destinationIp
}:${destinationPort}`;
await sendToClient(newtId, {
type: `newt/wg/${protocol}/remove`,
data: {
targets: [target] // We can only use one target for WireGuard right now
}
});
}

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { db, exitNodes, sites } from "@server/db";
import { clients, clientSites } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -17,6 +17,7 @@ import {
addPeer as olmAddPeer,
deletePeer as olmDeletePeer
} from "../olm/peers";
import axios from "axios";
const updateClientParamsSchema = z
.object({
@@ -53,6 +54,11 @@ registry.registerPath({
responses: {}
});
interface PeerDestination {
destinationIP: string;
destinationPort: number;
}
export async function updateClient(
req: Request,
res: Response,
@@ -123,16 +129,38 @@ export async function updateClient(
`Adding ${sitesAdded.length} new sites to client ${client.clientId}`
);
for (const siteId of sitesAdded) {
if (!client.subnet || !client.pubKey || !client.endpoint) {
logger.debug("Client subnet, pubKey or endpoint is not set");
if (!client.subnet || !client.pubKey) {
logger.debug(
"Client subnet, pubKey or endpoint is not set"
);
continue;
}
// TODO: WE NEED TO HANDLE THIS BETTER. RIGHT NOW WE ARE JUST GUESSING BASED ON THE OTHER SITES
// BUT REALLY WE NEED TO TRACK THE USERS PREFERENCE THAT THEY CHOSE IN THE CLIENTS
const isRelayed = true;
// get the clientsite
const [clientSite] = await db
.select()
.from(clientSites)
.where(and(
eq(clientSites.clientId, client.clientId),
eq(clientSites.siteId, siteId)
))
.limit(1);
if (!clientSite || !clientSite.endpoint) {
logger.debug("Client site is missing or has no endpoint");
continue;
}
const site = await newtAddPeer(siteId, {
publicKey: client.pubKey,
allowedIps: [`${client.subnet.split("/")[0]}/32`], // we want to only allow from that client
endpoint: client.endpoint
endpoint: isRelayed ? "" : clientSite.endpoint
});
if (!site) {
logger.debug("Failed to add peer to newt - missing site");
continue;
@@ -142,12 +170,49 @@ export async function updateClient(
logger.debug("Site endpoint or publicKey is not set");
continue;
}
let endpoint;
if (isRelayed) {
if (!site.exitNodeId) {
logger.warn(
`Site ${site.siteId} has no exit node, skipping`
);
return null;
}
// get the exit node for the site
const [exitNode] = await db
.select()
.from(exitNodes)
.where(eq(exitNodes.exitNodeId, site.exitNodeId))
.limit(1);
if (!exitNode) {
logger.warn(
`Exit node not found for site ${site.siteId}`
);
return null;
}
endpoint = `${exitNode.endpoint}:21820`;
} else {
if (!endpoint) {
logger.warn(
`Site ${site.siteId} has no endpoint, skipping`
);
return null;
}
endpoint = site.endpoint;
}
await olmAddPeer(client.clientId, {
siteId: siteId,
endpoint: site.endpoint,
siteId: site.siteId,
endpoint: endpoint,
publicKey: site.publicKey,
serverIP: site.address,
serverPort: site.listenPort
serverPort: site.listenPort,
remoteSubnets: site.remoteSubnets
});
}
@@ -170,7 +235,11 @@ export async function updateClient(
logger.debug("Site endpoint or publicKey is not set");
continue;
}
await olmDeletePeer(client.clientId, site.siteId, site.publicKey);
await olmDeletePeer(
client.clientId,
site.siteId,
site.publicKey
);
}
}
@@ -201,6 +270,114 @@ export async function updateClient(
}
}
// get all sites for this client and join with exit nodes with site.exitNodeId
const sitesData = await db
.select()
.from(sites)
.innerJoin(
clientSites,
eq(sites.siteId, clientSites.siteId)
)
.leftJoin(
exitNodes,
eq(sites.exitNodeId, exitNodes.exitNodeId)
)
.where(eq(clientSites.clientId, client.clientId));
let exitNodeDestinations: {
reachableAt: string;
sourceIp: string;
sourcePort: number;
destinations: PeerDestination[];
}[] = [];
for (const site of sitesData) {
if (!site.sites.subnet) {
logger.warn(
`Site ${site.sites.siteId} has no subnet, skipping`
);
continue;
}
if (!site.clientSites.endpoint) {
logger.warn(
`Site ${site.sites.siteId} has no endpoint, skipping`
);
continue;
}
// find the destinations in the array
let destinations = exitNodeDestinations.find(
(d) => d.reachableAt === site.exitNodes?.reachableAt
);
if (!destinations) {
destinations = {
reachableAt: site.exitNodes?.reachableAt || "",
sourceIp: site.clientSites.endpoint.split(":")[0] || "",
sourcePort: parseInt(site.clientSites.endpoint.split(":")[1]) || 0,
destinations: [
{
destinationIP:
site.sites.subnet.split("/")[0],
destinationPort: site.sites.listenPort || 0
}
]
};
} else {
// add to the existing destinations
destinations.destinations.push({
destinationIP: site.sites.subnet.split("/")[0],
destinationPort: site.sites.listenPort || 0
});
}
// update it in the array
exitNodeDestinations = exitNodeDestinations.filter(
(d) => d.reachableAt !== site.exitNodes?.reachableAt
);
exitNodeDestinations.push(destinations);
}
for (const destination of exitNodeDestinations) {
try {
logger.info(
`Updating destinations for exit node at ${destination.reachableAt}`
);
const payload = {
sourceIp: destination.sourceIp,
sourcePort: destination.sourcePort,
destinations: destination.destinations
};
logger.info(
`Payload for update-destinations: ${JSON.stringify(payload, null, 2)}`
);
const response = await axios.post(
`${destination.reachableAt}/update-destinations`,
payload,
{
headers: {
"Content-Type": "application/json"
}
}
);
logger.info("Destinations updated:", {
peer: response.data.status
});
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error(
`Error updating destinations (can Pangolin see Gerbil HTTP API?) for exit node at ${destination.reachableAt} (status: ${error.response?.status}): ${JSON.stringify(error.response?.data, null, 2)}`
);
} else {
logger.error(
`Error updating destinations for exit node at ${destination.reachableAt}: ${error}`
);
}
}
}
// Fetch the updated client
const [updatedClient] = await trx
.select()

View File

@@ -11,6 +11,7 @@ import { generateId } from "@server/auth/sessions/app";
import { eq, and } from "drizzle-orm";
import { isValidDomain } from "@server/lib/validators";
import { build } from "@server/build";
import config from "@server/lib/config";
const paramsSchema = z
.object({
@@ -29,6 +30,7 @@ export type CreateDomainResponse = {
domainId: string;
nsRecords?: string[];
cnameRecords?: { baseDomain: string; value: string }[];
aRecords?: { baseDomain: string; value: string }[];
txtRecords?: { baseDomain: string; value: string }[];
};
@@ -97,6 +99,7 @@ export async function createOrgDomain(
}
let numOrgDomains: OrgDomains[] | undefined;
let aRecords: CreateDomainResponse["aRecords"];
let cnameRecords: CreateDomainResponse["cnameRecords"];
let txtRecords: CreateDomainResponse["txtRecords"];
let nsRecords: CreateDomainResponse["nsRecords"];
@@ -226,20 +229,20 @@ export async function createOrgDomain(
// TODO: This needs to be cross region and not hardcoded
if (type === "ns") {
nsRecords = ["ns-east.fossorial.io", "ns-west.fossorial.io"];
nsRecords = config.getRawConfig().dns.nameservers as string[];
} else if (type === "cname") {
cnameRecords = [
{
value: `${domainId}.cname.fossorial.io`,
value: `${domainId}.${config.getRawConfig().dns.cname_extension}`,
baseDomain: baseDomain
},
{
value: `_acme-challenge.${domainId}.cname.fossorial.io`,
value: `_acme-challenge.${domainId}.${config.getRawConfig().dns.cname_extension}`,
baseDomain: `_acme-challenge.${baseDomain}`
}
];
} else if (type === "wildcard") {
cnameRecords = [
aRecords = [
{
value: `Server IP Address`,
baseDomain: `*.${baseDomain}`
@@ -271,7 +274,8 @@ export async function createOrgDomain(
domainId: returned.domainId,
cnameRecords,
txtRecords,
nsRecords
nsRecords,
aRecords
},
success: true,
error: false,

View File

@@ -9,6 +9,7 @@ import * as user from "./user";
import * as auth from "./auth";
import * as role from "./role";
import * as client from "./client";
import * as siteResource from "./siteResource";
import * as supporterKey from "./supporterKey";
import * as accessToken from "./accessToken";
import * as idp from "./idp";
@@ -34,7 +35,8 @@ import {
verifyDomainAccess,
verifyClientsEnabled,
verifyUserHasAction,
verifyUserIsOrgOwner
verifyUserIsOrgOwner,
verifySiteResourceAccess
} from "@server/middlewares";
import { createStore } from "@server/lib/rateLimitStore";
import { ActionsEnum } from "@server/auth/actions";
@@ -213,9 +215,60 @@ authenticated.get(
site.listContainers
);
// Site Resource endpoints
authenticated.put(
"/org/:orgId/site/:siteId/resource",
verifyOrgAccess,
verifySiteAccess,
verifyUserHasAction(ActionsEnum.createSiteResource),
siteResource.createSiteResource
);
authenticated.get(
"/org/:orgId/site/:siteId/resources",
verifyOrgAccess,
verifySiteAccess,
verifyUserHasAction(ActionsEnum.listSiteResources),
siteResource.listSiteResources
);
authenticated.get(
"/org/:orgId/site-resources",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.listSiteResources),
siteResource.listAllSiteResourcesByOrg
);
authenticated.get(
"/org/:orgId/site/:siteId/resource/:siteResourceId",
verifyOrgAccess,
verifySiteAccess,
verifySiteResourceAccess,
verifyUserHasAction(ActionsEnum.getSiteResource),
siteResource.getSiteResource
);
authenticated.post(
"/org/:orgId/site/:siteId/resource/:siteResourceId",
verifyOrgAccess,
verifySiteAccess,
verifySiteResourceAccess,
verifyUserHasAction(ActionsEnum.updateSiteResource),
siteResource.updateSiteResource
);
authenticated.delete(
"/org/:orgId/site/:siteId/resource/:siteResourceId",
verifyOrgAccess,
verifySiteAccess,
verifySiteResourceAccess,
verifyUserHasAction(ActionsEnum.deleteSiteResource),
siteResource.deleteSiteResource
);
authenticated.put(
"/org/:orgId/resource",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.createResource),
resource.createResource
);
@@ -233,6 +286,12 @@ authenticated.get(
resource.listResources
);
authenticated.get(
"/org/:orgId/user-resources",
verifyOrgAccess,
resource.getUserResources
);
authenticated.get(
"/org/:orgId/domains",
verifyOrgAccess,
@@ -391,28 +450,6 @@ authenticated.post(
user.addUserRole
);
// authenticated.put(
// "/role/:roleId/site",
// verifyRoleAccess,
// verifyUserInRole,
// verifyUserHasAction(ActionsEnum.addRoleSite),
// role.addRoleSite
// );
// authenticated.delete(
// "/role/:roleId/site",
// verifyRoleAccess,
// verifyUserInRole,
// verifyUserHasAction(ActionsEnum.removeRoleSite),
// role.removeRoleSite
// );
// authenticated.get(
// "/role/:roleId/sites",
// verifyRoleAccess,
// verifyUserInRole,
// verifyUserHasAction(ActionsEnum.listRoleSites),
// role.listRoleSites
// );
authenticated.post(
"/resource/:resourceId/roles",
verifyResourceAccess,
@@ -457,13 +494,6 @@ authenticated.get(
resource.getResourceWhitelist
);
authenticated.post(
`/resource/:resourceId/transfer`,
verifyResourceAccess,
verifyUserHasAction(ActionsEnum.updateResource),
resource.transferResource
);
authenticated.post(
`/resource/:resourceId/access-token`,
verifyResourceAccess,
@@ -620,8 +650,6 @@ authenticated.post(
authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp);
authenticated.get("/idp", verifyUserIsServerAdmin, idp.listIdps);
authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp);
authenticated.put(
@@ -1029,6 +1057,7 @@ authRouter.post("/idp/:idpId/oidc/validate-callback", idp.validateOidcCallback);
authRouter.put("/set-server-admin", auth.setServerAdmin);
authRouter.get("/initial-setup-complete", auth.initialSetupComplete);
authRouter.post("/validate-setup-token", auth.validateSetupToken);
// Security Key routes
authRouter.post(

View File

@@ -48,7 +48,7 @@ export async function getAllRelays(
}
// Fetch exit node
let [exitNode] = await db.select().from(exitNodes).where(eq(exitNodes.publicKey, publicKey));
const [exitNode] = await db.select().from(exitNodes).where(eq(exitNodes.publicKey, publicKey));
if (!exitNode) {
return next(createHttpError(HttpCode.NOT_FOUND, "Exit node not found"));
}
@@ -63,7 +63,7 @@ export async function getAllRelays(
}
// Initialize mappings object for multi-peer support
let mappings: { [key: string]: ProxyMapping } = {};
const mappings: { [key: string]: ProxyMapping } = {};
// Process each site
for (const site of sitesRes) {
@@ -78,19 +78,13 @@ export async function getAllRelays(
.where(eq(clientSites.siteId, site.siteId));
for (const clientSite of clientSitesRes) {
// Get client information
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, clientSite.clientId));
if (!client || !client.endpoint) {
if (!clientSite.endpoint) {
continue;
}
// Add this site as a destination for the client
if (!mappings[client.endpoint]) {
mappings[client.endpoint] = { destinations: [] };
if (!mappings[clientSite.endpoint]) {
mappings[clientSite.endpoint] = { destinations: [] };
}
// Add site as a destination for this client
@@ -100,13 +94,13 @@ export async function getAllRelays(
};
// Check if this destination is already in the array to avoid duplicates
const isDuplicate = mappings[client.endpoint].destinations.some(
const isDuplicate = mappings[clientSite.endpoint].destinations.some(
dest => dest.destinationIP === destination.destinationIP &&
dest.destinationPort === destination.destinationPort
);
if (!isDuplicate) {
mappings[client.endpoint].destinations.push(destination);
mappings[clientSite.endpoint].destinations.push(destination);
}
}

View File

@@ -112,7 +112,7 @@ export async function getConfig(
)
);
let peers = await Promise.all(
const peers = await Promise.all(
sitesRes.map(async (site) => {
if (site.type === "wireguard") {
return {

View File

@@ -1,15 +1,24 @@
import axios from 'axios';
import logger from '@server/logger';
import axios from "axios";
import logger from "@server/logger";
import { db } from "@server/db";
import { exitNodes } from '@server/db';
import { eq } from 'drizzle-orm';
import { exitNodes } from "@server/db";
import { eq } from "drizzle-orm";
export async function addPeer(exitNodeId: number, peer: {
publicKey: string;
allowedIps: string[];
}) {
const [exitNode] = await db.select().from(exitNodes).where(eq(exitNodes.exitNodeId, exitNodeId)).limit(1);
export async function addPeer(
exitNodeId: number,
peer: {
publicKey: string;
allowedIps: string[];
}
) {
logger.info(
`Adding peer with public key ${peer.publicKey} to exit node ${exitNodeId}`
);
const [exitNode] = await db
.select()
.from(exitNodes)
.where(eq(exitNodes.exitNodeId, exitNodeId))
.limit(1);
if (!exitNode) {
throw new Error(`Exit node with ID ${exitNodeId} not found`);
}
@@ -18,24 +27,40 @@ export async function addPeer(exitNodeId: number, peer: {
}
try {
const response = await axios.post(`${exitNode.reachableAt}/peer`, peer, {
headers: {
'Content-Type': 'application/json',
const response = await axios.post(
`${exitNode.reachableAt}/peer`,
peer,
{
headers: {
"Content-Type": "application/json"
}
}
});
);
logger.info('Peer added successfully:', { peer: response.data.status });
logger.info("Peer added successfully:", { peer: response.data.status });
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(`Error communicating with Gerbil. Make sure Pangolin can reach the Gerbil HTTP API: ${error.response?.status}`);
logger.error(
`Error adding peer (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}`
);
} else {
logger.error(
`Error adding peer for exit node at ${exitNode.reachableAt}: ${error}`
);
}
throw error;
}
}
export async function deletePeer(exitNodeId: number, publicKey: string) {
const [exitNode] = await db.select().from(exitNodes).where(eq(exitNodes.exitNodeId, exitNodeId)).limit(1);
logger.info(
`Deleting peer with public key ${publicKey} from exit node ${exitNodeId}`
);
const [exitNode] = await db
.select()
.from(exitNodes)
.where(eq(exitNodes.exitNodeId, exitNodeId))
.limit(1);
if (!exitNode) {
throw new Error(`Exit node with ID ${exitNodeId} not found`);
}
@@ -43,13 +68,20 @@ export async function deletePeer(exitNodeId: number, publicKey: string) {
throw new Error(`Exit node with ID ${exitNodeId} is not reachable`);
}
try {
const response = await axios.delete(`${exitNode.reachableAt}/peer?public_key=${encodeURIComponent(publicKey)}`);
logger.info('Peer deleted successfully:', response.data.status);
const response = await axios.delete(
`${exitNode.reachableAt}/peer?public_key=${encodeURIComponent(publicKey)}`
);
logger.info("Peer deleted successfully:", response.data.status);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(`Error communicating with Gerbil. Make sure Pangolin can reach the Gerbil HTTP API: ${error.response?.status}`);
logger.error(
`Error deleting peer (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}`
);
} else {
logger.error(
`Error deleting peer for exit node at ${exitNode.reachableAt}: ${error}`
);
}
throw error;
}
}

View File

@@ -31,7 +31,7 @@ export const receiveBandwidth = async (
const currentTime = new Date();
const oneMinuteAgo = new Date(currentTime.getTime() - 60000); // 1 minute ago
logger.debug(`Received data: ${JSON.stringify(bandwidthData)}`);
// logger.debug(`Received data: ${JSON.stringify(bandwidthData)}`);
await db.transaction(async (trx) => {
// First, handle sites that are actively reporting bandwidth

View File

@@ -1,14 +1,24 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { clients, newts, olms, Site, sites, clientSites } from "@server/db";
import {
clients,
newts,
olms,
Site,
sites,
clientSites,
exitNodes,
ExitNode
} from "@server/db";
import { db } from "@server/db";
import { eq } from "drizzle-orm";
import { eq, and } from "drizzle-orm";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { validateNewtSessionToken } from "@server/auth/sessions/newt";
import { validateOlmSessionToken } from "@server/auth/sessions/olm";
import axios from "axios";
// Define Zod schema for request validation
const updateHolePunchSchema = z.object({
@@ -17,7 +27,9 @@ const updateHolePunchSchema = z.object({
token: z.string(),
ip: z.string(),
port: z.number(),
timestamp: z.number()
timestamp: z.number(),
reachableAt: z.string().optional(),
publicKey: z.string().optional()
});
// New response type with multi-peer destination support
@@ -43,14 +55,24 @@ export async function updateHolePunch(
);
}
const { olmId, newtId, ip, port, timestamp, token } = parsedParams.data;
const {
olmId,
newtId,
ip,
port,
timestamp,
token,
reachableAt,
publicKey
} = parsedParams.data;
let currentSiteId: number | undefined;
let destinations: PeerDestination[] = [];
if (olmId) {
logger.debug(`Got hole punch with ip: ${ip}, port: ${port} for olmId: ${olmId}`);
logger.debug(
`Got hole punch with ip: ${ip}, port: ${port} for olmId: ${olmId}${publicKey ? ` with exit node publicKey: ${publicKey}` : ""}`
);
const { session, olm: olmSession } =
await validateOlmSessionToken(token);
@@ -61,7 +83,9 @@ export async function updateHolePunch(
}
if (olmId !== olmSession.olmId) {
logger.warn(`Olm ID mismatch: ${olmId} !== ${olmSession.olmId}`);
logger.warn(
`Olm ID mismatch: ${olmId} !== ${olmSession.olmId}`
);
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Unauthorized")
);
@@ -82,12 +106,64 @@ export async function updateHolePunch(
const [client] = await db
.update(clients)
.set({
endpoint: `${ip}:${port}`,
lastHolePunch: timestamp
})
.where(eq(clients.clientId, olm.clientId))
.returning();
let exitNode: ExitNode | undefined;
if (publicKey) {
// Get the exit node by public key
[exitNode] = await db
.select()
.from(exitNodes)
.where(eq(exitNodes.publicKey, publicKey));
} else {
// FOR BACKWARDS COMPATIBILITY IF GERBIL IS STILL =<1.1.0
[exitNode] = await db.select().from(exitNodes).limit(1);
}
if (!exitNode) {
logger.warn(`Exit node not found for publicKey: ${publicKey}`);
return next(
createHttpError(HttpCode.NOT_FOUND, "Exit node not found")
);
}
// Get sites that are on this specific exit node and connected to this client
const sitesOnExitNode = await db
.select({ siteId: sites.siteId, subnet: sites.subnet, listenPort: sites.listenPort })
.from(sites)
.innerJoin(clientSites, eq(sites.siteId, clientSites.siteId))
.where(
and(
eq(sites.exitNodeId, exitNode.exitNodeId),
eq(clientSites.clientId, olm.clientId)
)
);
// Update clientSites for each site on this exit node
for (const site of sitesOnExitNode) {
logger.debug(
`Updating site ${site.siteId} on exit node with publicKey: ${publicKey}`
);
await db
.update(clientSites)
.set({
endpoint: `${ip}:${port}`
})
.where(
and(
eq(clientSites.clientId, olm.clientId),
eq(clientSites.siteId, site.siteId)
)
);
}
logger.debug(
`Updated ${sitesOnExitNode.length} sites on exit node with publicKey: ${publicKey}`
);
if (!client) {
logger.warn(`Client not found for olm: ${olmId}`);
return next(
@@ -95,29 +171,9 @@ export async function updateHolePunch(
);
}
// Get all sites that this client is connected to
const clientSitePairs = await db
.select()
.from(clientSites)
.where(eq(clientSites.clientId, client.clientId));
if (clientSitePairs.length === 0) {
logger.warn(`No sites found for client: ${client.clientId}`);
return next(
createHttpError(HttpCode.NOT_FOUND, "No sites found for client")
);
}
// Get all sites details
const siteIds = clientSitePairs.map(pair => pair.siteId);
for (const siteId of siteIds) {
const [site] = await db
.select()
.from(sites)
.where(eq(sites.siteId, siteId));
if (site && site.subnet && site.listenPort) {
// Create a list of the destinations from the sites
for (const site of sitesOnExitNode) {
if (site.subnet && site.listenPort) {
destinations.push({
destinationIP: site.subnet.split("/")[0],
destinationPort: site.listenPort
@@ -126,6 +182,10 @@ export async function updateHolePunch(
}
} else if (newtId) {
logger.debug(
`Got hole punch with ip: ${ip}, port: ${port} for newtId: ${newtId}`
);
const { session, newt: newtSession } =
await validateNewtSessionToken(token);
@@ -136,7 +196,9 @@ export async function updateHolePunch(
}
if (newtId !== newtSession.newtId) {
logger.warn(`Newt ID mismatch: ${newtId} !== ${newtSession.newtId}`);
logger.warn(
`Newt ID mismatch: ${newtId} !== ${newtSession.newtId}`
);
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Unauthorized")
);
@@ -165,7 +227,7 @@ export async function updateHolePunch(
})
.where(eq(sites.siteId, newt.siteId))
.returning();
if (!updatedSite || !updatedSite.subnet) {
logger.warn(`Site not found: ${newt.siteId}`);
return next(
@@ -174,49 +236,50 @@ export async function updateHolePunch(
}
// Find all clients that connect to this site
const sitesClientPairs = await db
.select()
.from(clientSites)
.where(eq(clientSites.siteId, newt.siteId));
// const sitesClientPairs = await db
// .select()
// .from(clientSites)
// .where(eq(clientSites.siteId, newt.siteId));
// THE NEWT IS NOT SENDING RAW WG TO THE GERBIL SO IDK IF WE REALLY NEED THIS - REMOVING
// Get client details for each client
for (const pair of sitesClientPairs) {
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, pair.clientId));
if (client && client.endpoint) {
const [host, portStr] = client.endpoint.split(':');
if (host && portStr) {
destinations.push({
destinationIP: host,
destinationPort: parseInt(portStr, 10)
});
}
}
}
// for (const pair of sitesClientPairs) {
// const [client] = await db
// .select()
// .from(clients)
// .where(eq(clients.clientId, pair.clientId));
// if (client && client.endpoint) {
// const [host, portStr] = client.endpoint.split(':');
// if (host && portStr) {
// destinations.push({
// destinationIP: host,
// destinationPort: parseInt(portStr, 10)
// });
// }
// }
// }
// If this is a newt/site, also add other sites in the same org
// if (updatedSite.orgId) {
// const orgSites = await db
// .select()
// .from(sites)
// .where(eq(sites.orgId, updatedSite.orgId));
// for (const site of orgSites) {
// // Don't add the current site to the destinations
// if (site.siteId !== currentSiteId && site.subnet && site.endpoint && site.listenPort) {
// const [host, portStr] = site.endpoint.split(':');
// if (host && portStr) {
// destinations.push({
// destinationIP: host,
// destinationPort: site.listenPort
// });
// }
// }
// }
// }
// if (updatedSite.orgId) {
// const orgSites = await db
// .select()
// .from(sites)
// .where(eq(sites.orgId, updatedSite.orgId));
// for (const site of orgSites) {
// // Don't add the current site to the destinations
// if (site.siteId !== currentSiteId && site.subnet && site.endpoint && site.listenPort) {
// const [host, portStr] = site.endpoint.split(':');
// if (host && portStr) {
// destinations.push({
// destinationIP: host,
// destinationPort: site.listenPort
// });
// }
// }
// }
// }
}
// if (destinations.length === 0) {
@@ -226,6 +289,10 @@ export async function updateHolePunch(
// return next(createHttpError(HttpCode.NOT_FOUND, "No peer destinations found"));
// }
logger.debug(
`Returning ${destinations.length} peer destinations for olmId: ${olmId} or newtId: ${newtId}: ${JSON.stringify(destinations, null, 2)}`
);
// Return the new multi-peer structure
return res.status(HttpCode.OK).send({
destinations: destinations
@@ -239,4 +306,4 @@ export async function updateHolePunch(
)
);
}
}
}

View File

@@ -68,7 +68,7 @@ export async function createOidcIdp(
);
}
let {
const {
clientId,
clientSecret,
authUrl,

View File

@@ -85,7 +85,7 @@ export async function updateOidcIdp(
}
const { idpId } = parsedParams.data;
let {
const {
clientId,
clientSecret,
authUrl,

View File

@@ -162,6 +162,12 @@ export async function validateOidcCallback(
);
}
logger.debug("State verified", {
urL: ensureTrailingSlash(existingIdp.idpOidcConfig.tokenUrl),
expectedState,
state
});
const tokens = await client.validateAuthorizationCode(
ensureTrailingSlash(existingIdp.idpOidcConfig.tokenUrl),
code,
@@ -232,7 +238,7 @@ export async function validateOidcCallback(
const defaultRoleMapping = existingIdp.idp.defaultRoleMapping;
const defaultOrgMapping = existingIdp.idp.defaultOrgMapping;
let userOrgInfo: { orgId: string; roleId: number }[] = [];
const userOrgInfo: { orgId: string; roleId: number }[] = [];
for (const org of allOrgs) {
const [idpOrgRes] = await db
.select()
@@ -308,7 +314,7 @@ export async function validateOidcCallback(
let existingUserId = existingUser?.userId;
let orgUserCounts: { orgId: string; userCount: number }[] = [];
const orgUserCounts: { orgId: string; userCount: number }[] = [];
// sync the user with the orgs and roles
await db.transaction(async (trx) => {

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