mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-29 22:30:51 +00:00
Compare commits
392 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50022c9fc8 | ||
|
|
e0b76ffebc | ||
|
|
be5a9a840c | ||
|
|
6e5f429e0a | ||
|
|
e9d9d6e2f4 | ||
|
|
b4a57e630c | ||
|
|
1062e33dc8 | ||
|
|
0e14441f73 | ||
|
|
a6a909ae4f | ||
|
|
2b4a39e64c | ||
|
|
82b4921602 | ||
|
|
4229324a5d | ||
|
|
34d3ca9c51 | ||
|
|
9bd7002917 | ||
|
|
ebed9f7a68 | ||
|
|
5d34bd82c0 | ||
|
|
8bcb2b3b0f | ||
|
|
32ba17cf91 | ||
|
|
704ded4410 | ||
|
|
88277976c6 | ||
|
|
cb95f02912 | ||
|
|
928b406359 | ||
|
|
4757c7db8c | ||
|
|
5df87641a1 | ||
|
|
04077c53fd | ||
|
|
574be52b84 | ||
|
|
a66613c5ca | ||
|
|
01b3b19715 | ||
|
|
60d8831399 | ||
|
|
5ff5660db3 | ||
|
|
d62c359452 | ||
|
|
ec0b6b64fe | ||
|
|
c53eac76f8 | ||
|
|
49cb2ae260 | ||
|
|
77796e8a75 | ||
|
|
49f0f6ec7d | ||
|
|
2c273a85d8 | ||
|
|
8273554a1c | ||
|
|
ad8ab63fd5 | ||
|
|
7de0761329 | ||
|
|
907dab7d05 | ||
|
|
2907f22200 | ||
|
|
7bbe1b2dbe | ||
|
|
099513072c | ||
|
|
7de8bb00e7 | ||
|
|
12d44696e8 | ||
|
|
25cef26251 | ||
|
|
dceb398695 | ||
|
|
f60599abd3 | ||
|
|
44f8098e4a | ||
|
|
747979f939 | ||
|
|
b3083ae779 | ||
|
|
67580a8b69 | ||
|
|
291c7aaf0b | ||
|
|
1a098eecf6 | ||
|
|
0a05bdba1d | ||
|
|
37bfc07ffb | ||
|
|
eae3ab2dc1 | ||
|
|
1665bf6515 | ||
|
|
0383ffb7f3 | ||
|
|
a0d6646e49 | ||
|
|
254b3a0fc8 | ||
|
|
21743e5a23 | ||
|
|
0550924e08 | ||
|
|
7867302be5 | ||
|
|
14815b388d | ||
|
|
92cc82220e | ||
|
|
da1fae6016 | ||
|
|
34002470a5 | ||
|
|
49f84bccad | ||
|
|
4bcb4a1590 | ||
|
|
378de19f41 | ||
|
|
ffe2512734 | ||
|
|
b4be620a5b | ||
|
|
ac8b546393 | ||
|
|
9bdf31ee97 | ||
|
|
c29cd05db8 | ||
|
|
cd34820138 | ||
|
|
d207318494 | ||
|
|
117062f1d1 | ||
|
|
9d561ba94d | ||
|
|
97fcaed9b4 | ||
|
|
5e53ea3607 | ||
|
|
7dc74cb61b | ||
|
|
fbefcfedb9 | ||
|
|
36c0d9aba2 | ||
|
|
8c8a981452 | ||
|
|
7dd586e31d | ||
|
|
366a31b41b | ||
|
|
f09557d73c | ||
|
|
33a2ac402c | ||
|
|
632333c49f | ||
|
|
c8bea4d7de | ||
|
|
c1d75d32c2 | ||
|
|
b805daec51 | ||
|
|
af2088df4e | ||
|
|
3b8d1f40a7 | ||
|
|
8355d3664e | ||
|
|
83a696f743 | ||
|
|
7ca507b1ce | ||
|
|
609435328e | ||
|
|
d771317e3f | ||
|
|
d548563e65 | ||
|
|
f07cd8aee3 | ||
|
|
48963f24df | ||
|
|
7bf98c0c40 | ||
|
|
e73383cc79 | ||
|
|
79ce93d578 | ||
|
|
200a7fcd40 | ||
|
|
e043d0e654 | ||
|
|
21ce678e5b | ||
|
|
5c94887949 | ||
|
|
69a9bcb3da | ||
|
|
2fea091e1f | ||
|
|
24314a103f | ||
|
|
b56db41d0b | ||
|
|
825bff5d60 | ||
|
|
f9184cf489 | ||
|
|
5c04b1e14a | ||
|
|
2c96eb7851 | ||
|
|
67ba225003 | ||
|
|
04ecf41c5a | ||
|
|
6600de7320 | ||
|
|
f7b82f0a7a | ||
|
|
65bdb232f4 | ||
|
|
200e3af384 | ||
|
|
aabfa91f80 | ||
|
|
e5468a7391 | ||
|
|
d5a11edd0c | ||
|
|
fcc86b07ba | ||
|
|
74d2527af5 | ||
|
|
50cf284273 | ||
|
|
aaddde0a9b | ||
|
|
ac87345b7a | ||
|
|
23079d9ac0 | ||
|
|
b573d63648 | ||
|
|
34d705a54e | ||
|
|
eeb1d4954d | ||
|
|
b638adedff | ||
|
|
285e24cdc7 | ||
|
|
396e643b06 | ||
|
|
dc50190dc3 | ||
|
|
2c8bf4f18c | ||
|
|
1f6379a7e6 | ||
|
|
ddd8eb1da0 | ||
|
|
4c463de45f | ||
|
|
1f4a7a7f6f | ||
|
|
e7df29104e | ||
|
|
9987b35b60 | ||
|
|
16e876ab68 | ||
|
|
50fc2fc74e | ||
|
|
c244dc9c0c | ||
|
|
0f50981573 | ||
|
|
0c1cb20936 | ||
|
|
192617a884 | ||
|
|
297991ef5f | ||
|
|
75f97c4a31 | ||
|
|
40f520086c | ||
|
|
c8dda4f90d | ||
|
|
5f09f97032 | ||
|
|
168056d595 | ||
|
|
c70eaa0096 | ||
|
|
5f36b13408 | ||
|
|
9dc73efa3a | ||
|
|
e9c2868998 | ||
|
|
0a13b04c55 | ||
|
|
cf12d3ee56 | ||
|
|
cea7190453 | ||
|
|
c6d78680fb | ||
|
|
0bf302e013 | ||
|
|
1351fb6689 | ||
|
|
af638d666c | ||
|
|
e4fe601d9d | ||
|
|
4f3cd71e1e | ||
|
|
9c0295db9f | ||
|
|
3fc2d1df80 | ||
|
|
4a6747dcc7 | ||
|
|
54b3c92953 | ||
|
|
a4d460e850 | ||
|
|
3d8869066a | ||
|
|
880a123149 | ||
|
|
39e35bc1d6 | ||
|
|
f219f1e36b | ||
|
|
25ed3d65f8 | ||
|
|
30dbabd73d | ||
|
|
ea2e5bf486 | ||
|
|
ae52fcc757 | ||
|
|
b6c2f123e8 | ||
|
|
15f900317a | ||
|
|
22545cac8b | ||
|
|
03c8d82471 | ||
|
|
14d7a138a5 | ||
|
|
a829eb949b | ||
|
|
fd605d9c81 | ||
|
|
55b4a9eddb | ||
|
|
9ccf77b99c | ||
|
|
ea27075bab | ||
|
|
c3723d0fce | ||
|
|
0edb3cd316 | ||
|
|
e9e6b0bc4f | ||
|
|
4701da201d | ||
|
|
d6d2e052dd | ||
|
|
d3d1dcfe1d | ||
|
|
918ebf5e65 | ||
|
|
67184b88a8 | ||
|
|
fb0f4c3939 | ||
|
|
aae5343543 | ||
|
|
51e9762ca8 | ||
|
|
330dafc652 | ||
|
|
7ddf9fa54e | ||
|
|
f2ca09eedd | ||
|
|
f0e2c8416d | ||
|
|
338b7a8c13 | ||
|
|
b4284f82f3 | ||
|
|
0ce430cab5 | ||
|
|
95c0f6c093 | ||
|
|
387dbc360e | ||
|
|
a88be89c2f | ||
|
|
8bc353442f | ||
|
|
b3502bd627 | ||
|
|
56da7c242d | ||
|
|
fa8f49e87d | ||
|
|
6e08a70afc | ||
|
|
bd4be2b05c | ||
|
|
6b6ff0a95e | ||
|
|
4755cae5cb | ||
|
|
b2384ccc06 | ||
|
|
e6589308dd | ||
|
|
879b25be9f | ||
|
|
d3ad941b30 | ||
|
|
f077fbc3f5 | ||
|
|
4679ce968b | ||
|
|
101e462649 | ||
|
|
5d93ab9b9e | ||
|
|
d557832509 | ||
|
|
fe5c91db29 | ||
|
|
b2947193ec | ||
|
|
f6440753b6 | ||
|
|
17cf903804 | ||
|
|
dcf530d237 | ||
|
|
6b1808dab1 | ||
|
|
5889efd74a | ||
|
|
1a9de1e5c5 | ||
|
|
d1404a2b07 | ||
|
|
664dbf3f4c | ||
|
|
f32a8e26b6 | ||
|
|
b1a92fd4e0 | ||
|
|
1ea9fd2d49 | ||
|
|
f31e4e3176 | ||
|
|
e3287a7e9f | ||
|
|
ec21153d4b | ||
|
|
917e7a8c1d | ||
|
|
8e0a8dc272 | ||
|
|
91bac29ea3 | ||
|
|
3e333769bb | ||
|
|
b4bde6660a | ||
|
|
917f752081 | ||
|
|
915d561286 | ||
|
|
01ef809fd3 | ||
|
|
19902092ce | ||
|
|
39603b6e53 | ||
|
|
9c85a09d3e | ||
|
|
69baa6785f | ||
|
|
bb84d01e14 | ||
|
|
616dae2d8b | ||
|
|
3fbfe50e09 | ||
|
|
c0c8edb9d1 | ||
|
|
84268e484d | ||
|
|
c473c2fa81 | ||
|
|
7402590f49 | ||
|
|
529d1c9f66 | ||
|
|
e85b772ca5 | ||
|
|
f75169fc26 | ||
|
|
07b86521a5 | ||
|
|
961008bbe1 | ||
|
|
6d359b6bb9 | ||
|
|
ea6f803e78 | ||
|
|
0151f8a6a9 | ||
|
|
39c5101957 | ||
|
|
9b1cd5f79c | ||
|
|
36d0b83ed3 | ||
|
|
f0138fad4f | ||
|
|
69802e78f8 | ||
|
|
92e69f561f | ||
|
|
b351520e92 | ||
|
|
481714f095 | ||
|
|
d38656e026 | ||
|
|
69b28b9b02 | ||
|
|
35a68703c2 | ||
|
|
c49fe04750 | ||
|
|
31feabbec7 | ||
|
|
bc3cb2c3c9 | ||
|
|
5ec4481c92 | ||
|
|
be5cb48dfe | ||
|
|
48ff1ece16 | ||
|
|
ed20ed592f | ||
|
|
4fb3435c29 | ||
|
|
37eb14a01a | ||
|
|
d403bc86e3 | ||
|
|
0e2f0f2a4d | ||
|
|
1a4d34a802 | ||
|
|
bb15af9954 | ||
|
|
8a250d1011 | ||
|
|
2f9994f600 | ||
|
|
1cca06a274 | ||
|
|
8fdb3ea631 | ||
|
|
35823d5751 | ||
|
|
66f90a542a | ||
|
|
49981c4bee | ||
|
|
d732c1a845 | ||
|
|
4d7e25f97b | ||
|
|
80656f48e0 | ||
|
|
ebde149980 | ||
|
|
adc0a81592 | ||
|
|
b596f00ce5 | ||
|
|
448442f92b | ||
|
|
8518201562 | ||
|
|
17586c4559 | ||
|
|
f8622da7d4 | ||
|
|
b1a27e9060 | ||
|
|
3c6423d444 | ||
|
|
91b03160ea | ||
|
|
0c1e20ba48 | ||
|
|
1dcac85c0d | ||
|
|
3fc72dbec2 | ||
|
|
494329f568 | ||
|
|
a1e8211ba7 | ||
|
|
80aa7502af | ||
|
|
67bae76048 | ||
|
|
bda2aa46b6 | ||
|
|
27ac204bb6 | ||
|
|
a2526ea244 | ||
|
|
6d9ba8dd2f | ||
|
|
2ca8febff7 | ||
|
|
e105a523e4 | ||
|
|
28f8b05dbc | ||
|
|
d95286db0e | ||
|
|
8e45c34e8e | ||
|
|
9e87c42d0c | ||
|
|
0b52cd002e | ||
|
|
39c43c0c09 | ||
|
|
350485612e | ||
|
|
df31c13912 | ||
|
|
15adfcca8c | ||
|
|
1466788f77 | ||
|
|
760fe3aca9 | ||
|
|
5f75813e84 | ||
|
|
59cb06acf4 | ||
|
|
6349406523 | ||
|
|
bcc2c59f08 | ||
|
|
52d46f9879 | ||
|
|
0b50a5474d | ||
|
|
2259879595 | ||
|
|
4f5091ed7f | ||
|
|
b5afd73024 | ||
|
|
5c929badeb | ||
|
|
3f2de333fb | ||
|
|
7c12b8ae25 | ||
|
|
b54ccbfa2f | ||
|
|
114ce8997f | ||
|
|
f1bba3b958 | ||
|
|
053acef728 | ||
|
|
9f2710185b | ||
|
|
d000879c01 | ||
|
|
25ae169fee | ||
|
|
4443dda0f6 | ||
|
|
c484e989a9 | ||
|
|
86a4656651 | ||
|
|
f25aefeb11 | ||
|
|
228643c7d7 | ||
|
|
072d6d7094 | ||
|
|
de3ce672b8 | ||
|
|
6f5c191998 | ||
|
|
bbaea4def0 | ||
|
|
54f9282166 | ||
|
|
a39b1db266 | ||
|
|
2ddb4ec905 | ||
|
|
7a59e3acf7 | ||
|
|
b34c3db956 | ||
|
|
afea958aca | ||
|
|
dca2a29865 | ||
|
|
97b8e84143 | ||
|
|
23eb0da7d7 | ||
|
|
2edda471e7 | ||
|
|
676aa1358d | ||
|
|
87a36d6ae3 | ||
|
|
b67611094e | ||
|
|
342675276b | ||
|
|
515a621eb4 | ||
|
|
e83e8c2ee4 | ||
|
|
607b168b56 | ||
|
|
e0cf0916dd |
@@ -28,3 +28,4 @@ LICENSE
|
||||
CONTRIBUTING.md
|
||||
dist
|
||||
.git
|
||||
config/
|
||||
22
.github/dependabot.yml
vendored
22
.github/dependabot.yml
vendored
@@ -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"
|
||||
4
.github/workflows/cicd.yml
vendored
4
.github/workflows/cicd.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -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: |
|
||||
|
||||
4
.github/workflows/linting.yml
vendored
4
.github/workflows/linting.yml
vendored
@@ -18,12 +18,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '22'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -11,11 +11,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- 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
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -26,6 +26,10 @@ next-env.d.ts
|
||||
migrations
|
||||
tsconfig.tsbuildinfo
|
||||
config/config.yml
|
||||
config/postgres
|
||||
config/postgres*
|
||||
config/openapi.yaml
|
||||
config/key
|
||||
dist
|
||||
.dist
|
||||
installer
|
||||
@@ -34,4 +38,9 @@ bin
|
||||
.secrets
|
||||
test_event.json
|
||||
.idea/
|
||||
public/branding
|
||||
server/db/index.ts
|
||||
server/build.ts
|
||||
postgres/
|
||||
dynamic/
|
||||
certificates/
|
||||
|
||||
@@ -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.
|
||||
```
|
||||
```
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
FROM node:20-alpine AS builder
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ARG BUILD=oss
|
||||
ARG DATABASE=sqlite
|
||||
|
||||
# COPY package.json package-lock.json ./
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN echo 'export * from "./pg";' > server/db/index.ts
|
||||
RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts
|
||||
|
||||
RUN npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema.ts --out init
|
||||
RUN echo "export const build = \"$BUILD\" as any;" > server/build.ts
|
||||
|
||||
RUN npm run build:pg
|
||||
RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema.ts --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema.ts --out init; fi
|
||||
|
||||
RUN npm run build:$DATABASE
|
||||
RUN npm run build:cli
|
||||
|
||||
FROM node:20-alpine AS runner
|
||||
FROM node:22-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -38,4 +43,4 @@ COPY server/db/names.json ./dist/names.json
|
||||
|
||||
COPY public ./public
|
||||
|
||||
CMD ["npm", "run", "start:pg"]
|
||||
CMD ["npm", "run", "start"]
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# COPY package.json package-lock.json ./
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN echo 'export * from "./sqlite";' > server/db/index.ts
|
||||
|
||||
RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/sqlite/schema.ts --out init
|
||||
|
||||
RUN npm run build:sqlite
|
||||
RUN npm run build:cli
|
||||
|
||||
FROM node:20-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Curl used for the health checks
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
# COPY package.json package-lock.json ./
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/init ./dist/init
|
||||
|
||||
COPY ./cli/wrapper.sh /usr/local/bin/pangctl
|
||||
RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
|
||||
|
||||
COPY server/db/names.json ./dist/names.json
|
||||
|
||||
COPY public ./public
|
||||
|
||||
CMD ["npm", "run", "start:sqlite"]
|
||||
12
Makefile
12
Makefile
@@ -5,10 +5,10 @@ build-release:
|
||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest -f Dockerfile.sqlite --push .
|
||||
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:$(tag) -f Dockerfile.sqlite --push .
|
||||
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-latest -f Dockerfile.pg --push .
|
||||
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-$(tag) -f Dockerfile.pg --push .
|
||||
docker buildx build --build-arg DATABASE=sqlite --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest --push .
|
||||
docker buildx build --build-arg DATABASE=sqlite --platform linux/arm64,linux/amd64 -t fosrl/pangolin:$(tag) --push .
|
||||
docker buildx build --build-arg DATABASE=pg --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-latest --push .
|
||||
docker buildx build --build-arg DATABASE=pg --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-$(tag) --push .
|
||||
|
||||
build-arm:
|
||||
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
||||
@@ -17,10 +17,10 @@ build-x86:
|
||||
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
|
||||
|
||||
build-sqlite:
|
||||
docker build -t fosrl/pangolin:latest -f Dockerfile.sqlite .
|
||||
docker build --build-arg DATABASE=sqlite -t fosrl/pangolin:latest .
|
||||
|
||||
build-pg:
|
||||
docker build -t fosrl/pangolin:postgresql-latest -f Dockerfile.pg .
|
||||
docker build --build-arg DATABASE=pg -t fosrl/pangolin:postgresql-latest .
|
||||
|
||||
test:
|
||||
docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest
|
||||
|
||||
17
README.md
17
README.md
@@ -20,7 +20,7 @@ _Pangolin tunnels your services to the internet so you can access anything from
|
||||
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,17 +104,17 @@ 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://digpangolin.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.
|
||||
|
||||
### Hybrid & High Availability
|
||||
### Managed & High Availability
|
||||
|
||||
Managed control plane, your infrastructure
|
||||
|
||||
@@ -124,7 +123,7 @@ Managed control plane, your infrastructure
|
||||
- Traffic flows through your infra.
|
||||
- We coordinate failover between your nodes or to Cloud when things go bad.
|
||||
|
||||
If interested, [contact us](mailto:numbat@fossorial.io).
|
||||
Try it out using [Pangolin Cloud](https://pangolin.fossorial.io)
|
||||
|
||||
### Full Enterprise On-Premises
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
72
cli/commands/resetUserSecurityKeys.ts
Normal file
72
cli/commands/resetUserSecurityKeys.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -32,7 +32,9 @@ export const setAdminCredentials: CommandModule<{}, SetAdminCredentialsArgs> = {
|
||||
},
|
||||
handler: async (argv: { email: string; password: string }) => {
|
||||
try {
|
||||
const { email, password } = argv;
|
||||
const { password } = argv;
|
||||
let { email } = argv;
|
||||
email = email.trim().toLowerCase();
|
||||
|
||||
const parsed = passwordSchema.safeParse(password);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
53
config/traefik/dynamic_config.yml
Normal file
53
config/traefik/dynamic_config.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
http:
|
||||
middlewares:
|
||||
redirect-to-https:
|
||||
redirectScheme:
|
||||
scheme: https
|
||||
|
||||
routers:
|
||||
# HTTP to HTTPS redirect router
|
||||
main-app-router-redirect:
|
||||
rule: "Host(`{{.DashboardDomain}}`)"
|
||||
service: next-service
|
||||
entryPoints:
|
||||
- web
|
||||
middlewares:
|
||||
- redirect-to-https
|
||||
|
||||
# Next.js router (handles everything except API and WebSocket paths)
|
||||
next-router:
|
||||
rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)"
|
||||
service: next-service
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
# API router (handles /api/v1 paths)
|
||||
api-router:
|
||||
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
|
||||
service: api-service
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
# WebSocket router
|
||||
ws-router:
|
||||
rule: "Host(`{{.DashboardDomain}}`)"
|
||||
service: api-service
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
services:
|
||||
next-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://pangolin:3002" # Next.js server
|
||||
|
||||
api-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://pangolin:3000" # API/WebSocket server
|
||||
34
config/traefik/traefik_config.yml
Normal file
34
config/traefik/traefik_config.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
api:
|
||||
insecure: true
|
||||
dashboard: true
|
||||
|
||||
providers:
|
||||
file:
|
||||
directory: "/var/dynamic"
|
||||
watch: true
|
||||
|
||||
experimental:
|
||||
plugins:
|
||||
badger:
|
||||
moduleName: "github.com/fosrl/badger"
|
||||
version: "v1.2.0"
|
||||
|
||||
log:
|
||||
level: "DEBUG"
|
||||
format: "common"
|
||||
maxSize: 100
|
||||
maxBackups: 3
|
||||
maxAge: 3
|
||||
compress: true
|
||||
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
websecure:
|
||||
address: ":9443"
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: "30m"
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
@@ -22,8 +22,7 @@ services:
|
||||
command:
|
||||
- --reachableAt=http://gerbil:3003
|
||||
- --generateAndSaveKeyTo=/var/config/key
|
||||
- --remoteConfig=http://pangolin:3001/api/v1/gerbil/get-config
|
||||
- --reportBandwidthTo=http://pangolin:3001/api/v1/gerbil/receive-bandwidth
|
||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||
volumes:
|
||||
- ./config/:/var/config
|
||||
cap_add:
|
||||
@@ -31,11 +30,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 +51,5 @@ services:
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
name: pangolin
|
||||
name: pangolin
|
||||
enable_ipv6: true
|
||||
@@ -7,6 +7,8 @@ services:
|
||||
POSTGRES_DB: postgres # Default database name
|
||||
POSTGRES_USER: postgres # Default user
|
||||
POSTGRES_PASSWORD: password # Default password (change for production!)
|
||||
volumes:
|
||||
- ./config/postgres:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432" # Map host port 5432 to container port 5432
|
||||
restart: no
|
||||
restart: no
|
||||
32
docker-compose.t.yml
Normal file
32
docker-compose.t.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
name: pangolin
|
||||
services:
|
||||
gerbil:
|
||||
image: gerbil
|
||||
container_name: gerbil
|
||||
network_mode: host
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- --reachableAt=http://localhost:3003
|
||||
- --generateAndSaveKeyTo=/var/config/key
|
||||
- --remoteConfig=http://localhost:3001/api/v1/
|
||||
- --sni-port=443
|
||||
volumes:
|
||||
- ./config/:/var/config
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
|
||||
traefik:
|
||||
image: docker.io/traefik:v3.4.1
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
command:
|
||||
- --configFile=/etc/traefik/traefik_config.yml
|
||||
volumes:
|
||||
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
||||
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
||||
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
||||
- ./certificates:/var/certificates:ro
|
||||
- ./dynamic:/var/dynamic:ro
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -64,7 +64,7 @@ esbuild
|
||||
}),
|
||||
],
|
||||
sourcemap: true,
|
||||
target: "node20",
|
||||
target: "node22",
|
||||
})
|
||||
.then(() => {
|
||||
console.log("Build completed successfully");
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -37,15 +37,28 @@ type DynamicConfig struct {
|
||||
} `yaml:"http"`
|
||||
}
|
||||
|
||||
// ConfigValues holds the extracted configuration values
|
||||
type ConfigValues struct {
|
||||
// TraefikConfigValues holds the extracted configuration values
|
||||
type TraefikConfigValues struct {
|
||||
DashboardDomain string
|
||||
LetsEncryptEmail string
|
||||
BadgerVersion string
|
||||
}
|
||||
|
||||
// AppConfig represents the app section of the config.yml
|
||||
type AppConfig struct {
|
||||
App struct {
|
||||
DashboardURL string `yaml:"dashboard_url"`
|
||||
LogLevel string `yaml:"log_level"`
|
||||
} `yaml:"app"`
|
||||
}
|
||||
|
||||
type AppConfigValues struct {
|
||||
DashboardURL string
|
||||
LogLevel string
|
||||
}
|
||||
|
||||
// ReadTraefikConfig reads and extracts values from Traefik configuration files
|
||||
func ReadTraefikConfig(mainConfigPath, dynamicConfigPath string) (*ConfigValues, error) {
|
||||
func ReadTraefikConfig(mainConfigPath string) (*TraefikConfigValues, error) {
|
||||
// Read main config file
|
||||
mainConfigData, err := os.ReadFile(mainConfigPath)
|
||||
if err != nil {
|
||||
@@ -57,48 +70,33 @@ func ReadTraefikConfig(mainConfigPath, dynamicConfigPath string) (*ConfigValues,
|
||||
return nil, fmt.Errorf("error parsing main config file: %w", err)
|
||||
}
|
||||
|
||||
// Read dynamic config file
|
||||
dynamicConfigData, err := os.ReadFile(dynamicConfigPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading dynamic config file: %w", err)
|
||||
}
|
||||
|
||||
var dynamicConfig DynamicConfig
|
||||
if err := yaml.Unmarshal(dynamicConfigData, &dynamicConfig); err != nil {
|
||||
return nil, fmt.Errorf("error parsing dynamic config file: %w", err)
|
||||
}
|
||||
|
||||
// Extract values
|
||||
values := &ConfigValues{
|
||||
values := &TraefikConfigValues{
|
||||
BadgerVersion: mainConfig.Experimental.Plugins.Badger.Version,
|
||||
LetsEncryptEmail: mainConfig.CertificatesResolvers.LetsEncrypt.Acme.Email,
|
||||
}
|
||||
|
||||
// Extract DashboardDomain from router rules
|
||||
// Look for it in the main router rules
|
||||
for _, router := range dynamicConfig.HTTP.Routers {
|
||||
if router.Rule != "" {
|
||||
// Extract domain from Host(`mydomain.com`)
|
||||
if domain := extractDomainFromRule(router.Rule); domain != "" {
|
||||
values.DashboardDomain = domain
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// extractDomainFromRule extracts the domain from a router rule
|
||||
func extractDomainFromRule(rule string) string {
|
||||
// Look for the Host(`mydomain.com`) pattern
|
||||
if start := findPattern(rule, "Host(`"); start != -1 {
|
||||
end := findPattern(rule[start:], "`)")
|
||||
if end != -1 {
|
||||
return rule[start+6 : start+end]
|
||||
}
|
||||
func ReadAppConfig(configPath string) (*AppConfigValues, error) {
|
||||
// Read config file
|
||||
configData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading config file: %w", err)
|
||||
}
|
||||
return ""
|
||||
|
||||
var appConfig AppConfig
|
||||
if err := yaml.Unmarshal(configData, &appConfig); err != nil {
|
||||
return nil, fmt.Errorf("error parsing config file: %w", err)
|
||||
}
|
||||
|
||||
values := &AppConfigValues{
|
||||
DashboardURL: appConfig.App.DashboardURL,
|
||||
LogLevel: appConfig.App.LogLevel,
|
||||
}
|
||||
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// findPattern finds the start of a pattern in a string
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
# 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
|
||||
|
||||
gerbil:
|
||||
start_port: 51820
|
||||
base_endpoint: "{{.DashboardDomain}}"
|
||||
{{if .HybridMode}}
|
||||
managed:
|
||||
id: "{{.HybridId}}"
|
||||
secret: "{{.HybridSecret}}"
|
||||
|
||||
{{else}}
|
||||
app:
|
||||
dashboard_url: "https://{{.DashboardDomain}}"
|
||||
log_level: "info"
|
||||
@@ -17,15 +26,6 @@ server:
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||
credentials: false
|
||||
|
||||
gerbil:
|
||||
start_port: 51820
|
||||
base_endpoint: "{{.DashboardDomain}}"
|
||||
|
||||
orgs:
|
||||
block_size: 24
|
||||
subnet_group: 100.89.138.0/20
|
||||
|
||||
{{if .EnableEmail}}
|
||||
email:
|
||||
smtp_host: "{{.EmailSMTPHost}}"
|
||||
@@ -34,9 +34,9 @@ email:
|
||||
smtp_pass: "{{.EmailSMTPPass}}"
|
||||
no_reply: "{{.EmailNoReply}}"
|
||||
{{end}}
|
||||
|
||||
flags:
|
||||
require_email_verification: {{.EnableEmail}}
|
||||
disable_signup_without_invite: true
|
||||
disable_user_create_org: false
|
||||
allow_raw_resources: true
|
||||
{{end}}
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:latest
|
||||
image: docker.io/crowdsecurity/crowdsec:latest
|
||||
container_name: crowdsec
|
||||
environment:
|
||||
GID: "1000"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
name: pangolin
|
||||
services:
|
||||
pangolin:
|
||||
image: fosrl/pangolin:{{.PangolinVersion}}
|
||||
image: docker.io/fosrl/pangolin:{{.PangolinVersion}}
|
||||
container_name: pangolin
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
- pangolin-data:/var/certificates
|
||||
- pangolin-data:/var/dynamic
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
||||
interval: "10s"
|
||||
@@ -13,7 +15,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:
|
||||
@@ -22,8 +24,7 @@ services:
|
||||
command:
|
||||
- --reachableAt=http://gerbil:3003
|
||||
- --generateAndSaveKeyTo=/var/config/key
|
||||
- --remoteConfig=http://pangolin:3001/api/v1/gerbil/get-config
|
||||
- --reportBandwidthTo=http://pangolin:3001/api/v1/gerbil/receive-bandwidth
|
||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||
volumes:
|
||||
- ./config/:/var/config
|
||||
cap_add:
|
||||
@@ -31,11 +32,12 @@ services:
|
||||
- SYS_MODULE
|
||||
ports:
|
||||
- 51820:51820/udp
|
||||
- 443:443 # Port for traefik because of the network_mode
|
||||
- 80:80 # Port for traefik because of the network_mode
|
||||
- 21820:21820/udp
|
||||
- 443:{{if .HybridMode}}8443{{else}}443{{end}}
|
||||
- 80:80
|
||||
{{end}}
|
||||
traefik:
|
||||
image: traefik:v3.4.1
|
||||
image: docker.io/traefik:v3.5
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
{{if .InstallGerbil}}
|
||||
@@ -54,8 +56,15 @@ services:
|
||||
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
||||
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
||||
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
||||
# Shared volume for certificates and dynamic config in file mode
|
||||
- pangolin-data:/var/certificates:ro
|
||||
- pangolin-data:/var/dynamic:ro
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
name: pangolin
|
||||
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
||||
|
||||
volumes:
|
||||
pangolin-data:
|
||||
|
||||
@@ -3,12 +3,17 @@ api:
|
||||
dashboard: true
|
||||
|
||||
providers:
|
||||
{{if not .HybridMode}}
|
||||
http:
|
||||
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
||||
pollInterval: "5s"
|
||||
file:
|
||||
filename: "/etc/traefik/dynamic_config.yml"
|
||||
|
||||
{{else}}
|
||||
file:
|
||||
directory: "/var/dynamic"
|
||||
watch: true
|
||||
{{end}}
|
||||
experimental:
|
||||
plugins:
|
||||
badger:
|
||||
@@ -22,7 +27,7 @@ log:
|
||||
maxBackups: 3
|
||||
maxAge: 3
|
||||
compress: true
|
||||
|
||||
{{if not .HybridMode}}
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
acme:
|
||||
@@ -31,7 +36,7 @@ certificatesResolvers:
|
||||
email: "{{.LetsEncryptEmail}}"
|
||||
storage: "/letsencrypt/acme.json"
|
||||
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||
|
||||
{{end}}
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
@@ -40,9 +45,12 @@ entryPoints:
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: "30m"
|
||||
http:
|
||||
{{if not .HybridMode}} http:
|
||||
tls:
|
||||
certResolver: "letsencrypt"
|
||||
certResolver: "letsencrypt"{{end}}
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
|
||||
ping:
|
||||
entryPoint: "web"
|
||||
332
install/containers.go
Normal file
332
install/containers.go
Normal file
@@ -0,0 +1,332 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
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(string(containerType), "container", "inspect", "-f", "{{.State.Running}}", containerName)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
// If the container doesn't exist or there's another error, wait and retry
|
||||
time.Sleep(retryInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
isRunning := strings.TrimSpace(out.String()) == "true"
|
||||
if isRunning {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Container exists but isn't running yet, wait and retry
|
||||
time.Sleep(retryInterval)
|
||||
}
|
||||
|
||||
return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds()))
|
||||
}
|
||||
|
||||
func installDocker() error {
|
||||
// Detect Linux distribution
|
||||
cmd := exec.Command("cat", "/etc/os-release")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to detect Linux distribution: %v", err)
|
||||
}
|
||||
osRelease := string(output)
|
||||
|
||||
// Detect system architecture
|
||||
archCmd := exec.Command("uname", "-m")
|
||||
archOutput, err := archCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to detect system architecture: %v", err)
|
||||
}
|
||||
arch := strings.TrimSpace(string(archOutput))
|
||||
|
||||
// Map architecture to Docker's architecture naming
|
||||
var dockerArch string
|
||||
switch arch {
|
||||
case "x86_64":
|
||||
dockerArch = "amd64"
|
||||
case "aarch64":
|
||||
dockerArch = "arm64"
|
||||
default:
|
||||
return fmt.Errorf("unsupported architecture: %s", arch)
|
||||
}
|
||||
|
||||
var installCmd *exec.Cmd
|
||||
switch {
|
||||
case strings.Contains(osRelease, "ID=ubuntu"):
|
||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||
apt-get update &&
|
||||
apt-get install -y apt-transport-https ca-certificates curl software-properties-common &&
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
||||
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
||||
apt-get update &&
|
||||
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||
`, dockerArch))
|
||||
case strings.Contains(osRelease, "ID=debian"):
|
||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||
apt-get update &&
|
||||
apt-get install -y apt-transport-https ca-certificates curl software-properties-common &&
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
||||
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
||||
apt-get update &&
|
||||
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||
`, dockerArch))
|
||||
case strings.Contains(osRelease, "ID=fedora"):
|
||||
// Detect Fedora version to handle DNF 5 changes
|
||||
versionCmd := exec.Command("bash", "-c", "grep VERSION_ID /etc/os-release | cut -d'=' -f2 | tr -d '\"'")
|
||||
versionOutput, err := versionCmd.Output()
|
||||
var fedoraVersion int
|
||||
if err == nil {
|
||||
if v, parseErr := strconv.Atoi(strings.TrimSpace(string(versionOutput))); parseErr == nil {
|
||||
fedoraVersion = v
|
||||
}
|
||||
}
|
||||
|
||||
// Use appropriate DNF syntax based on version
|
||||
var repoCmd string
|
||||
if fedoraVersion >= 41 {
|
||||
// DNF 5 syntax for Fedora 41+
|
||||
repoCmd = "dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo"
|
||||
} else {
|
||||
// DNF 4 syntax for Fedora < 41
|
||||
repoCmd = "dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo"
|
||||
}
|
||||
|
||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||
dnf -y install dnf-plugins-core &&
|
||||
%s &&
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||
`, repoCmd))
|
||||
case strings.Contains(osRelease, "ID=opensuse") || strings.Contains(osRelease, "ID=\"opensuse-"):
|
||||
installCmd = exec.Command("bash", "-c", `
|
||||
zypper install -y docker docker-compose &&
|
||||
systemctl enable docker
|
||||
`)
|
||||
case strings.Contains(osRelease, "ID=rhel") || strings.Contains(osRelease, "ID=\"rhel"):
|
||||
installCmd = exec.Command("bash", "-c", `
|
||||
dnf remove -y runc &&
|
||||
dnf -y install yum-utils &&
|
||||
dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo &&
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin &&
|
||||
systemctl enable docker
|
||||
`)
|
||||
case strings.Contains(osRelease, "ID=amzn"):
|
||||
installCmd = exec.Command("bash", "-c", `
|
||||
yum update -y &&
|
||||
yum install -y docker &&
|
||||
systemctl enable docker &&
|
||||
usermod -a -G docker ec2-user
|
||||
`)
|
||||
default:
|
||||
return fmt.Errorf("unsupported Linux distribution")
|
||||
}
|
||||
|
||||
installCmd.Stdout = os.Stdout
|
||||
installCmd.Stderr = os.Stderr
|
||||
return installCmd.Run()
|
||||
}
|
||||
|
||||
func startDockerService() error {
|
||||
if runtime.GOOS == "linux" {
|
||||
cmd := exec.Command("systemctl", "enable", "--now", "docker")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
} else if runtime.GOOS == "darwin" {
|
||||
// On macOS, Docker is usually started via the Docker Desktop application
|
||||
fmt.Println("Please start Docker Desktop manually on macOS.")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unsupported operating system for starting Docker service")
|
||||
}
|
||||
|
||||
func isDockerInstalled() bool {
|
||||
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
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isUserInDockerGroup() bool {
|
||||
if runtime.GOOS == "darwin" {
|
||||
// Docker group is not applicable on macOS
|
||||
// So we assume that the user can run Docker commands
|
||||
return true
|
||||
}
|
||||
|
||||
if os.Geteuid() == 0 {
|
||||
return true // Root user can run Docker commands anyway
|
||||
}
|
||||
|
||||
// Check if the current user is in the docker group
|
||||
if dockerGroup, err := user.LookupGroup("docker"); err == nil {
|
||||
if currentUser, err := user.Current(); err == nil {
|
||||
if currentUserGroupIds, err := currentUser.GroupIds(); err == nil {
|
||||
for _, groupId := range currentUserGroupIds {
|
||||
if groupId == dockerGroup.Gid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Eventually, if any of the checks fail, we assume the user cannot run Docker commands
|
||||
return false
|
||||
}
|
||||
|
||||
// isDockerRunning checks if the Docker daemon is running by using the `docker info` command.
|
||||
func isDockerRunning() bool {
|
||||
cmd := exec.Command("docker", "info")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied
|
||||
func executeDockerComposeCommandWithArgs(args ...string) error {
|
||||
var cmd *exec.Cmd
|
||||
var useNewStyle bool
|
||||
|
||||
if !isDockerInstalled() {
|
||||
return fmt.Errorf("docker is not installed")
|
||||
}
|
||||
|
||||
checkCmd := exec.Command("docker", "compose", "version")
|
||||
if err := checkCmd.Run(); err == nil {
|
||||
useNewStyle = true
|
||||
} else {
|
||||
checkCmd = exec.Command("docker-compose", "version")
|
||||
if err := checkCmd.Run(); err == nil {
|
||||
useNewStyle = false
|
||||
} else {
|
||||
return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available")
|
||||
}
|
||||
}
|
||||
|
||||
if useNewStyle {
|
||||
cmd = exec.Command("docker", append([]string{"compose"}, args...)...)
|
||||
} else {
|
||||
cmd = exec.Command("docker-compose", args...)
|
||||
}
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// pullContainers pulls the containers using the appropriate command.
|
||||
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)
|
||||
}
|
||||
|
||||
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(containerType SupportedContainer) error {
|
||||
fmt.Println("Starting containers...")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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(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)
|
||||
}
|
||||
|
||||
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, 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
74
install/input.go
Normal file
74
install/input.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
func readString(reader *bufio.Reader, prompt string, defaultValue string) string {
|
||||
if defaultValue != "" {
|
||||
fmt.Printf("%s (default: %s): ", prompt, defaultValue)
|
||||
} else {
|
||||
fmt.Print(prompt + ": ")
|
||||
}
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
func readStringNoDefault(reader *bufio.Reader, prompt string) string {
|
||||
fmt.Print(prompt + ": ")
|
||||
input, _ := reader.ReadString('\n')
|
||||
return strings.TrimSpace(input)
|
||||
}
|
||||
|
||||
func readPassword(prompt string, reader *bufio.Reader) string {
|
||||
if term.IsTerminal(int(syscall.Stdin)) {
|
||||
fmt.Print(prompt + ": ")
|
||||
// Read password without echo if we're in a terminal
|
||||
password, err := term.ReadPassword(int(syscall.Stdin))
|
||||
fmt.Println() // Add a newline since ReadPassword doesn't add one
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
input := strings.TrimSpace(string(password))
|
||||
if input == "" {
|
||||
return readPassword(prompt, reader)
|
||||
}
|
||||
return input
|
||||
} else {
|
||||
// Fallback to reading from stdin if not in a terminal
|
||||
return readString(reader, prompt, "")
|
||||
}
|
||||
}
|
||||
|
||||
func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
|
||||
defaultStr := "no"
|
||||
if defaultValue {
|
||||
defaultStr = "yes"
|
||||
}
|
||||
input := readString(reader, prompt+" (yes/no)", defaultStr)
|
||||
return strings.ToLower(input) == "yes"
|
||||
}
|
||||
|
||||
func readBoolNoDefault(reader *bufio.Reader, prompt string) bool {
|
||||
input := readStringNoDefault(reader, prompt+" (yes/no)")
|
||||
return strings.ToLower(input) == "yes"
|
||||
}
|
||||
|
||||
func readInt(reader *bufio.Reader, prompt string, defaultValue int) int {
|
||||
input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue))
|
||||
if input == "" {
|
||||
return defaultValue
|
||||
}
|
||||
value := defaultValue
|
||||
fmt.Sscanf(input, "%d", &value)
|
||||
return value
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
docker
|
||||
example.com
|
||||
pangolin.example.com
|
||||
yes
|
||||
admin@example.com
|
||||
yes
|
||||
admin@example.com
|
||||
|
||||
794
install/main.go
794
install/main.go
@@ -7,19 +7,15 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/template"
|
||||
"time"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/term"
|
||||
"net"
|
||||
)
|
||||
|
||||
// DO NOT EDIT THIS FUNCTION; IT MATCHED BY REGEX IN CICD
|
||||
@@ -33,41 +29,58 @@ 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
|
||||
HybridMode bool
|
||||
HybridId string
|
||||
HybridSecret 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("\nPlease 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("\nLets get started!")
|
||||
|
||||
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.")
|
||||
os.Exit(1)
|
||||
}
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
var config Config
|
||||
|
||||
@@ -79,6 +92,26 @@ func main() {
|
||||
config.DoCrowdsecInstall = false
|
||||
config.Secret = generateRandomSecretKey()
|
||||
|
||||
fmt.Println("\n=== Generating Configuration Files ===")
|
||||
|
||||
// If the secret and id are not generated then generate them
|
||||
if config.HybridMode && (config.HybridId == "" || config.HybridSecret == "") {
|
||||
// fmt.Println("Requesting hybrid credentials from cloud...")
|
||||
credentials, err := requestHybridCredentials()
|
||||
if err != nil {
|
||||
fmt.Printf("Error requesting hybrid credentials: %v\n", err)
|
||||
fmt.Println("Please obtain credentials manually from the dashboard and run the installer again.")
|
||||
os.Exit(1)
|
||||
}
|
||||
config.HybridId = credentials.RemoteExitNodeId
|
||||
config.HybridSecret = credentials.Secret
|
||||
fmt.Printf("Your managed credentials have been obtained successfully.\n")
|
||||
fmt.Printf(" ID: %s\n", config.HybridId)
|
||||
fmt.Printf(" Secret: %s\n", config.HybridSecret)
|
||||
fmt.Println("Take these to the Pangolin dashboard https://pangolin.fossorial.io to adopt your node.")
|
||||
readBool(reader, "Have you adopted your node?", true)
|
||||
}
|
||||
|
||||
if err := createConfigFiles(config); err != nil {
|
||||
fmt.Printf("Error creating config files: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -86,65 +119,77 @@ func main() {
|
||||
|
||||
moveFile("config/docker-compose.yml", "docker-compose.yml")
|
||||
|
||||
if !isDockerInstalled() && runtime.GOOS == "linux" {
|
||||
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
||||
installDocker()
|
||||
// try to start docker service but ignore errors
|
||||
if err := startDockerService(); err != nil {
|
||||
fmt.Println("Error starting Docker service:", err)
|
||||
} else {
|
||||
fmt.Println("Docker service started successfully!")
|
||||
}
|
||||
// wait 10 seconds for docker to start checking if docker is running every 2 seconds
|
||||
fmt.Println("Waiting for Docker to start...")
|
||||
for i := 0; i < 5; i++ {
|
||||
if isDockerRunning() {
|
||||
fmt.Println("Docker is running!")
|
||||
break
|
||||
}
|
||||
fmt.Println("Docker is not running yet, waiting...")
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
if !isDockerRunning() {
|
||||
fmt.Println("Docker is still not running after 10 seconds. Please check the installation.")
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Docker installed successfully!")
|
||||
}
|
||||
}
|
||||
fmt.Println("\nConfiguration files created successfully!")
|
||||
|
||||
fmt.Println("\n=== Starting installation ===")
|
||||
|
||||
if isDockerInstalled() {
|
||||
if readBool(reader, "Would you like to install and start the containers?", true) {
|
||||
if err := pullContainers(); err != nil {
|
||||
fmt.Println("Error: ", err)
|
||||
return
|
||||
}
|
||||
if readBool(reader, "Would you like to install and start the containers?", true) {
|
||||
|
||||
if err := startContainers(); err != nil {
|
||||
fmt.Println("Error: ", err)
|
||||
return
|
||||
config.InstallationContainerType = podmanOrDocker(reader)
|
||||
|
||||
if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker {
|
||||
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
||||
installDocker()
|
||||
// try to start docker service but ignore errors
|
||||
if err := startDockerService(); err != nil {
|
||||
fmt.Println("Error starting Docker service:", err)
|
||||
} else {
|
||||
fmt.Println("Docker service started successfully!")
|
||||
}
|
||||
// wait 10 seconds for docker to start checking if docker is running every 2 seconds
|
||||
fmt.Println("Waiting for Docker to start...")
|
||||
for i := 0; i < 5; i++ {
|
||||
if isDockerRunning() {
|
||||
fmt.Println("Docker is running!")
|
||||
break
|
||||
}
|
||||
fmt.Println("Docker is not running yet, waiting...")
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
if !isDockerRunning() {
|
||||
fmt.Println("Docker is still not running after 10 seconds. Please check the installation.")
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Docker installed successfully!")
|
||||
}
|
||||
}
|
||||
|
||||
if err := pullContainers(config.InstallationContainerType); err != nil {
|
||||
fmt.Println("Error: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := startContainers(config.InstallationContainerType); err != nil {
|
||||
fmt.Println("Error: ", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
fmt.Println("Looks like you already installed, so I am going to do the setup...")
|
||||
fmt.Println("Looks like you already installed Pangolin!")
|
||||
}
|
||||
|
||||
if !checkIsCrowdsecInstalledInCompose() {
|
||||
if !checkIsCrowdsecInstalledInCompose() && !checkIsPangolinInstalledWithHybrid() {
|
||||
fmt.Println("\n=== CrowdSec Install ===")
|
||||
// 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")
|
||||
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml")
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading config: %v\n", err)
|
||||
return
|
||||
}
|
||||
config.DashboardDomain = traefikConfig.DashboardDomain
|
||||
appConfig, err := ReadAppConfig("config/config.yml")
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading config: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
config.DashboardDomain = appConfig.DashboardURL
|
||||
config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail
|
||||
config.BadgerVersion = traefikConfig.BadgerVersion
|
||||
|
||||
@@ -165,61 +210,97 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Installation complete!")
|
||||
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
||||
}
|
||||
if !config.HybridMode {
|
||||
// Setup Token Section
|
||||
fmt.Println("\n=== Setup Token ===")
|
||||
|
||||
func readString(reader *bufio.Reader, prompt string, defaultValue string) string {
|
||||
if defaultValue != "" {
|
||||
fmt.Printf("%s (default: %s): ", prompt, defaultValue)
|
||||
} else {
|
||||
fmt.Print(prompt + ": ")
|
||||
}
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
func readPassword(prompt string, reader *bufio.Reader) string {
|
||||
if term.IsTerminal(int(syscall.Stdin)) {
|
||||
fmt.Print(prompt + ": ")
|
||||
// Read password without echo if we're in a terminal
|
||||
password, err := term.ReadPassword(int(syscall.Stdin))
|
||||
fmt.Println() // Add a newline since ReadPassword doesn't add one
|
||||
if err != nil {
|
||||
return ""
|
||||
// Check if containers were started during this installation
|
||||
containersStarted := false
|
||||
if (isDockerInstalled() && config.InstallationContainerType == Docker) ||
|
||||
(isPodmanInstalled() && config.InstallationContainerType == Podman) {
|
||||
// Try to fetch and display the token if containers are running
|
||||
containersStarted = true
|
||||
printSetupToken(config.InstallationContainerType, config.DashboardDomain)
|
||||
}
|
||||
input := strings.TrimSpace(string(password))
|
||||
if input == "" {
|
||||
return readPassword(prompt, reader)
|
||||
|
||||
// If containers weren't started or token wasn't found, show instructions
|
||||
if !containersStarted {
|
||||
showSetupTokenInstructions(config.InstallationContainerType, config.DashboardDomain)
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
fmt.Println("\nInstallation complete!")
|
||||
|
||||
if !config.HybridMode && !checkIsPangolinInstalledWithHybrid() {
|
||||
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
||||
}
|
||||
}
|
||||
|
||||
func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
||||
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 {
|
||||
// Fallback to reading from stdin if not in a terminal
|
||||
return readString(reader, prompt, "")
|
||||
fmt.Printf("Unrecognized container type: %s. Valid options are 'docker' or 'podman'.\n", inputContainer)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
|
||||
defaultStr := "no"
|
||||
if defaultValue {
|
||||
defaultStr = "yes"
|
||||
}
|
||||
input := readString(reader, prompt+" (yes/no)", defaultStr)
|
||||
return strings.ToLower(input) == "yes"
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
func readInt(reader *bufio.Reader, prompt string, defaultValue int) int {
|
||||
input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue))
|
||||
if input == "" {
|
||||
return defaultValue
|
||||
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)
|
||||
}
|
||||
value := defaultValue
|
||||
fmt.Sscanf(input, "%d", &value)
|
||||
return value
|
||||
|
||||
return chosenContainer
|
||||
}
|
||||
|
||||
func collectUserInput(reader *bufio.Reader) Config {
|
||||
@@ -227,36 +308,73 @@ 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)
|
||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
||||
|
||||
// Email configuration
|
||||
fmt.Println("\n=== Email Configuration ===")
|
||||
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
||||
|
||||
if config.EnableEmail {
|
||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
||||
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "")
|
||||
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
||||
for {
|
||||
response := readString(reader, "Do you want to install Pangolin as a cloud-managed (beta) node? (yes/no)", "")
|
||||
if strings.EqualFold(response, "yes") || strings.EqualFold(response, "y") {
|
||||
config.HybridMode = true
|
||||
break
|
||||
} else if strings.EqualFold(response, "no") || strings.EqualFold(response, "n") {
|
||||
config.HybridMode = false
|
||||
break
|
||||
}
|
||||
fmt.Println("Please answer 'yes' or 'no'")
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if config.BaseDomain == "" {
|
||||
fmt.Println("Error: Domain name is required")
|
||||
os.Exit(1)
|
||||
if config.HybridMode {
|
||||
alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard? If not, we will create them later", false)
|
||||
|
||||
if alreadyHaveCreds {
|
||||
config.HybridId = readString(reader, "Enter your ID", "")
|
||||
config.HybridSecret = readString(reader, "Enter your secret", "")
|
||||
}
|
||||
|
||||
config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", "")
|
||||
config.InstallGerbil = true
|
||||
} else {
|
||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||
|
||||
// Set default dashboard domain after base domain is collected
|
||||
defaultDashboardDomain := ""
|
||||
if config.BaseDomain != "" {
|
||||
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
||||
}
|
||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
||||
|
||||
// Email configuration
|
||||
fmt.Println("\n=== Email Configuration ===")
|
||||
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
||||
|
||||
if config.EnableEmail {
|
||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
||||
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
||||
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if config.BaseDomain == "" {
|
||||
fmt.Println("Error: Domain name is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
if config.LetsEncryptEmail == "" {
|
||||
fmt.Println("Error: Let's Encrypt email is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Advanced configuration
|
||||
|
||||
fmt.Println("\n=== Advanced Configuration ===")
|
||||
|
||||
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
|
||||
|
||||
if config.DashboardDomain == "" {
|
||||
fmt.Println("Error: Dashboard Domain name is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
if config.LetsEncryptEmail == "" {
|
||||
fmt.Println("Error: Let's Encrypt email is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -286,6 +404,11 @@ func createConfigFiles(config Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// the hybrid does not need the dynamic config
|
||||
if config.HybridMode && strings.Contains(path, "dynamic_config.yml") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// skip .DS_Store
|
||||
if strings.Contains(path, ".DS_Store") {
|
||||
return nil
|
||||
@@ -330,7 +453,6 @@ func createConfigFiles(config Config) error {
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error walking config files: %v", err)
|
||||
}
|
||||
@@ -338,243 +460,6 @@ func createConfigFiles(config Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func installDocker() error {
|
||||
// Detect Linux distribution
|
||||
cmd := exec.Command("cat", "/etc/os-release")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to detect Linux distribution: %v", err)
|
||||
}
|
||||
osRelease := string(output)
|
||||
|
||||
// Detect system architecture
|
||||
archCmd := exec.Command("uname", "-m")
|
||||
archOutput, err := archCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to detect system architecture: %v", err)
|
||||
}
|
||||
arch := strings.TrimSpace(string(archOutput))
|
||||
|
||||
// Map architecture to Docker's architecture naming
|
||||
var dockerArch string
|
||||
switch arch {
|
||||
case "x86_64":
|
||||
dockerArch = "amd64"
|
||||
case "aarch64":
|
||||
dockerArch = "arm64"
|
||||
default:
|
||||
return fmt.Errorf("unsupported architecture: %s", arch)
|
||||
}
|
||||
|
||||
var installCmd *exec.Cmd
|
||||
switch {
|
||||
case strings.Contains(osRelease, "ID=ubuntu"):
|
||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||
apt-get update &&
|
||||
apt-get install -y apt-transport-https ca-certificates curl software-properties-common &&
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
||||
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
||||
apt-get update &&
|
||||
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||
`, dockerArch))
|
||||
case strings.Contains(osRelease, "ID=debian"):
|
||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||
apt-get update &&
|
||||
apt-get install -y apt-transport-https ca-certificates curl software-properties-common &&
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
||||
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
||||
apt-get update &&
|
||||
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||
`, dockerArch))
|
||||
case strings.Contains(osRelease, "ID=fedora"):
|
||||
// Detect Fedora version to handle DNF 5 changes
|
||||
versionCmd := exec.Command("bash", "-c", "grep VERSION_ID /etc/os-release | cut -d'=' -f2 | tr -d '\"'")
|
||||
versionOutput, err := versionCmd.Output()
|
||||
var fedoraVersion int
|
||||
if err == nil {
|
||||
if v, parseErr := strconv.Atoi(strings.TrimSpace(string(versionOutput))); parseErr == nil {
|
||||
fedoraVersion = v
|
||||
}
|
||||
}
|
||||
|
||||
// Use appropriate DNF syntax based on version
|
||||
var repoCmd string
|
||||
if fedoraVersion >= 41 {
|
||||
// DNF 5 syntax for Fedora 41+
|
||||
repoCmd = "dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo"
|
||||
} else {
|
||||
// DNF 4 syntax for Fedora < 41
|
||||
repoCmd = "dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo"
|
||||
}
|
||||
|
||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||
dnf -y install dnf-plugins-core &&
|
||||
%s &&
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||
`, repoCmd))
|
||||
case strings.Contains(osRelease, "ID=opensuse") || strings.Contains(osRelease, "ID=\"opensuse-"):
|
||||
installCmd = exec.Command("bash", "-c", `
|
||||
zypper install -y docker docker-compose &&
|
||||
systemctl enable docker
|
||||
`)
|
||||
case strings.Contains(osRelease, "ID=rhel") || strings.Contains(osRelease, "ID=\"rhel"):
|
||||
installCmd = exec.Command("bash", "-c", `
|
||||
dnf remove -y runc &&
|
||||
dnf -y install yum-utils &&
|
||||
dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo &&
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin &&
|
||||
systemctl enable docker
|
||||
`)
|
||||
case strings.Contains(osRelease, "ID=amzn"):
|
||||
installCmd = exec.Command("bash", "-c", `
|
||||
yum update -y &&
|
||||
yum install -y docker &&
|
||||
systemctl enable docker &&
|
||||
usermod -a -G docker ec2-user
|
||||
`)
|
||||
default:
|
||||
return fmt.Errorf("unsupported Linux distribution")
|
||||
}
|
||||
|
||||
installCmd.Stdout = os.Stdout
|
||||
installCmd.Stderr = os.Stderr
|
||||
return installCmd.Run()
|
||||
}
|
||||
|
||||
func startDockerService() error {
|
||||
if runtime.GOOS == "linux" {
|
||||
cmd := exec.Command("systemctl", "enable", "--now", "docker")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
} else if runtime.GOOS == "darwin" {
|
||||
// On macOS, Docker is usually started via the Docker Desktop application
|
||||
fmt.Println("Please start Docker Desktop manually on macOS.")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unsupported operating system for starting Docker service")
|
||||
}
|
||||
|
||||
func isDockerInstalled() bool {
|
||||
cmd := exec.Command("docker", "--version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isUserInDockerGroup() bool {
|
||||
if runtime.GOOS == "darwin" {
|
||||
// Docker group is not applicable on macOS
|
||||
// So we assume that the user can run Docker commands
|
||||
return true
|
||||
}
|
||||
|
||||
if os.Geteuid() == 0 {
|
||||
return true // Root user can run Docker commands anyway
|
||||
}
|
||||
|
||||
// Check if the current user is in the docker group
|
||||
if dockerGroup, err := user.LookupGroup("docker"); err == nil {
|
||||
if currentUser, err := user.Current(); err == nil {
|
||||
if currentUserGroupIds, err := currentUser.GroupIds(); err == nil {
|
||||
for _, groupId := range currentUserGroupIds {
|
||||
if groupId == dockerGroup.Gid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Eventually, if any of the checks fail, we assume the user cannot run Docker commands
|
||||
return false
|
||||
}
|
||||
|
||||
// isDockerRunning checks if the Docker daemon is running by using the `docker info` command.
|
||||
func isDockerRunning() bool {
|
||||
cmd := exec.Command("docker", "info")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied
|
||||
func executeDockerComposeCommandWithArgs(args ...string) error {
|
||||
var cmd *exec.Cmd
|
||||
var useNewStyle bool
|
||||
|
||||
if !isDockerInstalled() {
|
||||
return fmt.Errorf("docker is not installed")
|
||||
}
|
||||
|
||||
checkCmd := exec.Command("docker", "compose", "version")
|
||||
if err := checkCmd.Run(); err == nil {
|
||||
useNewStyle = true
|
||||
} else {
|
||||
checkCmd = exec.Command("docker-compose", "version")
|
||||
if err := checkCmd.Run(); err == nil {
|
||||
useNewStyle = false
|
||||
} else {
|
||||
return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available")
|
||||
}
|
||||
}
|
||||
|
||||
if useNewStyle {
|
||||
cmd = exec.Command("docker", append([]string{"compose"}, args...)...)
|
||||
} else {
|
||||
cmd = exec.Command("docker-compose", args...)
|
||||
}
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// pullContainers pulls the containers using the appropriate command.
|
||||
func pullContainers() error {
|
||||
fmt.Println("Pulling the container images...")
|
||||
|
||||
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil {
|
||||
return fmt.Errorf("failed to pull the containers: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// startContainers starts the containers using the appropriate command.
|
||||
func startContainers() 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)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// stopContainers stops the containers using the appropriate command.
|
||||
func stopContainers() error {
|
||||
fmt.Println("Stopping containers...")
|
||||
|
||||
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "down"); err != nil {
|
||||
return fmt.Errorf("failed to stop containers: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// restartContainer restarts a specific container using the appropriate command.
|
||||
func restartContainer(container string) error {
|
||||
fmt.Println("Restarting containers...")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
source, err := os.Open(src)
|
||||
if err != nil {
|
||||
@@ -600,32 +485,89 @@ func moveFile(src, dst string) error {
|
||||
return os.Remove(src)
|
||||
}
|
||||
|
||||
func waitForContainer(containerName string) error {
|
||||
maxAttempts := 30
|
||||
retryInterval := time.Second * 2
|
||||
func printSetupToken(containerType SupportedContainer, dashboardDomain string) {
|
||||
fmt.Println("Waiting for Pangolin to generate setup token...")
|
||||
|
||||
for attempt := 0; attempt < maxAttempts; attempt++ {
|
||||
// Check if container is running
|
||||
cmd := exec.Command("docker", "container", "inspect", "-f", "{{.State.Running}}", containerName)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
// If the container doesn't exist or there's another error, wait and retry
|
||||
time.Sleep(retryInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
isRunning := strings.TrimSpace(out.String()) == "true"
|
||||
if isRunning {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Container exists but isn't running yet, wait and retry
|
||||
time.Sleep(retryInterval)
|
||||
// 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
|
||||
}
|
||||
|
||||
return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds()))
|
||||
// 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 {
|
||||
@@ -641,3 +583,45 @@ 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
|
||||
}
|
||||
|
||||
func checkIsPangolinInstalledWithHybrid() bool {
|
||||
// Check if config/config.yml exists and contains hybrid section
|
||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Read config file to check for hybrid section
|
||||
content, err := os.ReadFile("config/config.yml")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for hybrid section
|
||||
return bytes.Contains(content, []byte("managed:"))
|
||||
}
|
||||
|
||||
110
install/quickStart.go
Normal file
110
install/quickStart.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
FRONTEND_SECRET_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e"
|
||||
// CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start"
|
||||
CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start"
|
||||
)
|
||||
|
||||
// HybridCredentials represents the response from the cloud API
|
||||
type HybridCredentials struct {
|
||||
RemoteExitNodeId string `json:"remoteExitNodeId"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
// APIResponse represents the full response structure from the cloud API
|
||||
type APIResponse struct {
|
||||
Data HybridCredentials `json:"data"`
|
||||
}
|
||||
|
||||
// RequestPayload represents the request body structure
|
||||
type RequestPayload struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func generateValidationToken() string {
|
||||
timestamp := time.Now().UnixMilli()
|
||||
data := fmt.Sprintf("%s|%d", FRONTEND_SECRET_KEY, timestamp)
|
||||
obfuscated := make([]byte, len(data))
|
||||
for i, char := range []byte(data) {
|
||||
obfuscated[i] = char + 5
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(obfuscated)
|
||||
}
|
||||
|
||||
// requestHybridCredentials makes an HTTP POST request to the cloud API
|
||||
// to get hybrid credentials (ID and secret)
|
||||
func requestHybridCredentials() (*HybridCredentials, error) {
|
||||
// Generate validation token
|
||||
token := generateValidationToken()
|
||||
|
||||
// Create request payload
|
||||
payload := RequestPayload{
|
||||
Token: token,
|
||||
}
|
||||
|
||||
// Marshal payload to JSON
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request payload: %v", err)
|
||||
}
|
||||
|
||||
// Create HTTP request
|
||||
req, err := http.NewRequest("POST", CLOUD_API_URL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create HTTP request: %v", err)
|
||||
}
|
||||
|
||||
// Set headers
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-CSRF-Token", "x-csrf-protection")
|
||||
|
||||
// Create HTTP client with timeout
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
// Make the request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make HTTP request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check response status
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API request failed with status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Read response body for debugging
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
// Print the raw JSON response for debugging
|
||||
// fmt.Printf("Raw JSON response: %s\n", string(body))
|
||||
|
||||
// Parse response
|
||||
var apiResponse APIResponse
|
||||
if err := json.Unmarshal(body, &apiResponse); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode API response: %v", err)
|
||||
}
|
||||
|
||||
// Validate response data
|
||||
if apiResponse.Data.RemoteExitNodeId == "" || apiResponse.Data.Secret == "" {
|
||||
return nil, fmt.Errorf("invalid response: missing remoteExitNodeId or secret")
|
||||
}
|
||||
|
||||
return &apiResponse.Data, nil
|
||||
}
|
||||
1454
messages/bg-BG.json
Normal file
1454
messages/bg-BG.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,8 @@
|
||||
"setupErrorIdentifier": "ID organizace je již použito. Zvolte prosím jiné.",
|
||||
"componentsErrorNoMemberCreate": "Zatím nejste členem žádné organizace. Abyste mohli začít, vytvořte si organizaci.",
|
||||
"componentsErrorNoMember": "Zatím nejste členem žádných organizací.",
|
||||
"welcome": "Welcome!",
|
||||
"welcomeTo": "Welcome to",
|
||||
"welcome": "Vítejte!",
|
||||
"welcomeTo": "Vítejte v",
|
||||
"componentsCreateOrg": "Vytvořte organizaci",
|
||||
"componentsMember": "Jste členem {count, plural, =0 {0 organizací} one {1 organizace} other {# organizací}}.",
|
||||
"componentsInvalidKey": "Byly nalezeny neplatné nebo propadlé licenční klíče. Pokud chcete nadále používat všechny funkce, postupujte podle licenčních podmínek.",
|
||||
@@ -59,95 +59,96 @@
|
||||
"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",
|
||||
"siteSeeConfigOnce": "You will only be able to see the configuration once.",
|
||||
"siteLoadWGConfig": "Loading WireGuard configuration...",
|
||||
"siteDocker": "Expand for Docker Deployment Details",
|
||||
"toggle": "Toggle",
|
||||
"siteSeeConfigOnce": "Konfiguraci uvidíte pouze jednou.",
|
||||
"siteLoadWGConfig": "Načítání konfigurace WireGuard...",
|
||||
"siteDocker": "Rozbalit pro detaily nasazení v Dockeru",
|
||||
"toggle": "Přepínač",
|
||||
"dockerCompose": "Docker Compose",
|
||||
"dockerRun": "Docker Run",
|
||||
"siteLearnLocal": "Local sites do not tunnel, learn more",
|
||||
"siteConfirmCopy": "I have copied the config",
|
||||
"searchSitesProgress": "Search sites...",
|
||||
"siteAdd": "Add Site",
|
||||
"siteInstallNewt": "Install Newt",
|
||||
"siteInstallNewtDescription": "Get Newt running on your system",
|
||||
"WgConfiguration": "WireGuard Configuration",
|
||||
"WgConfigurationDescription": "Use the following configuration to connect to your network",
|
||||
"operatingSystem": "Operating System",
|
||||
"commands": "Commands",
|
||||
"recommended": "Recommended",
|
||||
"siteNewtDescription": "For the best user experience, use Newt. It uses WireGuard under the hood and allows you to address your private resources by their LAN address on your private network from within the Pangolin dashboard.",
|
||||
"siteRunsInDocker": "Runs in Docker",
|
||||
"siteRunsInShell": "Runs in shell on macOS, Linux, and Windows",
|
||||
"siteErrorDelete": "Error deleting site",
|
||||
"siteErrorUpdate": "Failed to update site",
|
||||
"siteErrorUpdateDescription": "An error occurred while updating the site.",
|
||||
"siteUpdated": "Site updated",
|
||||
"siteUpdatedDescription": "The site has been updated.",
|
||||
"siteGeneralDescription": "Configure the general settings for this site",
|
||||
"siteSettingDescription": "Configure the settings on your site",
|
||||
"siteSetting": "{siteName} Settings",
|
||||
"siteNewtTunnel": "Newt Tunnel (Recommended)",
|
||||
"siteNewtTunnelDescription": "Easiest way to create an entrypoint into your network. No extra setup.",
|
||||
"siteWg": "Basic WireGuard",
|
||||
"siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
|
||||
"siteLocalDescription": "Local resources only. No tunneling.",
|
||||
"siteSeeAll": "See All Sites",
|
||||
"siteTunnelDescription": "Determine how you want to connect to your site",
|
||||
"siteNewtCredentials": "Newt Credentials",
|
||||
"siteNewtCredentialsDescription": "This is how Newt will authenticate with the server",
|
||||
"siteCredentialsSave": "Save Your Credentials",
|
||||
"siteCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
|
||||
"siteInfo": "Site Information",
|
||||
"status": "Status",
|
||||
"shareTitle": "Manage Share Links",
|
||||
"shareDescription": "Create shareable links to grant temporary or permanent access to your resources",
|
||||
"shareSearch": "Search share links...",
|
||||
"shareCreate": "Create Share Link",
|
||||
"shareErrorDelete": "Failed to delete link",
|
||||
"shareErrorDeleteMessage": "An error occurred deleting link",
|
||||
"shareDeleted": "Link deleted",
|
||||
"shareDeletedDescription": "The link has been deleted",
|
||||
"shareTokenDescription": "Your access token can be passed in two ways: as a query parameter or in the request headers. These must be passed from the client on every request for authenticated access.",
|
||||
"accessToken": "Access Token",
|
||||
"usageExamples": "Usage Examples",
|
||||
"tokenId": "Token ID",
|
||||
"requestHeades": "Request Headers",
|
||||
"queryParameter": "Query Parameter",
|
||||
"importantNote": "Important Note",
|
||||
"shareImportantDescription": "For security reasons, using headers is recommended over query parameters when possible, as query parameters may be logged in server logs or browser history.",
|
||||
"siteLearnLocal": "Místní lokality se netunelují, dozvědět se více",
|
||||
"siteConfirmCopy": "Konfiguraci jsem zkopíroval",
|
||||
"searchSitesProgress": "Hledat lokality...",
|
||||
"siteAdd": "Přidat lokalitu",
|
||||
"siteInstallNewt": "Nainstalovat Newt",
|
||||
"siteInstallNewtDescription": "Spustit Newt na vašem systému",
|
||||
"WgConfiguration": "Konfigurace WireGuard",
|
||||
"WgConfigurationDescription": "Použijte následující konfiguraci pro připojení k vaší síti",
|
||||
"operatingSystem": "Operační systém",
|
||||
"commands": "Příkazy",
|
||||
"recommended": "Doporučeno",
|
||||
"siteNewtDescription": "Ideálně použijte Newt, který využívá WireGuard a umožňuje adresovat vaše soukromé zdroje pomocí jejich LAN adresy ve vaší privátní síti přímo z dashboardu Pangolin.",
|
||||
"siteRunsInDocker": "Běží v Dockeru",
|
||||
"siteRunsInShell": "Běží v shellu na macOS, Linuxu a Windows",
|
||||
"siteErrorDelete": "Chyba při odstraňování lokality",
|
||||
"siteErrorUpdate": "Nepodařilo se upravit lokalitu",
|
||||
"siteErrorUpdateDescription": "Při úpravě lokality došlo k chybě.",
|
||||
"siteUpdated": "Lokalita upravena",
|
||||
"siteUpdatedDescription": "Lokalita byla upravena.",
|
||||
"siteGeneralDescription": "Upravte obecná nastavení pro tuto lokalitu",
|
||||
"siteSettingDescription": "Upravte nastavení vaší lokality",
|
||||
"siteSetting": "Nastavení {siteName}",
|
||||
"siteNewtTunnel": "Tunel Newt (doporučeno)",
|
||||
"siteNewtTunnelDescription": "Nejjednodušší způsob, jak vytvořit vstupní bod do vaší sítě. Žádné další nastavení.",
|
||||
"siteWg": "Základní WireGuard",
|
||||
"siteWgDescription": "Použijte jakéhokoli klienta WireGuard abyste sestavili tunel. Vyžaduje se ruční nastavení NAT.",
|
||||
"siteWgDescriptionSaas": "Použijte jakéhokoli klienta WireGuard abyste sestavili tunel. Vyžaduje se ruční nastavení NAT. FUNGUJE POUZE NA SELF-HOSTED SERVERECH",
|
||||
"siteLocalDescription": "Pouze lokální zdroje. Žádný tunel.",
|
||||
"siteLocalDescriptionSaas": "Pouze lokální zdroje. Žádný tunel. FUNGUJE POUZE NA SELF-HOSTED SERVERECH",
|
||||
"siteSeeAll": "Zobrazit všechny lokality",
|
||||
"siteTunnelDescription": "Určete jak se chcete připojit k vaší lokalitě",
|
||||
"siteNewtCredentials": "Přihlašovací údaje Newt",
|
||||
"siteNewtCredentialsDescription": "Tímto způsobem se bude Newt autentizovat na serveru",
|
||||
"siteCredentialsSave": "Uložit přihlašovací údaje",
|
||||
"siteCredentialsSaveDescription": "Toto nastavení uvidíte pouze jednou. Ujistěte se, že jej zkopírujete na bezpečné místo.",
|
||||
"siteInfo": "Údaje o lokalitě",
|
||||
"status": "Stav",
|
||||
"shareTitle": "Spravovat sdílení odkazů",
|
||||
"shareDescription": "Vytvořte odkazy, abyste udělili dočasný nebo trvalý přístup k vašim zdrojům",
|
||||
"shareSearch": "Hledat sdílené odkazy...",
|
||||
"shareCreate": "Vytvořit odkaz",
|
||||
"shareErrorDelete": "Nepodařilo se odstranit odkaz",
|
||||
"shareErrorDeleteMessage": "Došlo k chybě při odstraňování odkazu",
|
||||
"shareDeleted": "Odkaz odstraněn",
|
||||
"shareDeletedDescription": "Odkaz byl odstraněn",
|
||||
"shareTokenDescription": "Váš přístupový token může být předán dvěma způsoby: jako parametr dotazu nebo v záhlaví požadavku. Tyto údaje musí být předány klientem v každé žádosti o ověřený přístup.",
|
||||
"accessToken": "Přístupový token",
|
||||
"usageExamples": "Příklady použití",
|
||||
"tokenId": "ID tokenu",
|
||||
"requestHeades": "Hlavičky požadavku",
|
||||
"queryParameter": "Parametry dotazu",
|
||||
"importantNote": "Důležité upozornění",
|
||||
"shareImportantDescription": "Z bezpečnostních důvodů je doporučeno používat raději hlavičky než parametry dotazu pokud je to možné, protože parametry dotazu mohou být zaznamenány v logu serveru nebo v historii prohlížeče.",
|
||||
"token": "Token",
|
||||
"shareTokenSecurety": "Keep your access token secure. Do not share it in publicly accessible areas or client-side code.",
|
||||
"shareErrorFetchResource": "Failed to fetch resources",
|
||||
"shareErrorFetchResourceDescription": "An error occurred while fetching the resources",
|
||||
"shareErrorCreate": "Failed to create share link",
|
||||
"shareErrorCreateDescription": "An error occurred while creating the share link",
|
||||
"shareCreateDescription": "Anyone with this link can access the resource",
|
||||
"shareTitleOptional": "Title (optional)",
|
||||
"expireIn": "Expire In",
|
||||
"neverExpire": "Never expire",
|
||||
"shareExpireDescription": "Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource.",
|
||||
"shareSeeOnce": "You will only be able to see this linkonce. Make sure to copy it.",
|
||||
"shareAccessHint": "Anyone with this link can access the resource. Share it with care.",
|
||||
"shareTokenUsage": "See Access Token Usage",
|
||||
"createLink": "Create Link",
|
||||
"resourcesNotFound": "No resources found",
|
||||
"resourceSearch": "Search resources",
|
||||
"openMenu": "Open menu",
|
||||
"resource": "Resource",
|
||||
"title": "Title",
|
||||
"created": "Created",
|
||||
"expires": "Expires",
|
||||
"never": "Never",
|
||||
"shareErrorSelectResource": "Please select a resource",
|
||||
"resourceTitle": "Manage Resources",
|
||||
"resourceDescription": "Create secure proxies to your private applications",
|
||||
"resourcesSearch": "Search resources...",
|
||||
"resourceAdd": "Add Resource",
|
||||
"shareTokenSecurety": "Uchovejte přístupový token v bezpečí. Nesdílejte jej na veřejně přístupných místěch nebo v kódu na straně klienta.",
|
||||
"shareErrorFetchResource": "Nepodařilo se načíst zdroje",
|
||||
"shareErrorFetchResourceDescription": "Při načítání zdrojů došlo k chybě",
|
||||
"shareErrorCreate": "Nepodařilo se vytvořit odkaz",
|
||||
"shareErrorCreateDescription": "Při vytváření odkazu došlo k chybě",
|
||||
"shareCreateDescription": "Kdokoliv s tímto odkazem může přistupovat ke zdroji",
|
||||
"shareTitleOptional": "Název (volitelné)",
|
||||
"expireIn": "Platnost vyprší za",
|
||||
"neverExpire": "Nikdy nevyprší",
|
||||
"shareExpireDescription": "Doba platnosti určuje, jak dlouho bude odkaz použitelný a bude poskytovat přístup ke zdroji. Po této době odkaz již nebude fungovat a uživatelé kteří tento odkaz používali ztratí přístup ke zdroji.",
|
||||
"shareSeeOnce": "Tento odkaz uvidíte pouze jednou. Ujistěte se, že jste jej zkopírovali.",
|
||||
"shareAccessHint": "Kdokoli s tímto odkazem může přistupovat ke zdroji. Sdílejte jej s rozvahou.",
|
||||
"shareTokenUsage": "Zobrazit využití přístupového tokenu",
|
||||
"createLink": "Vytvořit odkaz",
|
||||
"resourcesNotFound": "Nebyly nalezeny žádné zdroje",
|
||||
"resourceSearch": "Vyhledat zdroje",
|
||||
"openMenu": "Otevřít nabídku",
|
||||
"resource": "Zdroj",
|
||||
"title": "Název",
|
||||
"created": "Vytvořeno",
|
||||
"expires": "Vyprší",
|
||||
"never": "Nikdy",
|
||||
"shareErrorSelectResource": "Zvolte prosím zdroj",
|
||||
"resourceTitle": "Spravovat zdroje",
|
||||
"resourceDescription": "Vytvořte bezpečné proxy služby pro přístup k privátním aplikacím",
|
||||
"resourcesSearch": "Prohledat zdroje...",
|
||||
"resourceAdd": "Přidat zdroj",
|
||||
"resourceErrorDelte": "Error deleting resource",
|
||||
"authentication": "Authentication",
|
||||
"protected": "Protected",
|
||||
@@ -167,7 +168,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 +199,7 @@
|
||||
"general": "General",
|
||||
"generalSettings": "General Settings",
|
||||
"proxy": "Proxy",
|
||||
"internal": "Internal",
|
||||
"rules": "Rules",
|
||||
"resourceSettingDescription": "Configure the settings on your resource",
|
||||
"resourceSetting": "{resourceName} Settings",
|
||||
@@ -491,7 +493,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 +836,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",
|
||||
@@ -953,6 +973,7 @@
|
||||
"logoutError": "Error logging out",
|
||||
"signingAs": "Signed in as",
|
||||
"serverAdmin": "Server Admin",
|
||||
"managedSelfhosted": "Managed Self-Hosted",
|
||||
"otpEnable": "Enable Two-factor",
|
||||
"otpDisable": "Disable Two-factor",
|
||||
"logout": "Log Out",
|
||||
@@ -968,6 +989,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 +1047,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 +1123,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 +1191,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 +1225,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 +1235,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 +1295,7 @@
|
||||
"createDomainName": "Name:",
|
||||
"createDomainValue": "Value:",
|
||||
"createDomainCnameRecords": "CNAME Records",
|
||||
"createDomainARecords": "A Records",
|
||||
"createDomainRecordNumber": "Record {number}",
|
||||
"createDomainTxtRecords": "TXT Records",
|
||||
"createDomainSaveTheseRecords": "Save These Records",
|
||||
@@ -1273,5 +1303,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. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -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,28 @@
|
||||
"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.",
|
||||
"siteWgDescriptionSaas": "Verwenden Sie jeden WireGuard-Client, um einen Tunnel zu erstellen. Manuelles NAT-Setup erforderlich. FUNKTIONIERT NUR BEI SELBSTGEHOSTETEN KNOTEN",
|
||||
"siteLocalDescription": "Nur lokale Ressourcen. Kein Tunneling.",
|
||||
"siteSeeAll": "Alle Sites anzeigen",
|
||||
"siteTunnelDescription": "Lege fest, wie du dich mit deiner Site verbinden möchtest",
|
||||
"siteLocalDescriptionSaas": "Nur lokale Ressourcen. Keine Tunneldurchführung. FUNKTIONIERT NUR BEI SELBSTGEHOSTETEN KNOTEN",
|
||||
"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 +165,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 zum Ziel herstellen.",
|
||||
"resourceType": "Ressourcentyp",
|
||||
"resourceTypeDescription": "Legen Sie fest, wie Sie auf Ihre Ressource zugreifen möchten",
|
||||
"resourceHTTPSSettings": "HTTPS-Einstellungen",
|
||||
@@ -198,6 +199,7 @@
|
||||
"general": "Allgemein",
|
||||
"generalSettings": "Allgemeine Einstellungen",
|
||||
"proxy": "Proxy",
|
||||
"internal": "Intern",
|
||||
"rules": "Regeln",
|
||||
"resourceSettingDescription": "Konfigurieren Sie die Einstellungen Ihrer Ressource",
|
||||
"resourceSetting": "{resourceName} Einstellungen",
|
||||
@@ -303,7 +305,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 +345,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 +470,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",
|
||||
@@ -491,7 +493,7 @@
|
||||
"targetTlsSniDescription": "Der zu verwendende TLS-Servername für SNI. Leer lassen, um den Standard zu verwenden.",
|
||||
"targetTlsSubmit": "Einstellungen speichern",
|
||||
"targets": "Ziel-Konfiguration",
|
||||
"targetsDescription": "Richten Sie Ziele ein, um Datenverkehr zu Ihren Diensten zu leiten",
|
||||
"targetsDescription": "Richten Sie Ziele ein, um Datenverkehr zu Ihren Backend-Diensten zu leiten",
|
||||
"targetStickySessions": "Sticky Sessions aktivieren",
|
||||
"targetStickySessionsDescription": "Verbindungen für die gesamte Sitzung auf demselben Backend-Ziel halten.",
|
||||
"methodSelect": "Methode auswählen",
|
||||
@@ -559,8 +561,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 +680,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 +703,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 +711,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 +836,24 @@
|
||||
"pincodeRequirementsLength": "PIN muss genau 6 Ziffern lang sein",
|
||||
"pincodeRequirementsChars": "PIN darf nur Zahlen enthalten",
|
||||
"passwordRequirementsLength": "Passwort muss mindestens 1 Zeichen lang sein",
|
||||
"passwordRequirementsTitle": "Passwortanforderungen:",
|
||||
"passwordRequirementLength": "Mindestens 8 Zeichen lang",
|
||||
"passwordRequirementUppercase": "Mindestens ein Großbuchstabe",
|
||||
"passwordRequirementLowercase": "Mindestens ein Kleinbuchstabe",
|
||||
"passwordRequirementNumber": "Mindestens eine Zahl",
|
||||
"passwordRequirementSpecial": "Mindestens ein Sonderzeichen",
|
||||
"passwordRequirementsMet": "✓ Passwort erfüllt alle Anforderungen",
|
||||
"passwordStrength": "Passwortstärke",
|
||||
"passwordStrengthWeak": "Schwach",
|
||||
"passwordStrengthMedium": "Mittel",
|
||||
"passwordStrengthStrong": "Stark",
|
||||
"passwordRequirements": "Anforderungen:",
|
||||
"passwordRequirementLengthText": "8+ Zeichen",
|
||||
"passwordRequirementUppercaseText": "Großbuchstabe (A-Z)",
|
||||
"passwordRequirementLowercaseText": "Kleinbuchstabe (a-z)",
|
||||
"passwordRequirementNumberText": "Zahl (0-9)",
|
||||
"passwordRequirementSpecialText": "Sonderzeichen (!@#$%...)",
|
||||
"passwordsDoNotMatch": "Passwörter stimmen nicht überein",
|
||||
"otpEmailRequirementsLength": "OTP muss mindestens 1 Zeichen lang sein",
|
||||
"otpEmailSent": "OTP gesendet",
|
||||
"otpEmailSentDescription": "Ein OTP wurde an Ihre E-Mail gesendet",
|
||||
@@ -953,6 +973,7 @@
|
||||
"logoutError": "Fehler beim Abmelden",
|
||||
"signingAs": "Angemeldet als",
|
||||
"serverAdmin": "Server-Administrator",
|
||||
"managedSelfhosted": "Verwaltetes Selbsthosted",
|
||||
"otpEnable": "Zwei-Faktor aktivieren",
|
||||
"otpDisable": "Zwei-Faktor deaktivieren",
|
||||
"logout": "Abmelden",
|
||||
@@ -964,12 +985,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",
|
||||
"setupTokenDescription": "Geben Sie das Setup-Token von der Serverkonsole ein.",
|
||||
"setupTokenRequired": "Setup-Token ist erforderlich",
|
||||
"actionUpdateSite": "Standorte aktualisieren",
|
||||
"actionListSiteRoles": "Erlaubte Standort-Rollen auflisten",
|
||||
"actionCreateResource": "Ressource erstellen",
|
||||
"actionDeleteResource": "Ressource löschen",
|
||||
"actionGetResource": "Ressource abrufen",
|
||||
@@ -1023,6 +1047,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 +1103,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 +1111,7 @@
|
||||
"apiKeysErrorNoUpdate": "Kein API-Schlüssel zum Aktualisieren",
|
||||
"sidebarOverview": "Übersicht",
|
||||
"sidebarHome": "Zuhause",
|
||||
"sidebarSites": "Seiten",
|
||||
"sidebarSites": "Standorte",
|
||||
"sidebarResources": "Ressourcen",
|
||||
"sidebarAccessControl": "Zugriffskontrolle",
|
||||
"sidebarUsers": "Benutzer",
|
||||
@@ -1094,7 +1123,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 +1191,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 +1225,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 +1235,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 +1295,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 +1303,152 @@
|
||||
"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": "Fügen Sie CIDR-Bereiche hinzu, die über Clients von dieser Site aus remote zugänglich sind. Verwenden Sie ein Format wie 10.0.0.0/24. Dies gilt NUR für die VPN-Client-Konnektivität.",
|
||||
"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",
|
||||
"addNewTarget": "Neues Ziel hinzufügen",
|
||||
"targetsList": "Ziel-Liste",
|
||||
"targetErrorDuplicateTargetFound": "Doppeltes Ziel gefunden",
|
||||
"httpMethod": "HTTP-Methode",
|
||||
"selectHttpMethod": "HTTP-Methode auswählen",
|
||||
"domainPickerSubdomainLabel": "Subdomain",
|
||||
"domainPickerBaseDomainLabel": "Basisdomäne",
|
||||
"domainPickerSearchDomains": "Domains suchen...",
|
||||
"domainPickerNoDomainsFound": "Keine Domains gefunden",
|
||||
"domainPickerLoadingDomains": "Domains werden geladen...",
|
||||
"domainPickerSelectBaseDomain": "Basisdomäne auswählen...",
|
||||
"domainPickerNotAvailableForCname": "Für CNAME-Domains nicht verfügbar",
|
||||
"domainPickerEnterSubdomainOrLeaveBlank": "Geben Sie eine Subdomain ein oder lassen Sie das Feld leer, um die Basisdomäne zu verwenden.",
|
||||
"domainPickerEnterSubdomainToSearch": "Geben Sie eine Subdomain ein, um verfügbare freie Domains zu suchen und auszuwählen.",
|
||||
"domainPickerFreeDomains": "Freie Domains",
|
||||
"domainPickerSearchForAvailableDomains": "Verfügbare Domains suchen",
|
||||
"resourceDomain": "Domain",
|
||||
"resourceEditDomain": "Domain bearbeiten",
|
||||
"siteName": "Site-Name",
|
||||
"proxyPort": "Port",
|
||||
"resourcesTableProxyResources": "Proxy-Ressourcen",
|
||||
"resourcesTableClientResources": "Client-Ressourcen",
|
||||
"resourcesTableNoProxyResourcesFound": "Keine Proxy-Ressourcen gefunden.",
|
||||
"resourcesTableNoInternalResourcesFound": "Keine internen Ressourcen gefunden.",
|
||||
"resourcesTableDestination": "Ziel",
|
||||
"resourcesTableTheseResourcesForUseWith": "Diese Ressourcen sind zur Verwendung mit",
|
||||
"resourcesTableClients": "Kunden",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "und sind nur intern zugänglich, wenn mit einem Client verbunden.",
|
||||
"editInternalResourceDialogEditClientResource": "Client-Ressource bearbeiten",
|
||||
"editInternalResourceDialogUpdateResourceProperties": "Aktualisieren Sie die Ressourceneigenschaften und die Zielkonfiguration für {resourceName}.",
|
||||
"editInternalResourceDialogResourceProperties": "Ressourceneigenschaften",
|
||||
"editInternalResourceDialogName": "Name",
|
||||
"editInternalResourceDialogProtocol": "Protokoll",
|
||||
"editInternalResourceDialogSitePort": "Site-Port",
|
||||
"editInternalResourceDialogTargetConfiguration": "Zielkonfiguration",
|
||||
"editInternalResourceDialogDestinationIP": "Ziel-IP",
|
||||
"editInternalResourceDialogDestinationPort": "Ziel-Port",
|
||||
"editInternalResourceDialogCancel": "Abbrechen",
|
||||
"editInternalResourceDialogSaveResource": "Ressource speichern",
|
||||
"editInternalResourceDialogSuccess": "Erfolg",
|
||||
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Interne Ressource erfolgreich aktualisiert",
|
||||
"editInternalResourceDialogError": "Fehler",
|
||||
"editInternalResourceDialogFailedToUpdateInternalResource": "Interne Ressource konnte nicht aktualisiert werden",
|
||||
"editInternalResourceDialogNameRequired": "Name ist erforderlich",
|
||||
"editInternalResourceDialogNameMaxLength": "Der Name darf nicht länger als 255 Zeichen sein",
|
||||
"editInternalResourceDialogProxyPortMin": "Proxy-Port muss mindestens 1 sein",
|
||||
"editInternalResourceDialogProxyPortMax": "Proxy-Port muss kleiner als 65536 sein",
|
||||
"editInternalResourceDialogInvalidIPAddressFormat": "Ungültiges IP-Adressformat",
|
||||
"editInternalResourceDialogDestinationPortMin": "Ziel-Port muss mindestens 1 sein",
|
||||
"editInternalResourceDialogDestinationPortMax": "Ziel-Port muss kleiner als 65536 sein",
|
||||
"createInternalResourceDialogNoSitesAvailable": "Keine Sites verfügbar",
|
||||
"createInternalResourceDialogNoSitesAvailableDescription": "Sie müssen mindestens eine Newt-Site mit einem konfigurierten Subnetz haben, um interne Ressourcen zu erstellen.",
|
||||
"createInternalResourceDialogClose": "Schließen",
|
||||
"createInternalResourceDialogCreateClientResource": "Ressource erstellen",
|
||||
"createInternalResourceDialogCreateClientResourceDescription": "Erstellen Sie eine neue Ressource, die für Clients zugänglich ist, die mit der ausgewählten Site verbunden sind.",
|
||||
"createInternalResourceDialogResourceProperties": "Ressourceneigenschaften",
|
||||
"createInternalResourceDialogName": "Name",
|
||||
"createInternalResourceDialogSite": "Standort",
|
||||
"createInternalResourceDialogSelectSite": "Standort auswählen...",
|
||||
"createInternalResourceDialogSearchSites": "Sites durchsuchen...",
|
||||
"createInternalResourceDialogNoSitesFound": "Keine Standorte gefunden.",
|
||||
"createInternalResourceDialogProtocol": "Protokoll",
|
||||
"createInternalResourceDialogTcp": "TCP",
|
||||
"createInternalResourceDialogUdp": "UDP",
|
||||
"createInternalResourceDialogSitePort": "Site-Port",
|
||||
"createInternalResourceDialogSitePortDescription": "Verwenden Sie diesen Port, um bei Verbindung mit einem Client auf die Ressource an der Site zuzugreifen.",
|
||||
"createInternalResourceDialogTargetConfiguration": "Zielkonfiguration",
|
||||
"createInternalResourceDialogDestinationIP": "Ziel-IP",
|
||||
"createInternalResourceDialogDestinationIPDescription": "Die IP-Adresse der Ressource im Netzwerkstandort der Site.",
|
||||
"createInternalResourceDialogDestinationPort": "Ziel-Port",
|
||||
"createInternalResourceDialogDestinationPortDescription": "Der Port auf der Ziel-IP, unter dem die Ressource zugänglich ist.",
|
||||
"createInternalResourceDialogCancel": "Abbrechen",
|
||||
"createInternalResourceDialogCreateResource": "Ressource erstellen",
|
||||
"createInternalResourceDialogSuccess": "Erfolg",
|
||||
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Interne Ressource erfolgreich erstellt",
|
||||
"createInternalResourceDialogError": "Fehler",
|
||||
"createInternalResourceDialogFailedToCreateInternalResource": "Interne Ressource konnte nicht erstellt werden",
|
||||
"createInternalResourceDialogNameRequired": "Name ist erforderlich",
|
||||
"createInternalResourceDialogNameMaxLength": "Der Name darf nicht länger als 255 Zeichen sein",
|
||||
"createInternalResourceDialogPleaseSelectSite": "Bitte wählen Sie eine Site aus",
|
||||
"createInternalResourceDialogProxyPortMin": "Proxy-Port muss mindestens 1 sein",
|
||||
"createInternalResourceDialogProxyPortMax": "Proxy-Port muss kleiner als 65536 sein",
|
||||
"createInternalResourceDialogInvalidIPAddressFormat": "Ungültiges IP-Adressformat",
|
||||
"createInternalResourceDialogDestinationPortMin": "Ziel-Port muss mindestens 1 sein",
|
||||
"createInternalResourceDialogDestinationPortMax": "Ziel-Port muss kleiner als 65536 sein",
|
||||
"siteConfiguration": "Konfiguration",
|
||||
"siteAcceptClientConnections": "Clientverbindungen akzeptieren",
|
||||
"siteAcceptClientConnectionsDescription": "Erlauben Sie anderen Geräten, über diese Newt-Instanz mit Clients als Gateway zu verbinden.",
|
||||
"siteAddress": "Site-Adresse",
|
||||
"siteAddressDescription": "Geben Sie die IP-Adresse des Hosts an, mit dem sich die Clients verbinden sollen. Dies ist die interne Adresse der Site im Pangolin-Netzwerk, die von Clients angesprochen werden muss. Muss innerhalb des Unternehmens-Subnetzes liegen.",
|
||||
"autoLoginExternalIdp": "Automatische Anmeldung mit externem IDP",
|
||||
"autoLoginExternalIdpDescription": "Leiten Sie den Benutzer sofort zur Authentifizierung an den externen IDP weiter.",
|
||||
"selectIdp": "IDP auswählen",
|
||||
"selectIdpPlaceholder": "Wählen Sie einen IDP...",
|
||||
"selectIdpRequired": "Bitte wählen Sie einen IDP aus, wenn automatische Anmeldung aktiviert ist.",
|
||||
"autoLoginTitle": "Weiterleitung",
|
||||
"autoLoginDescription": "Sie werden zum externen Identitätsanbieter zur Authentifizierung weitergeleitet.",
|
||||
"autoLoginProcessing": "Authentifizierung vorbereiten...",
|
||||
"autoLoginRedirecting": "Weiterleitung zur Anmeldung...",
|
||||
"autoLoginError": "Fehler bei der automatischen Anmeldung",
|
||||
"autoLoginErrorNoRedirectUrl": "Keine Weiterleitungs-URL vom Identitätsanbieter erhalten.",
|
||||
"autoLoginErrorGeneratingUrl": "Fehler beim Generieren der Authentifizierungs-URL."
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -95,7 +94,9 @@
|
||||
"siteNewtTunnelDescription": "Easiest way to create an entrypoint into your network. No extra setup.",
|
||||
"siteWg": "Basic WireGuard",
|
||||
"siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
|
||||
"siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES",
|
||||
"siteLocalDescription": "Local resources only. No tunneling.",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES",
|
||||
"siteSeeAll": "See All Sites",
|
||||
"siteTunnelDescription": "Determine how you want to connect to your site",
|
||||
"siteNewtCredentials": "Newt Credentials",
|
||||
@@ -167,7 +168,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 +199,7 @@
|
||||
"general": "General",
|
||||
"generalSettings": "General Settings",
|
||||
"proxy": "Proxy",
|
||||
"internal": "Internal",
|
||||
"rules": "Rules",
|
||||
"resourceSettingDescription": "Configure the settings on your resource",
|
||||
"resourceSetting": "{resourceName} Settings",
|
||||
@@ -491,7 +493,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 +836,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",
|
||||
@@ -953,6 +973,7 @@
|
||||
"logoutError": "Error logging out",
|
||||
"signingAs": "Signed in as",
|
||||
"serverAdmin": "Server Admin",
|
||||
"managedSelfhosted": "Managed Self-Hosted",
|
||||
"otpEnable": "Enable Two-factor",
|
||||
"otpDisable": "Disable Two-factor",
|
||||
"logout": "Log Out",
|
||||
@@ -968,6 +989,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 +1047,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 +1123,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 +1191,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 +1225,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 +1235,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}",
|
||||
@@ -1274,5 +1303,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. This is the internal address of the site in the Pangolin network for clients to address. Must fall within the Org subnet.",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -95,7 +94,9 @@
|
||||
"siteNewtTunnelDescription": "La forma más fácil de crear un punto de entrada en tu red. Sin configuración adicional.",
|
||||
"siteWg": "Wirex Guardia Básica",
|
||||
"siteWgDescription": "Utilice cualquier cliente Wirex Guard para establecer un túnel. Se requiere una configuración manual de NAT.",
|
||||
"siteWgDescriptionSaas": "Utilice cualquier cliente de WireGuard para establecer un túnel. Se requiere configuración manual de NAT. SOLO FUNCIONA EN NODOS AUTOGESTIONADOS",
|
||||
"siteLocalDescription": "Solo recursos locales. Sin túneles.",
|
||||
"siteLocalDescriptionSaas": "Solo recursos locales. Sin túneles. SOLO FUNCIONA EN NODOS AUTOGESTIONADOS",
|
||||
"siteSeeAll": "Ver todos los sitios",
|
||||
"siteTunnelDescription": "Determina cómo quieres conectarte a tu sitio",
|
||||
"siteNewtCredentials": "Credenciales nuevas",
|
||||
@@ -167,7 +168,7 @@
|
||||
"siteSelect": "Seleccionar sitio",
|
||||
"siteSearch": "Buscar sitio",
|
||||
"siteNotFound": "Sitio no encontrado.",
|
||||
"siteSelectionDescription": "Este sitio proporcionará conectividad al recurso.",
|
||||
"siteSelectionDescription": "Este sitio proporcionará conectividad al objetivo.",
|
||||
"resourceType": "Tipo de recurso",
|
||||
"resourceTypeDescription": "Determina cómo quieres acceder a tu recurso",
|
||||
"resourceHTTPSSettings": "Configuración HTTPS",
|
||||
@@ -198,6 +199,7 @@
|
||||
"general": "General",
|
||||
"generalSettings": "Configuración General",
|
||||
"proxy": "Proxy",
|
||||
"internal": "Interno",
|
||||
"rules": "Reglas",
|
||||
"resourceSettingDescription": "Configure la configuración de su recurso",
|
||||
"resourceSetting": "Ajustes {resourceName}",
|
||||
@@ -834,6 +836,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": "Requisitos de la contraseña:",
|
||||
"passwordRequirementLength": "Al menos 8 caracteres de largo",
|
||||
"passwordRequirementUppercase": "Al menos una letra mayúscula",
|
||||
"passwordRequirementLowercase": "Al menos una letra minúscula",
|
||||
"passwordRequirementNumber": "Al menos un número",
|
||||
"passwordRequirementSpecial": "Al menos un carácter especial",
|
||||
"passwordRequirementsMet": "✓ La contraseña cumple con todos los requisitos",
|
||||
"passwordStrength": "Seguridad de la contraseña",
|
||||
"passwordStrengthWeak": "Débil",
|
||||
"passwordStrengthMedium": "Media",
|
||||
"passwordStrengthStrong": "Fuerte",
|
||||
"passwordRequirements": "Requisitos:",
|
||||
"passwordRequirementLengthText": "8+ caracteres",
|
||||
"passwordRequirementUppercaseText": "Letra mayúscula (A-Z)",
|
||||
"passwordRequirementLowercaseText": "Letra minúscula (a-z)",
|
||||
"passwordRequirementNumberText": "Número (0-9)",
|
||||
"passwordRequirementSpecialText": "Caracter especial (!@#$%...)",
|
||||
"passwordsDoNotMatch": "Las contraseñas no coinciden",
|
||||
"otpEmailRequirementsLength": "OTP debe tener al menos 1 carácter",
|
||||
"otpEmailSent": "OTP enviado",
|
||||
"otpEmailSentDescription": "Un OTP ha sido enviado a tu correo electrónico",
|
||||
@@ -953,6 +973,7 @@
|
||||
"logoutError": "Error al cerrar sesión",
|
||||
"signingAs": "Conectado como",
|
||||
"serverAdmin": "Admin Servidor",
|
||||
"managedSelfhosted": "Autogestionado",
|
||||
"otpEnable": "Activar doble factor",
|
||||
"otpDisable": "Desactivar doble factor",
|
||||
"logout": "Cerrar sesión",
|
||||
@@ -968,6 +989,9 @@
|
||||
"actionDeleteSite": "Eliminar sitio",
|
||||
"actionGetSite": "Obtener sitio",
|
||||
"actionListSites": "Listar sitios",
|
||||
"setupToken": "Configuración de token",
|
||||
"setupTokenDescription": "Ingrese el token de configuración desde la consola del servidor.",
|
||||
"setupTokenRequired": "Se requiere el token de configuración",
|
||||
"actionUpdateSite": "Actualizar sitio",
|
||||
"actionListSiteRoles": "Lista de roles permitidos del sitio",
|
||||
"actionCreateResource": "Crear Recurso",
|
||||
@@ -1023,6 +1047,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 +1123,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 +1191,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 +1225,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 +1235,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 +1295,7 @@
|
||||
"createDomainName": "Nombre:",
|
||||
"createDomainValue": "Valor:",
|
||||
"createDomainCnameRecords": "Registros CNAME",
|
||||
"createDomainARecords": "Registros A",
|
||||
"createDomainRecordNumber": "Registro {number}",
|
||||
"createDomainTxtRecords": "Registros TXT",
|
||||
"createDomainSaveTheseRecords": "Guardar estos registros",
|
||||
@@ -1273,5 +1303,152 @@
|
||||
"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": "Agregue rangos CIDR que se puedan acceder desde este sitio de forma remota usando clientes. Utilice el formato como 10.0.0.0/24. Esto SOLO se aplica a la conectividad del cliente VPN.",
|
||||
"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",
|
||||
"addNewTarget": "Agregar nuevo destino",
|
||||
"targetsList": "Lista de destinos",
|
||||
"targetErrorDuplicateTargetFound": "Se encontró un destino duplicado",
|
||||
"httpMethod": "Método HTTP",
|
||||
"selectHttpMethod": "Seleccionar método HTTP",
|
||||
"domainPickerSubdomainLabel": "Subdominio",
|
||||
"domainPickerBaseDomainLabel": "Dominio base",
|
||||
"domainPickerSearchDomains": "Buscar dominios...",
|
||||
"domainPickerNoDomainsFound": "No se encontraron dominios",
|
||||
"domainPickerLoadingDomains": "Cargando dominios...",
|
||||
"domainPickerSelectBaseDomain": "Seleccionar dominio base...",
|
||||
"domainPickerNotAvailableForCname": "No disponible para dominios CNAME",
|
||||
"domainPickerEnterSubdomainOrLeaveBlank": "Ingrese subdominio o deje en blanco para usar dominio base.",
|
||||
"domainPickerEnterSubdomainToSearch": "Ingrese un subdominio para buscar y seleccionar entre dominios gratuitos disponibles.",
|
||||
"domainPickerFreeDomains": "Dominios gratuitos",
|
||||
"domainPickerSearchForAvailableDomains": "Buscar dominios disponibles",
|
||||
"resourceDomain": "Dominio",
|
||||
"resourceEditDomain": "Editar dominio",
|
||||
"siteName": "Nombre del sitio",
|
||||
"proxyPort": "Puerto",
|
||||
"resourcesTableProxyResources": "Recursos de proxy",
|
||||
"resourcesTableClientResources": "Recursos del cliente",
|
||||
"resourcesTableNoProxyResourcesFound": "No se encontraron recursos de proxy.",
|
||||
"resourcesTableNoInternalResourcesFound": "No se encontraron recursos internos.",
|
||||
"resourcesTableDestination": "Destino",
|
||||
"resourcesTableTheseResourcesForUseWith": "Estos recursos son para uso con",
|
||||
"resourcesTableClients": "Clientes",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "y solo son accesibles internamente cuando se conectan con un cliente.",
|
||||
"editInternalResourceDialogEditClientResource": "Editar recurso del cliente",
|
||||
"editInternalResourceDialogUpdateResourceProperties": "Actualizar las propiedades del recurso y la configuración del objetivo para {resourceName}.",
|
||||
"editInternalResourceDialogResourceProperties": "Propiedades del recurso",
|
||||
"editInternalResourceDialogName": "Nombre",
|
||||
"editInternalResourceDialogProtocol": "Protocolo",
|
||||
"editInternalResourceDialogSitePort": "Puerto del sitio",
|
||||
"editInternalResourceDialogTargetConfiguration": "Configuración de objetivos",
|
||||
"editInternalResourceDialogDestinationIP": "IP de destino",
|
||||
"editInternalResourceDialogDestinationPort": "Puerto de destino",
|
||||
"editInternalResourceDialogCancel": "Cancelar",
|
||||
"editInternalResourceDialogSaveResource": "Guardar recurso",
|
||||
"editInternalResourceDialogSuccess": "Éxito",
|
||||
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Recurso interno actualizado con éxito",
|
||||
"editInternalResourceDialogError": "Error",
|
||||
"editInternalResourceDialogFailedToUpdateInternalResource": "Error al actualizar el recurso interno",
|
||||
"editInternalResourceDialogNameRequired": "El nombre es requerido",
|
||||
"editInternalResourceDialogNameMaxLength": "El nombre no debe tener más de 255 caracteres",
|
||||
"editInternalResourceDialogProxyPortMin": "El puerto del proxy debe ser al menos 1",
|
||||
"editInternalResourceDialogProxyPortMax": "El puerto del proxy debe ser menor de 65536",
|
||||
"editInternalResourceDialogInvalidIPAddressFormat": "Formato de dirección IP inválido",
|
||||
"editInternalResourceDialogDestinationPortMin": "El puerto de destino debe ser al menos 1",
|
||||
"editInternalResourceDialogDestinationPortMax": "El puerto de destino debe ser menor de 65536",
|
||||
"createInternalResourceDialogNoSitesAvailable": "No hay sitios disponibles",
|
||||
"createInternalResourceDialogNoSitesAvailableDescription": "Necesita tener al menos un sitio de Newt con una subred configurada para crear recursos internos.",
|
||||
"createInternalResourceDialogClose": "Cerrar",
|
||||
"createInternalResourceDialogCreateClientResource": "Crear recurso del cliente",
|
||||
"createInternalResourceDialogCreateClientResourceDescription": "Crear un nuevo recurso que será accesible para los clientes conectados al sitio seleccionado.",
|
||||
"createInternalResourceDialogResourceProperties": "Propiedades del recurso",
|
||||
"createInternalResourceDialogName": "Nombre",
|
||||
"createInternalResourceDialogSite": "Sitio",
|
||||
"createInternalResourceDialogSelectSite": "Seleccionar sitio...",
|
||||
"createInternalResourceDialogSearchSites": "Buscar sitios...",
|
||||
"createInternalResourceDialogNoSitesFound": "Sitios no encontrados.",
|
||||
"createInternalResourceDialogProtocol": "Protocolo",
|
||||
"createInternalResourceDialogTcp": "TCP",
|
||||
"createInternalResourceDialogUdp": "UDP",
|
||||
"createInternalResourceDialogSitePort": "Puerto del sitio",
|
||||
"createInternalResourceDialogSitePortDescription": "Use este puerto para acceder al recurso en el sitio cuando se conecta con un cliente.",
|
||||
"createInternalResourceDialogTargetConfiguration": "Configuración de objetivos",
|
||||
"createInternalResourceDialogDestinationIP": "IP de destino",
|
||||
"createInternalResourceDialogDestinationIPDescription": "La dirección IP del recurso en la red del sitio.",
|
||||
"createInternalResourceDialogDestinationPort": "Puerto de destino",
|
||||
"createInternalResourceDialogDestinationPortDescription": "El puerto en la IP de destino donde el recurso es accesible.",
|
||||
"createInternalResourceDialogCancel": "Cancelar",
|
||||
"createInternalResourceDialogCreateResource": "Crear recurso",
|
||||
"createInternalResourceDialogSuccess": "Éxito",
|
||||
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Recurso interno creado con éxito",
|
||||
"createInternalResourceDialogError": "Error",
|
||||
"createInternalResourceDialogFailedToCreateInternalResource": "Error al crear recurso interno",
|
||||
"createInternalResourceDialogNameRequired": "El nombre es requerido",
|
||||
"createInternalResourceDialogNameMaxLength": "El nombre debe ser menor de 255 caracteres",
|
||||
"createInternalResourceDialogPleaseSelectSite": "Por favor seleccione un sitio",
|
||||
"createInternalResourceDialogProxyPortMin": "El puerto del proxy debe ser al menos 1",
|
||||
"createInternalResourceDialogProxyPortMax": "El puerto del proxy debe ser menor de 65536",
|
||||
"createInternalResourceDialogInvalidIPAddressFormat": "Formato de dirección IP inválido",
|
||||
"createInternalResourceDialogDestinationPortMin": "El puerto de destino debe ser al menos 1",
|
||||
"createInternalResourceDialogDestinationPortMax": "El puerto de destino debe ser menor de 65536",
|
||||
"siteConfiguration": "Configuración",
|
||||
"siteAcceptClientConnections": "Aceptar conexiones de clientes",
|
||||
"siteAcceptClientConnectionsDescription": "Permitir que otros dispositivos se conecten a través de esta instancia Newt como una puerta de enlace utilizando clientes.",
|
||||
"siteAddress": "Dirección del sitio",
|
||||
"siteAddressDescription": "Especifique la dirección IP del host que los clientes deben usar para conectarse. Esta es la dirección interna del sitio en la red de Pangolín para que los clientes dirijan. Debe estar dentro de la subred de la organización.",
|
||||
"autoLoginExternalIdp": "Inicio de sesión automático con IDP externo",
|
||||
"autoLoginExternalIdpDescription": "Redirigir inmediatamente al usuario al IDP externo para autenticación.",
|
||||
"selectIdp": "Seleccionar IDP",
|
||||
"selectIdpPlaceholder": "Elegir un IDP...",
|
||||
"selectIdpRequired": "Por favor seleccione un IDP cuando el inicio de sesión automático esté habilitado.",
|
||||
"autoLoginTitle": "Redirigiendo",
|
||||
"autoLoginDescription": "Te estamos redirigiendo al proveedor de identidad externo para autenticación.",
|
||||
"autoLoginProcessing": "Preparando autenticación...",
|
||||
"autoLoginRedirecting": "Redirigiendo al inicio de sesión...",
|
||||
"autoLoginError": "Error de inicio de sesión automático",
|
||||
"autoLoginErrorNoRedirectUrl": "No se recibió URL de redirección del proveedor de identidad.",
|
||||
"autoLoginErrorGeneratingUrl": "Error al generar URL de autenticación."
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -95,7 +94,9 @@
|
||||
"siteNewtTunnelDescription": "La façon la plus simple de créer un point d'entrée dans votre réseau. Pas de configuration supplémentaire.",
|
||||
"siteWg": "WireGuard basique",
|
||||
"siteWgDescription": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise.",
|
||||
"siteWgDescriptionSaas": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES",
|
||||
"siteLocalDescription": "Ressources locales seulement. Pas de tunneling.",
|
||||
"siteLocalDescriptionSaas": "Ressources locales uniquement. Pas de tunneling. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES",
|
||||
"siteSeeAll": "Voir tous les sites",
|
||||
"siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre site",
|
||||
"siteNewtCredentials": "Identifiants Newt",
|
||||
@@ -167,7 +168,7 @@
|
||||
"siteSelect": "Sélectionner un site",
|
||||
"siteSearch": "Chercher un site",
|
||||
"siteNotFound": "Aucun site trouvé.",
|
||||
"siteSelectionDescription": "Ce site fournira la connectivité à la ressource.",
|
||||
"siteSelectionDescription": "Ce site fournira la connectivité à la cible.",
|
||||
"resourceType": "Type de ressource",
|
||||
"resourceTypeDescription": "Déterminer comment vous voulez accéder à votre ressource",
|
||||
"resourceHTTPSSettings": "Paramètres HTTPS",
|
||||
@@ -198,6 +199,7 @@
|
||||
"general": "Généraux",
|
||||
"generalSettings": "Paramètres généraux",
|
||||
"proxy": "Proxy",
|
||||
"internal": "Interne",
|
||||
"rules": "Règles",
|
||||
"resourceSettingDescription": "Configurer les paramètres de votre ressource",
|
||||
"resourceSetting": "Réglages {resourceName}",
|
||||
@@ -491,7 +493,7 @@
|
||||
"targetTlsSniDescription": "Le nom de serveur TLS à utiliser pour SNI. Laissez vide pour utiliser la valeur par défaut.",
|
||||
"targetTlsSubmit": "Enregistrer les paramètres",
|
||||
"targets": "Configuration des cibles",
|
||||
"targetsDescription": "Configurez les cibles pour router le trafic vers vos services",
|
||||
"targetsDescription": "Configurez les cibles pour router le trafic vers vos services.",
|
||||
"targetStickySessions": "Activer les sessions persistantes",
|
||||
"targetStickySessionsDescription": "Maintenir les connexions sur la même cible backend pendant toute leur session.",
|
||||
"methodSelect": "Sélectionner la méthode",
|
||||
@@ -834,6 +836,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": "Exigences relatives au mot de passe :",
|
||||
"passwordRequirementLength": "Au moins 8 caractères",
|
||||
"passwordRequirementUppercase": "Au moins une lettre majuscule",
|
||||
"passwordRequirementLowercase": "Au moins une lettre minuscule",
|
||||
"passwordRequirementNumber": "Au moins un chiffre",
|
||||
"passwordRequirementSpecial": "Au moins un caractère spécial",
|
||||
"passwordRequirementsMet": "✓ Le mot de passe répond à toutes les exigences",
|
||||
"passwordStrength": "Solidité du mot de passe",
|
||||
"passwordStrengthWeak": "Faible",
|
||||
"passwordStrengthMedium": "Moyen",
|
||||
"passwordStrengthStrong": "Fort",
|
||||
"passwordRequirements": "Exigences :",
|
||||
"passwordRequirementLengthText": "8+ caractères",
|
||||
"passwordRequirementUppercaseText": "Lettre majuscule (A-Z)",
|
||||
"passwordRequirementLowercaseText": "Lettre minuscule (a-z)",
|
||||
"passwordRequirementNumberText": "Nombre (0-9)",
|
||||
"passwordRequirementSpecialText": "Caractère spécial (!@#$%...)",
|
||||
"passwordsDoNotMatch": "Les mots de passe ne correspondent pas",
|
||||
"otpEmailRequirementsLength": "L'OTP doit comporter au moins 1 caractère",
|
||||
"otpEmailSent": "OTP envoyé",
|
||||
"otpEmailSentDescription": "Un OTP a été envoyé à votre e-mail",
|
||||
@@ -953,6 +973,7 @@
|
||||
"logoutError": "Erreur lors de la déconnexion",
|
||||
"signingAs": "Connecté en tant que",
|
||||
"serverAdmin": "Admin Serveur",
|
||||
"managedSelfhosted": "Gestion autonome",
|
||||
"otpEnable": "Activer l'authentification à deux facteurs",
|
||||
"otpDisable": "Désactiver l'authentification à deux facteurs",
|
||||
"logout": "Déconnexion",
|
||||
@@ -968,6 +989,9 @@
|
||||
"actionDeleteSite": "Supprimer un site",
|
||||
"actionGetSite": "Obtenir un site",
|
||||
"actionListSites": "Lister les sites",
|
||||
"setupToken": "Jeton de configuration",
|
||||
"setupTokenDescription": "Entrez le jeton de configuration depuis la console du serveur.",
|
||||
"setupTokenRequired": "Le jeton de configuration est requis.",
|
||||
"actionUpdateSite": "Mettre à jour un site",
|
||||
"actionListSiteRoles": "Lister les rôles autorisés du site",
|
||||
"actionCreateResource": "Créer une ressource",
|
||||
@@ -1023,6 +1047,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 +1123,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 +1191,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 +1225,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 +1235,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 +1295,7 @@
|
||||
"createDomainName": "Nom :",
|
||||
"createDomainValue": "Valeur :",
|
||||
"createDomainCnameRecords": "Enregistrements CNAME",
|
||||
"createDomainARecords": "Enregistrements A",
|
||||
"createDomainRecordNumber": "Enregistrement {number}",
|
||||
"createDomainTxtRecords": "Enregistrements TXT",
|
||||
"createDomainSaveTheseRecords": "Enregistrez ces enregistrements",
|
||||
@@ -1273,5 +1303,152 @@
|
||||
"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": "Ajoutez des plages CIDR accessibles à distance depuis ce site à l'aide de clients. Utilisez le format comme 10.0.0.0/24. Cela s'applique UNIQUEMENT à la connectivité des clients VPN.",
|
||||
"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é",
|
||||
"addNewTarget": "Ajouter une nouvelle cible",
|
||||
"targetsList": "Liste des cibles",
|
||||
"targetErrorDuplicateTargetFound": "Cible en double trouvée",
|
||||
"httpMethod": "Méthode HTTP",
|
||||
"selectHttpMethod": "Sélectionnez la méthode HTTP",
|
||||
"domainPickerSubdomainLabel": "Sous-domaine",
|
||||
"domainPickerBaseDomainLabel": "Domaine de base",
|
||||
"domainPickerSearchDomains": "Rechercher des domaines...",
|
||||
"domainPickerNoDomainsFound": "Aucun domaine trouvé",
|
||||
"domainPickerLoadingDomains": "Chargement des domaines...",
|
||||
"domainPickerSelectBaseDomain": "Sélectionnez le domaine de base...",
|
||||
"domainPickerNotAvailableForCname": "Non disponible pour les domaines CNAME",
|
||||
"domainPickerEnterSubdomainOrLeaveBlank": "Entrez un sous-domaine ou laissez vide pour utiliser le domaine de base.",
|
||||
"domainPickerEnterSubdomainToSearch": "Entrez un sous-domaine pour rechercher et sélectionner parmi les domaines gratuits disponibles.",
|
||||
"domainPickerFreeDomains": "Domaines gratuits",
|
||||
"domainPickerSearchForAvailableDomains": "Rechercher des domaines disponibles",
|
||||
"resourceDomain": "Domaine",
|
||||
"resourceEditDomain": "Modifier le domaine",
|
||||
"siteName": "Nom du site",
|
||||
"proxyPort": "Port",
|
||||
"resourcesTableProxyResources": "Ressources proxy",
|
||||
"resourcesTableClientResources": "Ressources client",
|
||||
"resourcesTableNoProxyResourcesFound": "Aucune ressource proxy trouvée.",
|
||||
"resourcesTableNoInternalResourcesFound": "Aucune ressource interne trouvée.",
|
||||
"resourcesTableDestination": "Destination",
|
||||
"resourcesTableTheseResourcesForUseWith": "Ces ressources sont à utiliser avec",
|
||||
"resourcesTableClients": "Clients",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "et sont uniquement accessibles en interne lorsqu'elles sont connectées avec un client.",
|
||||
"editInternalResourceDialogEditClientResource": "Modifier la ressource client",
|
||||
"editInternalResourceDialogUpdateResourceProperties": "Mettez à jour les propriétés de la ressource et la configuration de la cible pour {resourceName}.",
|
||||
"editInternalResourceDialogResourceProperties": "Propriétés de la ressource",
|
||||
"editInternalResourceDialogName": "Nom",
|
||||
"editInternalResourceDialogProtocol": "Protocole",
|
||||
"editInternalResourceDialogSitePort": "Port du site",
|
||||
"editInternalResourceDialogTargetConfiguration": "Configuration de la cible",
|
||||
"editInternalResourceDialogDestinationIP": "IP de destination",
|
||||
"editInternalResourceDialogDestinationPort": "Port de destination",
|
||||
"editInternalResourceDialogCancel": "Abandonner",
|
||||
"editInternalResourceDialogSaveResource": "Enregistrer la ressource",
|
||||
"editInternalResourceDialogSuccess": "Succès",
|
||||
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Ressource interne mise à jour avec succès",
|
||||
"editInternalResourceDialogError": "Erreur",
|
||||
"editInternalResourceDialogFailedToUpdateInternalResource": "Échec de la mise à jour de la ressource interne",
|
||||
"editInternalResourceDialogNameRequired": "Le nom est requis",
|
||||
"editInternalResourceDialogNameMaxLength": "Le nom doit être inférieur à 255 caractères",
|
||||
"editInternalResourceDialogProxyPortMin": "Le port proxy doit être d'au moins 1",
|
||||
"editInternalResourceDialogProxyPortMax": "Le port proxy doit être inférieur à 65536",
|
||||
"editInternalResourceDialogInvalidIPAddressFormat": "Format d'adresse IP invalide",
|
||||
"editInternalResourceDialogDestinationPortMin": "Le port de destination doit être d'au moins 1",
|
||||
"editInternalResourceDialogDestinationPortMax": "Le port de destination doit être inférieur à 65536",
|
||||
"createInternalResourceDialogNoSitesAvailable": "Aucun site disponible",
|
||||
"createInternalResourceDialogNoSitesAvailableDescription": "Vous devez avoir au moins un site Newt avec un sous-réseau configuré pour créer des ressources internes.",
|
||||
"createInternalResourceDialogClose": "Fermer",
|
||||
"createInternalResourceDialogCreateClientResource": "Créer une ressource client",
|
||||
"createInternalResourceDialogCreateClientResourceDescription": "Créez une ressource accessible aux clients connectés au site sélectionné.",
|
||||
"createInternalResourceDialogResourceProperties": "Propriétés de la ressource",
|
||||
"createInternalResourceDialogName": "Nom",
|
||||
"createInternalResourceDialogSite": "Site",
|
||||
"createInternalResourceDialogSelectSite": "Sélectionner un site...",
|
||||
"createInternalResourceDialogSearchSites": "Rechercher des sites...",
|
||||
"createInternalResourceDialogNoSitesFound": "Aucun site trouvé.",
|
||||
"createInternalResourceDialogProtocol": "Protocole",
|
||||
"createInternalResourceDialogTcp": "TCP",
|
||||
"createInternalResourceDialogUdp": "UDP",
|
||||
"createInternalResourceDialogSitePort": "Port du site",
|
||||
"createInternalResourceDialogSitePortDescription": "Utilisez ce port pour accéder à la ressource sur le site lors de la connexion avec un client.",
|
||||
"createInternalResourceDialogTargetConfiguration": "Configuration de la cible",
|
||||
"createInternalResourceDialogDestinationIP": "IP de destination",
|
||||
"createInternalResourceDialogDestinationIPDescription": "L'adresse IP de la ressource sur le réseau du site.",
|
||||
"createInternalResourceDialogDestinationPort": "Port de destination",
|
||||
"createInternalResourceDialogDestinationPortDescription": "Le port sur l'IP de destination où la ressource est accessible.",
|
||||
"createInternalResourceDialogCancel": "Abandonner",
|
||||
"createInternalResourceDialogCreateResource": "Créer une ressource",
|
||||
"createInternalResourceDialogSuccess": "Succès",
|
||||
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Ressource interne créée avec succès",
|
||||
"createInternalResourceDialogError": "Erreur",
|
||||
"createInternalResourceDialogFailedToCreateInternalResource": "Échec de la création de la ressource interne",
|
||||
"createInternalResourceDialogNameRequired": "Le nom est requis",
|
||||
"createInternalResourceDialogNameMaxLength": "Le nom doit être inférieur à 255 caractères",
|
||||
"createInternalResourceDialogPleaseSelectSite": "Veuillez sélectionner un site",
|
||||
"createInternalResourceDialogProxyPortMin": "Le port proxy doit être d'au moins 1",
|
||||
"createInternalResourceDialogProxyPortMax": "Le port proxy doit être inférieur à 65536",
|
||||
"createInternalResourceDialogInvalidIPAddressFormat": "Format d'adresse IP invalide",
|
||||
"createInternalResourceDialogDestinationPortMin": "Le port de destination doit être d'au moins 1",
|
||||
"createInternalResourceDialogDestinationPortMax": "Le port de destination doit être inférieur à 65536",
|
||||
"siteConfiguration": "Configuration",
|
||||
"siteAcceptClientConnections": "Accepter les connexions client",
|
||||
"siteAcceptClientConnectionsDescription": "Permet à d'autres appareils de se connecter via cette instance de Newt en tant que passerelle utilisant des clients.",
|
||||
"siteAddress": "Adresse du site",
|
||||
"siteAddressDescription": "Spécifiez l'adresse IP de l'hôte pour que les clients puissent s'y connecter. C'est l'adresse interne du site dans le réseau Pangolin pour que les clients puissent s'adresser. Doit être dans le sous-réseau de l'organisation.",
|
||||
"autoLoginExternalIdp": "Connexion automatique avec IDP externe",
|
||||
"autoLoginExternalIdpDescription": "Rediriger immédiatement l'utilisateur vers l'IDP externe pour l'authentification.",
|
||||
"selectIdp": "Sélectionner l'IDP",
|
||||
"selectIdpPlaceholder": "Choisissez un IDP...",
|
||||
"selectIdpRequired": "Veuillez sélectionner un IDP lorsque la connexion automatique est activée.",
|
||||
"autoLoginTitle": "Redirection",
|
||||
"autoLoginDescription": "Redirection vers le fournisseur d'identité externe pour l'authentification.",
|
||||
"autoLoginProcessing": "Préparation de l'authentification...",
|
||||
"autoLoginRedirecting": "Redirection vers la connexion...",
|
||||
"autoLoginError": "Erreur de connexion automatique",
|
||||
"autoLoginErrorNoRedirectUrl": "Aucune URL de redirection reçue du fournisseur d'identité.",
|
||||
"autoLoginErrorGeneratingUrl": "Échec de la génération de l'URL d'authentification."
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -95,7 +94,9 @@
|
||||
"siteNewtTunnelDescription": "Modo più semplice per creare un entrypoint nella rete. Nessuna configurazione aggiuntiva.",
|
||||
"siteWg": "WireGuard Base",
|
||||
"siteWgDescription": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.",
|
||||
"siteWgDescriptionSaas": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta. FUNZIONA SOLO SU NODI AUTO-OSPITATI",
|
||||
"siteLocalDescription": "Solo risorse locali. Nessun tunneling.",
|
||||
"siteLocalDescriptionSaas": "Solo risorse locali. Nessun tunneling. FUNZIONA SOLO SU NODI AUTO-OSPITATI",
|
||||
"siteSeeAll": "Vedi Tutti I Siti",
|
||||
"siteTunnelDescription": "Determina come vuoi connetterti al tuo sito",
|
||||
"siteNewtCredentials": "Credenziali Newt",
|
||||
@@ -167,7 +168,7 @@
|
||||
"siteSelect": "Seleziona sito",
|
||||
"siteSearch": "Cerca sito",
|
||||
"siteNotFound": "Nessun sito trovato.",
|
||||
"siteSelectionDescription": "Questo sito fornirà connettività alla risorsa.",
|
||||
"siteSelectionDescription": "Questo sito fornirà connettività all'obiettivo.",
|
||||
"resourceType": "Tipo Di Risorsa",
|
||||
"resourceTypeDescription": "Determina come vuoi accedere alla tua risorsa",
|
||||
"resourceHTTPSSettings": "Impostazioni HTTPS",
|
||||
@@ -198,6 +199,7 @@
|
||||
"general": "Generale",
|
||||
"generalSettings": "Impostazioni Generali",
|
||||
"proxy": "Proxy",
|
||||
"internal": "Interno",
|
||||
"rules": "Regole",
|
||||
"resourceSettingDescription": "Configura le impostazioni sulla tua risorsa",
|
||||
"resourceSetting": "Impostazioni {resourceName}",
|
||||
@@ -491,7 +493,7 @@
|
||||
"targetTlsSniDescription": "Il Nome Server TLS da usare per SNI. Lascia vuoto per usare quello predefinito.",
|
||||
"targetTlsSubmit": "Salva Impostazioni",
|
||||
"targets": "Configurazione Target",
|
||||
"targetsDescription": "Configura i target per instradare il traffico ai tuoi servizi",
|
||||
"targetsDescription": "Configura i target per instradare il traffico ai tuoi servizi backend",
|
||||
"targetStickySessions": "Abilita Sessioni Persistenti",
|
||||
"targetStickySessionsDescription": "Mantieni le connessioni sullo stesso target backend per l'intera sessione.",
|
||||
"methodSelect": "Seleziona metodo",
|
||||
@@ -834,6 +836,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": "Requisiti della password:",
|
||||
"passwordRequirementLength": "Almeno 8 caratteri",
|
||||
"passwordRequirementUppercase": "Almeno una lettera maiuscola",
|
||||
"passwordRequirementLowercase": "Almeno una lettera minuscola",
|
||||
"passwordRequirementNumber": "Almeno un numero",
|
||||
"passwordRequirementSpecial": "Almeno un carattere speciale",
|
||||
"passwordRequirementsMet": "✓ La password soddisfa tutti i requisiti",
|
||||
"passwordStrength": "Forza della password",
|
||||
"passwordStrengthWeak": "Debole",
|
||||
"passwordStrengthMedium": "Media",
|
||||
"passwordStrengthStrong": "Forte",
|
||||
"passwordRequirements": "Requisiti:",
|
||||
"passwordRequirementLengthText": "8+ caratteri",
|
||||
"passwordRequirementUppercaseText": "Lettera maiuscola (A-Z)",
|
||||
"passwordRequirementLowercaseText": "Lettera minuscola (a-z)",
|
||||
"passwordRequirementNumberText": "Numero (0-9)",
|
||||
"passwordRequirementSpecialText": "Carattere speciale (!@#$%...)",
|
||||
"passwordsDoNotMatch": "Le password non coincidono",
|
||||
"otpEmailRequirementsLength": "L'OTP deve essere lungo almeno 1 carattere",
|
||||
"otpEmailSent": "OTP Inviato",
|
||||
"otpEmailSentDescription": "Un OTP è stato inviato alla tua email",
|
||||
@@ -953,6 +973,7 @@
|
||||
"logoutError": "Errore durante il logout",
|
||||
"signingAs": "Accesso come",
|
||||
"serverAdmin": "Amministratore Server",
|
||||
"managedSelfhosted": "Gestito Auto-Ospitato",
|
||||
"otpEnable": "Abilita Autenticazione a Due Fattori",
|
||||
"otpDisable": "Disabilita Autenticazione a Due Fattori",
|
||||
"logout": "Disconnetti",
|
||||
@@ -968,6 +989,9 @@
|
||||
"actionDeleteSite": "Elimina Sito",
|
||||
"actionGetSite": "Ottieni Sito",
|
||||
"actionListSites": "Elenca Siti",
|
||||
"setupToken": "Configura Token",
|
||||
"setupTokenDescription": "Inserisci il token di configurazione dalla console del server.",
|
||||
"setupTokenRequired": "Il token di configurazione è richiesto",
|
||||
"actionUpdateSite": "Aggiorna Sito",
|
||||
"actionListSiteRoles": "Elenca Ruoli Sito Consentiti",
|
||||
"actionCreateResource": "Crea Risorsa",
|
||||
@@ -1023,6 +1047,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 +1123,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 +1191,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 +1225,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 +1235,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 +1295,7 @@
|
||||
"createDomainName": "Nome:",
|
||||
"createDomainValue": "Valore:",
|
||||
"createDomainCnameRecords": "Record CNAME",
|
||||
"createDomainARecords": "Record A",
|
||||
"createDomainRecordNumber": "Record {number}",
|
||||
"createDomainTxtRecords": "Record TXT",
|
||||
"createDomainSaveTheseRecords": "Salva Questi Record",
|
||||
@@ -1273,5 +1303,152 @@
|
||||
"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": "Aggiungi intervalli CIDR che possono essere accessibili da questo sito in remoto utilizzando i client. Usa il formato come 10.0.0.0/24. Questo si applica SOLO alla connettività del client VPN.",
|
||||
"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",
|
||||
"addNewTarget": "Aggiungi Nuovo Target",
|
||||
"targetsList": "Elenco dei Target",
|
||||
"targetErrorDuplicateTargetFound": "Target duplicato trovato",
|
||||
"httpMethod": "Metodo HTTP",
|
||||
"selectHttpMethod": "Seleziona metodo HTTP",
|
||||
"domainPickerSubdomainLabel": "Sottodominio",
|
||||
"domainPickerBaseDomainLabel": "Dominio Base",
|
||||
"domainPickerSearchDomains": "Cerca domini...",
|
||||
"domainPickerNoDomainsFound": "Nessun dominio trovato",
|
||||
"domainPickerLoadingDomains": "Caricamento domini...",
|
||||
"domainPickerSelectBaseDomain": "Seleziona dominio base...",
|
||||
"domainPickerNotAvailableForCname": "Non disponibile per i domini CNAME",
|
||||
"domainPickerEnterSubdomainOrLeaveBlank": "Inserisci un sottodominio o lascia vuoto per utilizzare il dominio base.",
|
||||
"domainPickerEnterSubdomainToSearch": "Inserisci un sottodominio per cercare e selezionare dai domini gratuiti disponibili.",
|
||||
"domainPickerFreeDomains": "Domini Gratuiti",
|
||||
"domainPickerSearchForAvailableDomains": "Cerca domini disponibili",
|
||||
"resourceDomain": "Dominio",
|
||||
"resourceEditDomain": "Modifica Dominio",
|
||||
"siteName": "Nome del Sito",
|
||||
"proxyPort": "Porta",
|
||||
"resourcesTableProxyResources": "Risorse Proxy",
|
||||
"resourcesTableClientResources": "Risorse Client",
|
||||
"resourcesTableNoProxyResourcesFound": "Nessuna risorsa proxy trovata.",
|
||||
"resourcesTableNoInternalResourcesFound": "Nessuna risorsa interna trovata.",
|
||||
"resourcesTableDestination": "Destinazione",
|
||||
"resourcesTableTheseResourcesForUseWith": "Queste risorse sono per uso con",
|
||||
"resourcesTableClients": "Client",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "e sono accessibili solo internamente quando connessi con un client.",
|
||||
"editInternalResourceDialogEditClientResource": "Modifica Risorsa Client",
|
||||
"editInternalResourceDialogUpdateResourceProperties": "Aggiorna le proprietà della risorsa e la configurazione del target per {resourceName}.",
|
||||
"editInternalResourceDialogResourceProperties": "Proprietà della Risorsa",
|
||||
"editInternalResourceDialogName": "Nome",
|
||||
"editInternalResourceDialogProtocol": "Protocollo",
|
||||
"editInternalResourceDialogSitePort": "Porta del Sito",
|
||||
"editInternalResourceDialogTargetConfiguration": "Configurazione Target",
|
||||
"editInternalResourceDialogDestinationIP": "IP di Destinazione",
|
||||
"editInternalResourceDialogDestinationPort": "Porta di Destinazione",
|
||||
"editInternalResourceDialogCancel": "Annulla",
|
||||
"editInternalResourceDialogSaveResource": "Salva Risorsa",
|
||||
"editInternalResourceDialogSuccess": "Successo",
|
||||
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Risorsa interna aggiornata con successo",
|
||||
"editInternalResourceDialogError": "Errore",
|
||||
"editInternalResourceDialogFailedToUpdateInternalResource": "Impossibile aggiornare la risorsa interna",
|
||||
"editInternalResourceDialogNameRequired": "Il nome è obbligatorio",
|
||||
"editInternalResourceDialogNameMaxLength": "Il nome deve essere inferiore a 255 caratteri",
|
||||
"editInternalResourceDialogProxyPortMin": "La porta proxy deve essere almeno 1",
|
||||
"editInternalResourceDialogProxyPortMax": "La porta proxy deve essere inferiore a 65536",
|
||||
"editInternalResourceDialogInvalidIPAddressFormat": "Formato dell'indirizzo IP non valido",
|
||||
"editInternalResourceDialogDestinationPortMin": "La porta di destinazione deve essere almeno 1",
|
||||
"editInternalResourceDialogDestinationPortMax": "La porta di destinazione deve essere inferiore a 65536",
|
||||
"createInternalResourceDialogNoSitesAvailable": "Nessun Sito Disponibile",
|
||||
"createInternalResourceDialogNoSitesAvailableDescription": "Devi avere almeno un sito Newt con una subnet configurata per creare risorse interne.",
|
||||
"createInternalResourceDialogClose": "Chiudi",
|
||||
"createInternalResourceDialogCreateClientResource": "Crea Risorsa Client",
|
||||
"createInternalResourceDialogCreateClientResourceDescription": "Crea una nuova risorsa che sarà accessibile ai client connessi al sito selezionato.",
|
||||
"createInternalResourceDialogResourceProperties": "Proprietà della Risorsa",
|
||||
"createInternalResourceDialogName": "Nome",
|
||||
"createInternalResourceDialogSite": "Sito",
|
||||
"createInternalResourceDialogSelectSite": "Seleziona sito...",
|
||||
"createInternalResourceDialogSearchSites": "Cerca siti...",
|
||||
"createInternalResourceDialogNoSitesFound": "Nessun sito trovato.",
|
||||
"createInternalResourceDialogProtocol": "Protocollo",
|
||||
"createInternalResourceDialogTcp": "TCP",
|
||||
"createInternalResourceDialogUdp": "UDP",
|
||||
"createInternalResourceDialogSitePort": "Porta del Sito",
|
||||
"createInternalResourceDialogSitePortDescription": "Usa questa porta per accedere alla risorsa nel sito quando sei connesso con un client.",
|
||||
"createInternalResourceDialogTargetConfiguration": "Configurazione Target",
|
||||
"createInternalResourceDialogDestinationIP": "IP di Destinazione",
|
||||
"createInternalResourceDialogDestinationIPDescription": "L'indirizzo IP della risorsa sulla rete del sito.",
|
||||
"createInternalResourceDialogDestinationPort": "Porta di Destinazione",
|
||||
"createInternalResourceDialogDestinationPortDescription": "La porta sull'IP di destinazione dove la risorsa è accessibile.",
|
||||
"createInternalResourceDialogCancel": "Annulla",
|
||||
"createInternalResourceDialogCreateResource": "Crea Risorsa",
|
||||
"createInternalResourceDialogSuccess": "Successo",
|
||||
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Risorsa interna creata con successo",
|
||||
"createInternalResourceDialogError": "Errore",
|
||||
"createInternalResourceDialogFailedToCreateInternalResource": "Impossibile creare la risorsa interna",
|
||||
"createInternalResourceDialogNameRequired": "Il nome è obbligatorio",
|
||||
"createInternalResourceDialogNameMaxLength": "Il nome non deve superare i 255 caratteri",
|
||||
"createInternalResourceDialogPleaseSelectSite": "Si prega di selezionare un sito",
|
||||
"createInternalResourceDialogProxyPortMin": "La porta proxy deve essere almeno 1",
|
||||
"createInternalResourceDialogProxyPortMax": "La porta proxy deve essere inferiore a 65536",
|
||||
"createInternalResourceDialogInvalidIPAddressFormat": "Formato dell'indirizzo IP non valido",
|
||||
"createInternalResourceDialogDestinationPortMin": "La porta di destinazione deve essere almeno 1",
|
||||
"createInternalResourceDialogDestinationPortMax": "La porta di destinazione deve essere inferiore a 65536",
|
||||
"siteConfiguration": "Configurazione",
|
||||
"siteAcceptClientConnections": "Accetta Connessioni Client",
|
||||
"siteAcceptClientConnectionsDescription": "Permetti ad altri dispositivi di connettersi attraverso questa istanza Newt come gateway utilizzando i client.",
|
||||
"siteAddress": "Indirizzo del Sito",
|
||||
"siteAddressDescription": "Specifica l'indirizzo IP dell'host a cui i client si collegano. Questo è l'indirizzo interno del sito nella rete Pangolin per indirizzare i client. Deve rientrare nella subnet dell'Organizzazione.",
|
||||
"autoLoginExternalIdp": "Accesso Automatico con IDP Esterno",
|
||||
"autoLoginExternalIdpDescription": "Reindirizzare immediatamente l'utente all'IDP esterno per l'autenticazione.",
|
||||
"selectIdp": "Seleziona IDP",
|
||||
"selectIdpPlaceholder": "Scegli un IDP...",
|
||||
"selectIdpRequired": "Si prega di selezionare un IDP quando l'accesso automatico è abilitato.",
|
||||
"autoLoginTitle": "Reindirizzamento",
|
||||
"autoLoginDescription": "Reindirizzandoti al provider di identità esterno per l'autenticazione.",
|
||||
"autoLoginProcessing": "Preparazione dell'autenticazione...",
|
||||
"autoLoginRedirecting": "Reindirizzamento al login...",
|
||||
"autoLoginError": "Errore di Accesso Automatico",
|
||||
"autoLoginErrorNoRedirectUrl": "Nessun URL di reindirizzamento ricevuto dal provider di identità.",
|
||||
"autoLoginErrorGeneratingUrl": "Impossibile generare l'URL di autenticazione."
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@
|
||||
"siteErrorCreate": "사이트 생성 오류",
|
||||
"siteErrorCreateKeyPair": "키 쌍 또는 사이트 기본값을 찾을 수 없습니다",
|
||||
"siteErrorCreateDefaults": "사이트 기본값을 찾을 수 없습니다",
|
||||
"siteNameDescription": "이것은 사이트의 표시 이름입니다.",
|
||||
"method": "방법",
|
||||
"siteMethodDescription": "이것이 연결을 노출하는 방법입니다.",
|
||||
"siteLearnNewt": "시스템에 Newt 설치하는 방법 배우기",
|
||||
@@ -95,7 +94,9 @@
|
||||
"siteNewtTunnelDescription": "네트워크에 대한 진입점을 생성하는 가장 쉬운 방법입니다. 추가 설정이 필요 없습니다.",
|
||||
"siteWg": "기본 WireGuard",
|
||||
"siteWgDescription": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다.",
|
||||
"siteWgDescriptionSaas": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다. 자체 호스팅 노드에서만 작동합니다.",
|
||||
"siteLocalDescription": "로컬 리소스만 사용 가능합니다. 터널링이 없습니다.",
|
||||
"siteLocalDescriptionSaas": "로컬 리소스만. 터널링 없음. 자체 호스팅 노드에서만 작동합니다.",
|
||||
"siteSeeAll": "모든 사이트 보기",
|
||||
"siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요",
|
||||
"siteNewtCredentials": "Newt 자격 증명",
|
||||
@@ -167,7 +168,7 @@
|
||||
"siteSelect": "사이트 선택",
|
||||
"siteSearch": "사이트 검색",
|
||||
"siteNotFound": "사이트를 찾을 수 없습니다.",
|
||||
"siteSelectionDescription": "이 사이트는 리소스에 대한 연결을 제공합니다.",
|
||||
"siteSelectionDescription": "이 사이트는 대상에 대한 연결을 제공합니다.",
|
||||
"resourceType": "리소스 유형",
|
||||
"resourceTypeDescription": "리소스에 접근하는 방법을 결정하세요",
|
||||
"resourceHTTPSSettings": "HTTPS 설정",
|
||||
@@ -198,6 +199,7 @@
|
||||
"general": "일반",
|
||||
"generalSettings": "일반 설정",
|
||||
"proxy": "프록시",
|
||||
"internal": "내부",
|
||||
"rules": "규칙",
|
||||
"resourceSettingDescription": "리소스의 설정을 구성하세요.",
|
||||
"resourceSetting": "{resourceName} 설정",
|
||||
@@ -491,7 +493,7 @@
|
||||
"targetTlsSniDescription": "SNI에 사용할 TLS 서버 이름. 기본값을 사용하려면 비워 두십시오.",
|
||||
"targetTlsSubmit": "설정 저장",
|
||||
"targets": "대상 구성",
|
||||
"targetsDescription": "서비스로 트래픽을 라우팅할 대상을 설정하십시오",
|
||||
"targetsDescription": "사용자 백엔드 서비스로 트래픽을 라우팅할 대상을 설정하십시오.",
|
||||
"targetStickySessions": "스티키 세션 활성화",
|
||||
"targetStickySessionsDescription": "세션 전체 동안 동일한 백엔드 대상을 유지합니다.",
|
||||
"methodSelect": "선택 방법",
|
||||
@@ -834,6 +836,24 @@
|
||||
"pincodeRequirementsLength": "PIN은 정확히 6자리여야 합니다",
|
||||
"pincodeRequirementsChars": "PIN은 숫자만 포함해야 합니다.",
|
||||
"passwordRequirementsLength": "비밀번호는 최소 1자 이상이어야 합니다",
|
||||
"passwordRequirementsTitle": "비밀번호 요구사항:",
|
||||
"passwordRequirementLength": "최소 8자 이상",
|
||||
"passwordRequirementUppercase": "최소 대문자 하나",
|
||||
"passwordRequirementLowercase": "최소 소문자 하나",
|
||||
"passwordRequirementNumber": "최소 숫자 하나",
|
||||
"passwordRequirementSpecial": "최소 특수 문자 하나",
|
||||
"passwordRequirementsMet": "✓ 비밀번호가 모든 요구사항을 충족합니다.",
|
||||
"passwordStrength": "비밀번호 강도",
|
||||
"passwordStrengthWeak": "약함",
|
||||
"passwordStrengthMedium": "보통",
|
||||
"passwordStrengthStrong": "강함",
|
||||
"passwordRequirements": "요구 사항:",
|
||||
"passwordRequirementLengthText": "8자 이상",
|
||||
"passwordRequirementUppercaseText": "대문자 (A-Z)",
|
||||
"passwordRequirementLowercaseText": "소문자 (a-z)",
|
||||
"passwordRequirementNumberText": "숫자 (0-9)",
|
||||
"passwordRequirementSpecialText": "특수 문자 (!@#$%...)",
|
||||
"passwordsDoNotMatch": "비밀번호가 일치하지 않습니다.",
|
||||
"otpEmailRequirementsLength": "OTP는 최소 1자 이상이어야 합니다",
|
||||
"otpEmailSent": "OTP 전송됨",
|
||||
"otpEmailSentDescription": "OTP가 귀하의 이메일로 전송되었습니다.",
|
||||
@@ -953,6 +973,7 @@
|
||||
"logoutError": "로그아웃 중 오류 발생",
|
||||
"signingAs": "로그인한 사용자",
|
||||
"serverAdmin": "서버 관리자",
|
||||
"managedSelfhosted": "관리 자체 호스팅",
|
||||
"otpEnable": "이중 인증 활성화",
|
||||
"otpDisable": "이중 인증 비활성화",
|
||||
"logout": "로그 아웃",
|
||||
@@ -968,6 +989,9 @@
|
||||
"actionDeleteSite": "사이트 삭제",
|
||||
"actionGetSite": "사이트 가져오기",
|
||||
"actionListSites": "사이트 목록",
|
||||
"setupToken": "설정 토큰",
|
||||
"setupTokenDescription": "서버 콘솔에서 설정 토큰 입력.",
|
||||
"setupTokenRequired": "설정 토큰이 필요합니다",
|
||||
"actionUpdateSite": "사이트 업데이트",
|
||||
"actionListSiteRoles": "허용된 사이트 역할 목록",
|
||||
"actionCreateResource": "리소스 생성",
|
||||
@@ -1023,6 +1047,11 @@
|
||||
"actionDeleteIdpOrg": "IDP 조직 정책 삭제",
|
||||
"actionListIdpOrgs": "IDP 조직 목록",
|
||||
"actionUpdateIdpOrg": "IDP 조직 업데이트",
|
||||
"actionCreateClient": "클라이언트 생성",
|
||||
"actionDeleteClient": "클라이언트 삭제",
|
||||
"actionUpdateClient": "클라이언트 업데이트",
|
||||
"actionListClients": "클라이언트 목록",
|
||||
"actionGetClient": "클라이언트 가져오기",
|
||||
"noneSelected": "선택된 항목 없음",
|
||||
"orgNotFound2": "조직이 없습니다.",
|
||||
"searchProgress": "검색...",
|
||||
@@ -1094,7 +1123,7 @@
|
||||
"sidebarAllUsers": "모든 사용자",
|
||||
"sidebarIdentityProviders": "신원 공급자",
|
||||
"sidebarLicense": "라이선스",
|
||||
"sidebarClients": "클라이언트",
|
||||
"sidebarClients": "클라이언트 (Beta)",
|
||||
"sidebarDomains": "도메인",
|
||||
"enableDockerSocket": "Docker 소켓 활성화",
|
||||
"enableDockerSocketDescription": "컨테이너 정보를 채우기 위해 Docker 소켓 검색을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.",
|
||||
@@ -1162,7 +1191,7 @@
|
||||
"selectDomainTypeCnameName": "단일 도메인 (CNAME)",
|
||||
"selectDomainTypeCnameDescription": "단일 하위 도메인 또는 특정 도메인 항목에 사용됩니다.",
|
||||
"selectDomainTypeWildcardName": "와일드카드 도메인",
|
||||
"selectDomainTypeWildcardDescription": "이 도메인과 그 첫 번째 레벨의 하위 도메인입니다.",
|
||||
"selectDomainTypeWildcardDescription": "이 도메인 및 그 하위 도메인.",
|
||||
"domainDelegation": "단일 도메인",
|
||||
"selectType": "유형 선택",
|
||||
"actions": "작업",
|
||||
@@ -1196,17 +1225,17 @@
|
||||
"sidebarExpand": "확장하기",
|
||||
"newtUpdateAvailable": "업데이트 가능",
|
||||
"newtUpdateAvailableInfo": "뉴트의 새 버전이 출시되었습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
|
||||
"domainPickerEnterDomain": "도메인 입력",
|
||||
"domainPickerEnterDomain": "도메인",
|
||||
"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",
|
||||
"domainPickerDescription": "리소스의 전체 도메인을 입력하여 사용 가능한 옵션을 확인하십시오.",
|
||||
"domainPickerDescriptionSaas": "전체 도메인, 서브도메인 또는 이름을 입력하여 사용 가능한 옵션을 확인하십시오.",
|
||||
"domainPickerTabAll": "모두",
|
||||
"domainPickerTabOrganization": "조직",
|
||||
"domainPickerTabProvided": "제공 됨",
|
||||
"domainPickerSortAsc": "A-Z",
|
||||
"domainPickerSortDesc": "Z-A",
|
||||
"domainPickerCheckingAvailability": "가용성을 확인 중...",
|
||||
"domainPickerNoMatchingDomains": "\"{userInput}\"에 해당하는 도메인을 찾을 수 없습니다. 다른 도메인을 시도하거나 조직의 도메인 설정을 확인하세요.",
|
||||
"domainPickerNoMatchingDomains": "일치하는 도메인을 찾을 수 없습니다. 다른 도메인을 시도하거나 조직의 도메인 설정을 확인하십시오.",
|
||||
"domainPickerOrganizationDomains": "조직 도메인",
|
||||
"domainPickerProvidedDomains": "제공된 도메인",
|
||||
"domainPickerSubdomain": "서브도메인: {subdomain}",
|
||||
@@ -1232,7 +1261,7 @@
|
||||
"securityKeyRemoveSuccess": "보안 키가 성공적으로 제거되었습니다",
|
||||
"securityKeyRemoveError": "보안 키 제거 실패",
|
||||
"securityKeyLoadError": "보안 키를 불러오는 데 실패했습니다",
|
||||
"securityKeyLogin": "Continue with security key",
|
||||
"securityKeyLogin": "보안 키로 계속하기",
|
||||
"securityKeyAuthError": "보안 키를 사용한 인증 실패",
|
||||
"securityKeyRecommendation": "항상 계정에 액세스할 수 있도록 다른 장치에 백업 보안 키를 등록하세요.",
|
||||
"registering": "등록 중...",
|
||||
@@ -1266,6 +1295,7 @@
|
||||
"createDomainName": "이름:",
|
||||
"createDomainValue": "값:",
|
||||
"createDomainCnameRecords": "CNAME 레코드",
|
||||
"createDomainARecords": "A 레코드",
|
||||
"createDomainRecordNumber": "레코드 {number}",
|
||||
"createDomainTxtRecords": "TXT 레코드",
|
||||
"createDomainSaveTheseRecords": "이 레코드 저장",
|
||||
@@ -1273,5 +1303,152 @@
|
||||
"createDomainDnsPropagation": "DNS 전파",
|
||||
"createDomainDnsPropagationDescription": "DNS 변경 사항은 인터넷 전체에 전파되는 데 시간이 걸립니다. DNS 제공자와 TTL 설정에 따라 몇 분에서 48시간까지 걸릴 수 있습니다.",
|
||||
"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": "이 사이트에서 원격으로 액세스할 수 있는 CIDR 범위를 추가하세요. 10.0.0.0/24와 같은 형식을 사용하세요. 이는 VPN 클라이언트 연결에만 적용됩니다.",
|
||||
"resourceEnableProxy": "공개 프록시 사용",
|
||||
"resourceEnableProxyDescription": "이 리소스에 대한 공개 프록시를 활성화하십시오. 이를 통해 네트워크 외부로부터 클라우드를 통해 열린 포트에서 리소스에 액세스할 수 있습니다. Traefik 구성이 필요합니다.",
|
||||
"externalProxyEnabled": "외부 프록시 활성화됨",
|
||||
"addNewTarget": "새 대상 추가",
|
||||
"targetsList": "대상 목록",
|
||||
"targetErrorDuplicateTargetFound": "중복 대상 발견",
|
||||
"httpMethod": "HTTP 메소드",
|
||||
"selectHttpMethod": "HTTP 메소드 선택",
|
||||
"domainPickerSubdomainLabel": "서브도메인",
|
||||
"domainPickerBaseDomainLabel": "기본 도메인",
|
||||
"domainPickerSearchDomains": "도메인 검색...",
|
||||
"domainPickerNoDomainsFound": "찾을 수 없는 도메인이 없습니다",
|
||||
"domainPickerLoadingDomains": "도메인 로딩 중...",
|
||||
"domainPickerSelectBaseDomain": "기본 도메인 선택...",
|
||||
"domainPickerNotAvailableForCname": "CNAME 도메인에는 사용할 수 없습니다",
|
||||
"domainPickerEnterSubdomainOrLeaveBlank": "서브도메인을 입력하거나 기본 도메인을 사용하려면 공백으로 두십시오.",
|
||||
"domainPickerEnterSubdomainToSearch": "사용 가능한 무료 도메인에서 검색 및 선택할 서브도메인 입력.",
|
||||
"domainPickerFreeDomains": "무료 도메인",
|
||||
"domainPickerSearchForAvailableDomains": "사용 가능한 도메인 검색",
|
||||
"resourceDomain": "도메인",
|
||||
"resourceEditDomain": "도메인 수정",
|
||||
"siteName": "사이트 이름",
|
||||
"proxyPort": "포트",
|
||||
"resourcesTableProxyResources": "프록시 리소스",
|
||||
"resourcesTableClientResources": "클라이언트 리소스",
|
||||
"resourcesTableNoProxyResourcesFound": "프록시 리소스를 찾을 수 없습니다.",
|
||||
"resourcesTableNoInternalResourcesFound": "내부 리소스를 찾을 수 없습니다.",
|
||||
"resourcesTableDestination": "대상지",
|
||||
"resourcesTableTheseResourcesForUseWith": "이 리소스는 다음과 함께 사용하기 위한 것입니다.",
|
||||
"resourcesTableClients": "클라이언트",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "클라이언트와 연결되었을 때만 내부적으로 접근 가능합니다.",
|
||||
"editInternalResourceDialogEditClientResource": "클라이언트 리소스 수정",
|
||||
"editInternalResourceDialogUpdateResourceProperties": "{resourceName}의 리소스 속성과 대상 구성을 업데이트하세요.",
|
||||
"editInternalResourceDialogResourceProperties": "리소스 속성",
|
||||
"editInternalResourceDialogName": "이름",
|
||||
"editInternalResourceDialogProtocol": "프로토콜",
|
||||
"editInternalResourceDialogSitePort": "사이트 포트",
|
||||
"editInternalResourceDialogTargetConfiguration": "대상 구성",
|
||||
"editInternalResourceDialogDestinationIP": "대상 IP",
|
||||
"editInternalResourceDialogDestinationPort": "대상 IP의 포트",
|
||||
"editInternalResourceDialogCancel": "취소",
|
||||
"editInternalResourceDialogSaveResource": "리소스 저장",
|
||||
"editInternalResourceDialogSuccess": "성공",
|
||||
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "내부 리소스가 성공적으로 업데이트되었습니다",
|
||||
"editInternalResourceDialogError": "오류",
|
||||
"editInternalResourceDialogFailedToUpdateInternalResource": "내부 리소스 업데이트 실패",
|
||||
"editInternalResourceDialogNameRequired": "이름은 필수입니다.",
|
||||
"editInternalResourceDialogNameMaxLength": "이름은 255자 이하이어야 합니다.",
|
||||
"editInternalResourceDialogProxyPortMin": "프록시 포트는 최소 1이어야 합니다.",
|
||||
"editInternalResourceDialogProxyPortMax": "프록시 포트는 65536 미만이어야 합니다.",
|
||||
"editInternalResourceDialogInvalidIPAddressFormat": "잘못된 IP 주소 형식",
|
||||
"editInternalResourceDialogDestinationPortMin": "대상 포트는 최소 1이어야 합니다.",
|
||||
"editInternalResourceDialogDestinationPortMax": "대상 포트는 65536 미만이어야 합니다.",
|
||||
"createInternalResourceDialogNoSitesAvailable": "사용 가능한 사이트가 없습니다.",
|
||||
"createInternalResourceDialogNoSitesAvailableDescription": "내부 리소스를 생성하려면 서브넷이 구성된 최소 하나의 Newt 사이트가 필요합니다.",
|
||||
"createInternalResourceDialogClose": "닫기",
|
||||
"createInternalResourceDialogCreateClientResource": "클라이언트 리소스 생성",
|
||||
"createInternalResourceDialogCreateClientResourceDescription": "선택한 사이트에 연결된 클라이언트에 접근할 새 리소스를 생성합니다.",
|
||||
"createInternalResourceDialogResourceProperties": "리소스 속성",
|
||||
"createInternalResourceDialogName": "이름",
|
||||
"createInternalResourceDialogSite": "사이트",
|
||||
"createInternalResourceDialogSelectSite": "사이트 선택...",
|
||||
"createInternalResourceDialogSearchSites": "사이트 검색...",
|
||||
"createInternalResourceDialogNoSitesFound": "사이트를 찾을 수 없습니다.",
|
||||
"createInternalResourceDialogProtocol": "프로토콜",
|
||||
"createInternalResourceDialogTcp": "TCP",
|
||||
"createInternalResourceDialogUdp": "UDP",
|
||||
"createInternalResourceDialogSitePort": "사이트 포트",
|
||||
"createInternalResourceDialogSitePortDescription": "사이트에 연결되었을 때 리소스에 접근하기 위해 이 포트를 사용합니다.",
|
||||
"createInternalResourceDialogTargetConfiguration": "대상 설정",
|
||||
"createInternalResourceDialogDestinationIP": "대상 IP",
|
||||
"createInternalResourceDialogDestinationIPDescription": "사이트 네트워크의 자원 IP 주소입니다.",
|
||||
"createInternalResourceDialogDestinationPort": "대상 포트",
|
||||
"createInternalResourceDialogDestinationPortDescription": "대상 IP에서 리소스에 접근할 수 있는 포트입니다.",
|
||||
"createInternalResourceDialogCancel": "취소",
|
||||
"createInternalResourceDialogCreateResource": "리소스 생성",
|
||||
"createInternalResourceDialogSuccess": "성공",
|
||||
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "내부 리소스가 성공적으로 생성되었습니다.",
|
||||
"createInternalResourceDialogError": "오류",
|
||||
"createInternalResourceDialogFailedToCreateInternalResource": "내부 리소스 생성 실패",
|
||||
"createInternalResourceDialogNameRequired": "이름은 필수입니다.",
|
||||
"createInternalResourceDialogNameMaxLength": "이름은 255자 이하이어야 합니다.",
|
||||
"createInternalResourceDialogPleaseSelectSite": "사이트를 선택하세요",
|
||||
"createInternalResourceDialogProxyPortMin": "프록시 포트는 최소 1이어야 합니다.",
|
||||
"createInternalResourceDialogProxyPortMax": "프록시 포트는 65536 미만이어야 합니다.",
|
||||
"createInternalResourceDialogInvalidIPAddressFormat": "잘못된 IP 주소 형식",
|
||||
"createInternalResourceDialogDestinationPortMin": "대상 포트는 최소 1이어야 합니다.",
|
||||
"createInternalResourceDialogDestinationPortMax": "대상 포트는 65536 미만이어야 합니다.",
|
||||
"siteConfiguration": "설정",
|
||||
"siteAcceptClientConnections": "클라이언트 연결 허용",
|
||||
"siteAcceptClientConnectionsDescription": "이 Newt 인스턴스를 게이트웨이로 사용하여 다른 장치가 연결될 수 있도록 허용합니다.",
|
||||
"siteAddress": "사이트 주소",
|
||||
"siteAddressDescription": "클라이언트가 연결하기 위한 호스트의 IP 주소를 지정합니다. 이는 클라이언트가 주소를 지정하기 위한 Pangolin 네트워크의 사이트 내부 주소입니다. 조직 서브넷 내에 있어야 합니다.",
|
||||
"autoLoginExternalIdp": "외부 IDP로 자동 로그인",
|
||||
"autoLoginExternalIdpDescription": "인증을 위해 외부 IDP로 사용자를 즉시 리디렉션합니다.",
|
||||
"selectIdp": "IDP 선택",
|
||||
"selectIdpPlaceholder": "IDP 선택...",
|
||||
"selectIdpRequired": "자동 로그인이 활성화된 경우 IDP를 선택하십시오.",
|
||||
"autoLoginTitle": "리디렉션 중",
|
||||
"autoLoginDescription": "인증을 위해 외부 ID 공급자로 리디렉션 중입니다.",
|
||||
"autoLoginProcessing": "인증 준비 중...",
|
||||
"autoLoginRedirecting": "로그인으로 리디렉션 중...",
|
||||
"autoLoginError": "자동 로그인 오류",
|
||||
"autoLoginErrorNoRedirectUrl": "ID 공급자로부터 리디렉션 URL을 받지 못했습니다.",
|
||||
"autoLoginErrorGeneratingUrl": "인증 URL 생성 실패."
|
||||
}
|
||||
|
||||
1454
messages/nb-NO.json
Normal file
1454
messages/nb-NO.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
@@ -95,7 +94,9 @@
|
||||
"siteNewtTunnelDescription": "Gemakkelijkste manier om een ingangspunt in uw netwerk te maken. Geen extra opzet.",
|
||||
"siteWg": "Basis WireGuard",
|
||||
"siteWgDescription": "Gebruik een WireGuard client om een tunnel te bouwen. Handmatige NAT installatie vereist.",
|
||||
"siteWgDescriptionSaas": "Gebruik elke WireGuard-client om een tunnel op te zetten. Handmatige NAT-instelling vereist. WERKT ALLEEN OP SELF HOSTED NODES",
|
||||
"siteLocalDescription": "Alleen lokale bronnen. Geen tunneling.",
|
||||
"siteLocalDescriptionSaas": "Alleen lokale bronnen. Geen tunneling. WERKT ALLEEN OP SELF HOSTED NODES",
|
||||
"siteSeeAll": "Alle werkruimtes bekijken",
|
||||
"siteTunnelDescription": "Bepaal hoe u verbinding wilt maken met uw site",
|
||||
"siteNewtCredentials": "Nieuwste aanmeldgegevens",
|
||||
@@ -167,7 +168,7 @@
|
||||
"siteSelect": "Selecteer site",
|
||||
"siteSearch": "Zoek site",
|
||||
"siteNotFound": "Geen site gevonden.",
|
||||
"siteSelectionDescription": "Deze site zal connectiviteit met de bron geven.",
|
||||
"siteSelectionDescription": "Deze site zal connectiviteit met het doelwit bieden.",
|
||||
"resourceType": "Type bron",
|
||||
"resourceTypeDescription": "Bepaal hoe u toegang wilt krijgen tot uw bron",
|
||||
"resourceHTTPSSettings": "HTTPS instellingen",
|
||||
@@ -198,6 +199,7 @@
|
||||
"general": "Algemeen",
|
||||
"generalSettings": "Algemene instellingen",
|
||||
"proxy": "Proxy",
|
||||
"internal": "Intern",
|
||||
"rules": "Regels",
|
||||
"resourceSettingDescription": "Configureer de instellingen op uw bron",
|
||||
"resourceSetting": "{resourceName} instellingen",
|
||||
@@ -491,7 +493,7 @@
|
||||
"targetTlsSniDescription": "De TLS servernaam om te gebruiken voor SNI. Laat leeg om de standaard te gebruiken.",
|
||||
"targetTlsSubmit": "Instellingen opslaan",
|
||||
"targets": "Doelstellingen configuratie",
|
||||
"targetsDescription": "Stel doelen in om verkeer naar uw diensten te leiden",
|
||||
"targetsDescription": "Stel doelen in om verkeer naar uw backend-services te leiden",
|
||||
"targetStickySessions": "Sticky sessies inschakelen",
|
||||
"targetStickySessionsDescription": "Behoud verbindingen op hetzelfde backend doel voor hun hele sessie.",
|
||||
"methodSelect": "Selecteer methode",
|
||||
@@ -834,6 +836,24 @@
|
||||
"pincodeRequirementsLength": "Pincode moet precies 6 cijfers zijn",
|
||||
"pincodeRequirementsChars": "Pincode mag alleen cijfers bevatten",
|
||||
"passwordRequirementsLength": "Wachtwoord moet ten minste 1 teken lang zijn",
|
||||
"passwordRequirementsTitle": "Wachtwoordvereisten:",
|
||||
"passwordRequirementLength": "Minstens 8 tekens lang",
|
||||
"passwordRequirementUppercase": "Minstens één hoofdletter",
|
||||
"passwordRequirementLowercase": "Minstens één kleine letter",
|
||||
"passwordRequirementNumber": "Minstens één cijfer",
|
||||
"passwordRequirementSpecial": "Minstens één speciaal teken",
|
||||
"passwordRequirementsMet": "✓ Wachtwoord voldoet aan alle vereisten",
|
||||
"passwordStrength": "Wachtwoord sterkte",
|
||||
"passwordStrengthWeak": "Zwak",
|
||||
"passwordStrengthMedium": "Gemiddeld",
|
||||
"passwordStrengthStrong": "Sterk",
|
||||
"passwordRequirements": "Vereisten:",
|
||||
"passwordRequirementLengthText": "8+ tekens",
|
||||
"passwordRequirementUppercaseText": "Hoofdletter (A-Z)",
|
||||
"passwordRequirementLowercaseText": "Kleine letter (a-z)",
|
||||
"passwordRequirementNumberText": "Cijfer (0-9)",
|
||||
"passwordRequirementSpecialText": "Speciaal teken (!@#$%...)",
|
||||
"passwordsDoNotMatch": "Wachtwoorden komen niet overeen",
|
||||
"otpEmailRequirementsLength": "OTP moet minstens 1 teken lang zijn",
|
||||
"otpEmailSent": "OTP verzonden",
|
||||
"otpEmailSentDescription": "Een OTP is naar uw e-mail verzonden",
|
||||
@@ -953,6 +973,7 @@
|
||||
"logoutError": "Fout bij uitloggen",
|
||||
"signingAs": "Ingelogd als",
|
||||
"serverAdmin": "Server Beheerder",
|
||||
"managedSelfhosted": "Beheerde Self-Hosted",
|
||||
"otpEnable": "Twee-factor inschakelen",
|
||||
"otpDisable": "Tweestapsverificatie uitschakelen",
|
||||
"logout": "Log uit",
|
||||
@@ -968,6 +989,9 @@
|
||||
"actionDeleteSite": "Site verwijderen",
|
||||
"actionGetSite": "Site ophalen",
|
||||
"actionListSites": "Sites weergeven",
|
||||
"setupToken": "Setup Token",
|
||||
"setupTokenDescription": "Voer het setup-token in vanaf de serverconsole.",
|
||||
"setupTokenRequired": "Setup-token is vereist",
|
||||
"actionUpdateSite": "Site bijwerken",
|
||||
"actionListSiteRoles": "Toon toegestane sitenollen",
|
||||
"actionCreateResource": "Bron maken",
|
||||
@@ -1023,6 +1047,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 +1123,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 +1191,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 +1225,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 +1235,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 +1295,7 @@
|
||||
"createDomainName": "Naam:",
|
||||
"createDomainValue": "Waarde:",
|
||||
"createDomainCnameRecords": "CNAME-records",
|
||||
"createDomainARecords": "A Records",
|
||||
"createDomainRecordNumber": "Record {number}",
|
||||
"createDomainTxtRecords": "TXT-records",
|
||||
"createDomainSaveTheseRecords": "Deze records opslaan",
|
||||
@@ -1273,5 +1303,152 @@
|
||||
"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": "Voeg CIDR-bereiken toe die vanaf deze site op afstand toegankelijk zijn met behulp van clients. Gebruik een formaat zoals 10.0.0.0/24. Dit geldt ALLEEN voor VPN-clientconnectiviteit.",
|
||||
"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",
|
||||
"addNewTarget": "Voeg nieuw doelwit toe",
|
||||
"targetsList": "Lijst met doelen",
|
||||
"targetErrorDuplicateTargetFound": "Dubbel doelwit gevonden",
|
||||
"httpMethod": "HTTP-methode",
|
||||
"selectHttpMethod": "Selecteer HTTP-methode",
|
||||
"domainPickerSubdomainLabel": "Subdomein",
|
||||
"domainPickerBaseDomainLabel": "Basisdomein",
|
||||
"domainPickerSearchDomains": "Zoek domeinen...",
|
||||
"domainPickerNoDomainsFound": "Geen domeinen gevonden",
|
||||
"domainPickerLoadingDomains": "Domeinen laden...",
|
||||
"domainPickerSelectBaseDomain": "Selecteer basisdomein...",
|
||||
"domainPickerNotAvailableForCname": "Niet beschikbaar voor CNAME-domeinen",
|
||||
"domainPickerEnterSubdomainOrLeaveBlank": "Voer een subdomein in of laat leeg om basisdomein te gebruiken.",
|
||||
"domainPickerEnterSubdomainToSearch": "Voer een subdomein in om te zoeken en te selecteren uit beschikbare gratis domeinen.",
|
||||
"domainPickerFreeDomains": "Gratis Domeinen",
|
||||
"domainPickerSearchForAvailableDomains": "Zoek naar beschikbare domeinen",
|
||||
"resourceDomain": "Domein",
|
||||
"resourceEditDomain": "Domein bewerken",
|
||||
"siteName": "Site Naam",
|
||||
"proxyPort": "Poort",
|
||||
"resourcesTableProxyResources": "Proxybronnen",
|
||||
"resourcesTableClientResources": "Clientbronnen",
|
||||
"resourcesTableNoProxyResourcesFound": "Geen proxybronnen gevonden.",
|
||||
"resourcesTableNoInternalResourcesFound": "Geen interne bronnen gevonden.",
|
||||
"resourcesTableDestination": "Bestemming",
|
||||
"resourcesTableTheseResourcesForUseWith": "Deze bronnen zijn bedoeld voor gebruik met",
|
||||
"resourcesTableClients": "Clienten",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "en zijn alleen intern toegankelijk wanneer verbonden met een client.",
|
||||
"editInternalResourceDialogEditClientResource": "Bewerk clientbron",
|
||||
"editInternalResourceDialogUpdateResourceProperties": "Werk de eigenschapen van de bron en doelconfiguratie bij voor {resourceName}.",
|
||||
"editInternalResourceDialogResourceProperties": "Bron eigenschappen",
|
||||
"editInternalResourceDialogName": "Naam",
|
||||
"editInternalResourceDialogProtocol": "Protocol",
|
||||
"editInternalResourceDialogSitePort": "Site Poort",
|
||||
"editInternalResourceDialogTargetConfiguration": "Doelconfiguratie",
|
||||
"editInternalResourceDialogDestinationIP": "Bestemming IP",
|
||||
"editInternalResourceDialogDestinationPort": "Bestemmingspoort",
|
||||
"editInternalResourceDialogCancel": "Annuleren",
|
||||
"editInternalResourceDialogSaveResource": "Sla bron op",
|
||||
"editInternalResourceDialogSuccess": "Succes",
|
||||
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Interne bron succesvol bijgewerkt",
|
||||
"editInternalResourceDialogError": "Fout",
|
||||
"editInternalResourceDialogFailedToUpdateInternalResource": "Het bijwerken van de interne bron is mislukt",
|
||||
"editInternalResourceDialogNameRequired": "Naam is verplicht",
|
||||
"editInternalResourceDialogNameMaxLength": "Naam mag niet langer zijn dan 255 tekens",
|
||||
"editInternalResourceDialogProxyPortMin": "Proxy poort moet minstens 1 zijn",
|
||||
"editInternalResourceDialogProxyPortMax": "Proxy poort moet minder dan 65536 zijn",
|
||||
"editInternalResourceDialogInvalidIPAddressFormat": "Ongeldig IP-adresformaat",
|
||||
"editInternalResourceDialogDestinationPortMin": "Bestemmingspoort moet minstens 1 zijn",
|
||||
"editInternalResourceDialogDestinationPortMax": "Bestemmingspoort moet minder dan 65536 zijn",
|
||||
"createInternalResourceDialogNoSitesAvailable": "Geen sites beschikbaar",
|
||||
"createInternalResourceDialogNoSitesAvailableDescription": "U moet ten minste één Newt-site hebben met een geconfigureerd subnet om interne bronnen aan te maken.",
|
||||
"createInternalResourceDialogClose": "Sluiten",
|
||||
"createInternalResourceDialogCreateClientResource": "Maak clientbron",
|
||||
"createInternalResourceDialogCreateClientResourceDescription": "Maak een nieuwe bron die toegankelijk zal zijn voor clients die verbonden zijn met de geselecteerde site.",
|
||||
"createInternalResourceDialogResourceProperties": "Bron-eigenschappen",
|
||||
"createInternalResourceDialogName": "Naam",
|
||||
"createInternalResourceDialogSite": "Site",
|
||||
"createInternalResourceDialogSelectSite": "Selecteer site...",
|
||||
"createInternalResourceDialogSearchSites": "Zoek sites...",
|
||||
"createInternalResourceDialogNoSitesFound": "Geen sites gevonden.",
|
||||
"createInternalResourceDialogProtocol": "Protocol",
|
||||
"createInternalResourceDialogTcp": "TCP",
|
||||
"createInternalResourceDialogUdp": "UDP",
|
||||
"createInternalResourceDialogSitePort": "Site Poort",
|
||||
"createInternalResourceDialogSitePortDescription": "Gebruik deze poort om toegang te krijgen tot de bron op de site wanneer verbonden met een client.",
|
||||
"createInternalResourceDialogTargetConfiguration": "Doelconfiguratie",
|
||||
"createInternalResourceDialogDestinationIP": "Bestemming IP",
|
||||
"createInternalResourceDialogDestinationIPDescription": "Het IP-adres van de bron op het netwerk van de site.",
|
||||
"createInternalResourceDialogDestinationPort": "Bestemmingspoort",
|
||||
"createInternalResourceDialogDestinationPortDescription": "De poort op het bestemmings-IP waar de bron toegankelijk is.",
|
||||
"createInternalResourceDialogCancel": "Annuleren",
|
||||
"createInternalResourceDialogCreateResource": "Bron aanmaken",
|
||||
"createInternalResourceDialogSuccess": "Succes",
|
||||
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Interne bron succesvol aangemaakt",
|
||||
"createInternalResourceDialogError": "Fout",
|
||||
"createInternalResourceDialogFailedToCreateInternalResource": "Het aanmaken van de interne bron is mislukt",
|
||||
"createInternalResourceDialogNameRequired": "Naam is verplicht",
|
||||
"createInternalResourceDialogNameMaxLength": "Naam mag niet langer zijn dan 255 tekens",
|
||||
"createInternalResourceDialogPleaseSelectSite": "Selecteer alstublieft een site",
|
||||
"createInternalResourceDialogProxyPortMin": "Proxy poort moet minstens 1 zijn",
|
||||
"createInternalResourceDialogProxyPortMax": "Proxy poort moet minder dan 65536 zijn",
|
||||
"createInternalResourceDialogInvalidIPAddressFormat": "Ongeldig IP-adresformaat",
|
||||
"createInternalResourceDialogDestinationPortMin": "Bestemmingspoort moet minstens 1 zijn",
|
||||
"createInternalResourceDialogDestinationPortMax": "Bestemmingspoort moet minder dan 65536 zijn",
|
||||
"siteConfiguration": "Configuratie",
|
||||
"siteAcceptClientConnections": "Accepteer clientverbindingen",
|
||||
"siteAcceptClientConnectionsDescription": "Sta toe dat andere apparaten verbinding maken via deze Newt-instantie als een gateway met behulp van clients.",
|
||||
"siteAddress": "Siteadres",
|
||||
"siteAddressDescription": "Specificeren het IP-adres van de host voor clients om verbinding mee te maken. Dit is het interne adres van de site in het Pangolin netwerk voor clients om te adresseren. Moet binnen het Organisatienetwerk vallen.",
|
||||
"autoLoginExternalIdp": "Auto Login met Externe IDP",
|
||||
"autoLoginExternalIdpDescription": "De gebruiker onmiddellijk doorsturen naar de externe IDP voor authenticatie.",
|
||||
"selectIdp": "Selecteer IDP",
|
||||
"selectIdpPlaceholder": "Kies een IDP...",
|
||||
"selectIdpRequired": "Selecteer alstublieft een IDP wanneer automatisch inloggen is ingeschakeld.",
|
||||
"autoLoginTitle": "Omleiden",
|
||||
"autoLoginDescription": "Je wordt doorverwezen naar de externe identity provider voor authenticatie.",
|
||||
"autoLoginProcessing": "Authenticatie voorbereiden...",
|
||||
"autoLoginRedirecting": "Redirecting naar inloggen...",
|
||||
"autoLoginError": "Auto Login Fout",
|
||||
"autoLoginErrorNoRedirectUrl": "Geen redirect URL ontvangen van de identity provider.",
|
||||
"autoLoginErrorGeneratingUrl": "Genereren van authenticatie-URL mislukt."
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -95,7 +94,9 @@
|
||||
"siteNewtTunnelDescription": "Łatwiejszy sposób na stworzenie punktu wejścia w sieci. Nie ma dodatkowej konfiguracji.",
|
||||
"siteWg": "Podstawowy WireGuard",
|
||||
"siteWgDescription": "Użyj dowolnego klienta WireGuard do utworzenia tunelu. Wymagana jest ręczna konfiguracja NAT.",
|
||||
"siteWgDescriptionSaas": "Użyj dowolnego klienta WireGuard do utworzenia tunelu. Wymagana ręczna konfiguracja NAT. DZIAŁA TYLKO NA SAMODZIELNIE HOSTOWANYCH WĘZŁACH",
|
||||
"siteLocalDescription": "Tylko lokalne zasoby. Brak tunelu.",
|
||||
"siteLocalDescriptionSaas": "Tylko zasoby lokalne. Brak tunelowania. DZIAŁA TYLKO NA SAMODZIELNIE HOSTOWANYCH WĘZŁACH",
|
||||
"siteSeeAll": "Zobacz wszystkie witryny",
|
||||
"siteTunnelDescription": "Określ jak chcesz połączyć się ze swoją stroną",
|
||||
"siteNewtCredentials": "Aktualne dane logowania",
|
||||
@@ -167,7 +168,7 @@
|
||||
"siteSelect": "Wybierz witrynę",
|
||||
"siteSearch": "Szukaj witryny",
|
||||
"siteNotFound": "Nie znaleziono witryny.",
|
||||
"siteSelectionDescription": "Ta strona zapewni połączenie z zasobem.",
|
||||
"siteSelectionDescription": "Ta strona zapewni połączenie z celem.",
|
||||
"resourceType": "Typ zasobu",
|
||||
"resourceTypeDescription": "Określ jak chcesz uzyskać dostęp do swojego zasobu",
|
||||
"resourceHTTPSSettings": "Ustawienia HTTPS",
|
||||
@@ -198,6 +199,7 @@
|
||||
"general": "Ogólny",
|
||||
"generalSettings": "Ustawienia ogólne",
|
||||
"proxy": "Serwer pośredniczący",
|
||||
"internal": "Wewętrzny",
|
||||
"rules": "Regulamin",
|
||||
"resourceSettingDescription": "Skonfiguruj ustawienia zasobu",
|
||||
"resourceSetting": "Ustawienia {resourceName}",
|
||||
@@ -491,7 +493,7 @@
|
||||
"targetTlsSniDescription": "Nazwa serwera TLS do użycia dla SNI. Pozostaw puste, aby użyć domyślnej.",
|
||||
"targetTlsSubmit": "Zapisz ustawienia",
|
||||
"targets": "Konfiguracja celów",
|
||||
"targetsDescription": "Skonfiguruj cele do kierowania ruchu do swoich usług",
|
||||
"targetsDescription": "Skonfiguruj cele do kierowania ruchu do usług zaplecza",
|
||||
"targetStickySessions": "Włącz sesje trwałe",
|
||||
"targetStickySessionsDescription": "Utrzymuj połączenia na tym samym celu backendowym przez całą sesję.",
|
||||
"methodSelect": "Wybierz metodę",
|
||||
@@ -834,6 +836,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": "Wymagania dotyczące hasła:",
|
||||
"passwordRequirementLength": "Przynajmniej 8 znaków długości",
|
||||
"passwordRequirementUppercase": "Przynajmniej jedna wielka litera",
|
||||
"passwordRequirementLowercase": "Przynajmniej jedna mała litera",
|
||||
"passwordRequirementNumber": "Przynajmniej jedna cyfra",
|
||||
"passwordRequirementSpecial": "Przynajmniej jeden znak specjalny",
|
||||
"passwordRequirementsMet": "✓ Hasło spełnia wszystkie wymagania",
|
||||
"passwordStrength": "Siła hasła",
|
||||
"passwordStrengthWeak": "Słabe",
|
||||
"passwordStrengthMedium": "Średnie",
|
||||
"passwordStrengthStrong": "Silne",
|
||||
"passwordRequirements": "Wymagania:",
|
||||
"passwordRequirementLengthText": "8+ znaków",
|
||||
"passwordRequirementUppercaseText": "Wielka litera (A-Z)",
|
||||
"passwordRequirementLowercaseText": "Mała litera (a-z)",
|
||||
"passwordRequirementNumberText": "Cyfra (0-9)",
|
||||
"passwordRequirementSpecialText": "Znak specjalny (!@#$%...)",
|
||||
"passwordsDoNotMatch": "Hasła nie są zgodne",
|
||||
"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",
|
||||
@@ -953,6 +973,7 @@
|
||||
"logoutError": "Błąd podczas wylogowywania",
|
||||
"signingAs": "Zalogowany jako",
|
||||
"serverAdmin": "Administrator serwera",
|
||||
"managedSelfhosted": "Zarządzane Samodzielnie-Hostingowane",
|
||||
"otpEnable": "Włącz uwierzytelnianie dwuskładnikowe",
|
||||
"otpDisable": "Wyłącz uwierzytelnianie dwuskładnikowe",
|
||||
"logout": "Wyloguj się",
|
||||
@@ -968,6 +989,9 @@
|
||||
"actionDeleteSite": "Usuń witrynę",
|
||||
"actionGetSite": "Pobierz witrynę",
|
||||
"actionListSites": "Lista witryn",
|
||||
"setupToken": "Skonfiguruj token",
|
||||
"setupTokenDescription": "Wprowadź token konfiguracji z konsoli serwera.",
|
||||
"setupTokenRequired": "Wymagany jest token konfiguracji",
|
||||
"actionUpdateSite": "Aktualizuj witrynę",
|
||||
"actionListSiteRoles": "Lista dozwolonych ról witryny",
|
||||
"actionCreateResource": "Utwórz zasób",
|
||||
@@ -1023,6 +1047,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 +1123,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 +1191,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 +1225,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 +1235,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 +1295,7 @@
|
||||
"createDomainName": "Nazwa:",
|
||||
"createDomainValue": "Wartość:",
|
||||
"createDomainCnameRecords": "Rekordy CNAME",
|
||||
"createDomainARecords": "Rekordy A",
|
||||
"createDomainRecordNumber": "Rekord {number}",
|
||||
"createDomainTxtRecords": "Rekordy TXT",
|
||||
"createDomainSaveTheseRecords": "Zapisz te rekordy",
|
||||
@@ -1273,5 +1303,152 @@
|
||||
"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": "Dodaj zakresy CIDR, które można uzyskać zdalnie z tej strony za pomocą klientów. Użyj formatu jak 10.0.0.0/24. Dotyczy to WYŁĄCZNIE łączności klienta VPN.",
|
||||
"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",
|
||||
"addNewTarget": "Dodaj nowy cel",
|
||||
"targetsList": "Lista celów",
|
||||
"targetErrorDuplicateTargetFound": "Znaleziono duplikat celu",
|
||||
"httpMethod": "Metoda HTTP",
|
||||
"selectHttpMethod": "Wybierz metodę HTTP",
|
||||
"domainPickerSubdomainLabel": "Poddomena",
|
||||
"domainPickerBaseDomainLabel": "Domen bazowa",
|
||||
"domainPickerSearchDomains": "Szukaj domen...",
|
||||
"domainPickerNoDomainsFound": "Nie znaleziono domen",
|
||||
"domainPickerLoadingDomains": "Ładowanie domen...",
|
||||
"domainPickerSelectBaseDomain": "Wybierz domenę bazową...",
|
||||
"domainPickerNotAvailableForCname": "Niedostępne dla domen CNAME",
|
||||
"domainPickerEnterSubdomainOrLeaveBlank": "Wprowadź poddomenę lub pozostaw puste, aby użyć domeny bazowej.",
|
||||
"domainPickerEnterSubdomainToSearch": "Wprowadź poddomenę, aby wyszukać i wybrać z dostępnych darmowych domen.",
|
||||
"domainPickerFreeDomains": "Darmowe domeny",
|
||||
"domainPickerSearchForAvailableDomains": "Szukaj dostępnych domen",
|
||||
"resourceDomain": "Domena",
|
||||
"resourceEditDomain": "Edytuj domenę",
|
||||
"siteName": "Nazwa strony",
|
||||
"proxyPort": "Port",
|
||||
"resourcesTableProxyResources": "Zasoby proxy",
|
||||
"resourcesTableClientResources": "Zasoby klienta",
|
||||
"resourcesTableNoProxyResourcesFound": "Nie znaleziono zasobów proxy.",
|
||||
"resourcesTableNoInternalResourcesFound": "Nie znaleziono wewnętrznych zasobów.",
|
||||
"resourcesTableDestination": "Miejsce docelowe",
|
||||
"resourcesTableTheseResourcesForUseWith": "Te zasoby są do użytku z",
|
||||
"resourcesTableClients": "Klientami",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "i są dostępne tylko wewnętrznie po połączeniu z klientem.",
|
||||
"editInternalResourceDialogEditClientResource": "Edytuj zasób klienta",
|
||||
"editInternalResourceDialogUpdateResourceProperties": "Zaktualizuj właściwości zasobu i konfigurację celu dla {resourceName}.",
|
||||
"editInternalResourceDialogResourceProperties": "Właściwości zasobów",
|
||||
"editInternalResourceDialogName": "Nazwa",
|
||||
"editInternalResourceDialogProtocol": "Protokół",
|
||||
"editInternalResourceDialogSitePort": "Port witryny",
|
||||
"editInternalResourceDialogTargetConfiguration": "Konfiguracja celu",
|
||||
"editInternalResourceDialogDestinationIP": "IP docelowe",
|
||||
"editInternalResourceDialogDestinationPort": "Port docelowy",
|
||||
"editInternalResourceDialogCancel": "Anuluj",
|
||||
"editInternalResourceDialogSaveResource": "Zapisz zasób",
|
||||
"editInternalResourceDialogSuccess": "Sukces",
|
||||
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Wewnętrzny zasób zaktualizowany pomyślnie",
|
||||
"editInternalResourceDialogError": "Błąd",
|
||||
"editInternalResourceDialogFailedToUpdateInternalResource": "Nie udało się zaktualizować wewnętrznego zasobu",
|
||||
"editInternalResourceDialogNameRequired": "Nazwa jest wymagana",
|
||||
"editInternalResourceDialogNameMaxLength": "Nazwa nie może mieć więcej niż 255 znaków",
|
||||
"editInternalResourceDialogProxyPortMin": "Port proxy musi wynosić przynajmniej 1",
|
||||
"editInternalResourceDialogProxyPortMax": "Port proxy nie może być większy niż 65536",
|
||||
"editInternalResourceDialogInvalidIPAddressFormat": "Nieprawidłowy format adresu IP",
|
||||
"editInternalResourceDialogDestinationPortMin": "Port docelowy musi wynosić przynajmniej 1",
|
||||
"editInternalResourceDialogDestinationPortMax": "Port docelowy nie może być większy niż 65536",
|
||||
"createInternalResourceDialogNoSitesAvailable": "Brak dostępnych stron",
|
||||
"createInternalResourceDialogNoSitesAvailableDescription": "Musisz mieć co najmniej jedną stronę Newt z skonfigurowanym podsiecią, aby tworzyć wewnętrzne zasoby.",
|
||||
"createInternalResourceDialogClose": "Zamknij",
|
||||
"createInternalResourceDialogCreateClientResource": "Utwórz zasób klienta",
|
||||
"createInternalResourceDialogCreateClientResourceDescription": "Utwórz nowy zasób, który będzie dostępny dla klientów połączonych z wybraną stroną.",
|
||||
"createInternalResourceDialogResourceProperties": "Właściwości zasobów",
|
||||
"createInternalResourceDialogName": "Nazwa",
|
||||
"createInternalResourceDialogSite": "Witryna",
|
||||
"createInternalResourceDialogSelectSite": "Wybierz stronę...",
|
||||
"createInternalResourceDialogSearchSites": "Szukaj stron...",
|
||||
"createInternalResourceDialogNoSitesFound": "Nie znaleziono stron.",
|
||||
"createInternalResourceDialogProtocol": "Protokół",
|
||||
"createInternalResourceDialogTcp": "TCP",
|
||||
"createInternalResourceDialogUdp": "UDP",
|
||||
"createInternalResourceDialogSitePort": "Port witryny",
|
||||
"createInternalResourceDialogSitePortDescription": "Użyj tego portu, aby uzyskać dostęp do zasobu na stronie, gdy połączony z klientem.",
|
||||
"createInternalResourceDialogTargetConfiguration": "Konfiguracja celu",
|
||||
"createInternalResourceDialogDestinationIP": "IP docelowe",
|
||||
"createInternalResourceDialogDestinationIPDescription": "Adres IP zasobu w sieci strony.",
|
||||
"createInternalResourceDialogDestinationPort": "Port docelowy",
|
||||
"createInternalResourceDialogDestinationPortDescription": "Port na docelowym IP, gdzie zasób jest dostępny.",
|
||||
"createInternalResourceDialogCancel": "Anuluj",
|
||||
"createInternalResourceDialogCreateResource": "Utwórz zasób",
|
||||
"createInternalResourceDialogSuccess": "Sukces",
|
||||
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Wewnętrzny zasób utworzony pomyślnie",
|
||||
"createInternalResourceDialogError": "Błąd",
|
||||
"createInternalResourceDialogFailedToCreateInternalResource": "Nie udało się utworzyć wewnętrznego zasobu",
|
||||
"createInternalResourceDialogNameRequired": "Nazwa jest wymagana",
|
||||
"createInternalResourceDialogNameMaxLength": "Nazwa nie może mieć więcej niż 255 znaków",
|
||||
"createInternalResourceDialogPleaseSelectSite": "Proszę wybrać stronę",
|
||||
"createInternalResourceDialogProxyPortMin": "Port proxy musi wynosić przynajmniej 1",
|
||||
"createInternalResourceDialogProxyPortMax": "Port proxy nie może być większy niż 65536",
|
||||
"createInternalResourceDialogInvalidIPAddressFormat": "Nieprawidłowy format adresu IP",
|
||||
"createInternalResourceDialogDestinationPortMin": "Port docelowy musi wynosić przynajmniej 1",
|
||||
"createInternalResourceDialogDestinationPortMax": "Port docelowy nie może być większy niż 65536",
|
||||
"siteConfiguration": "Konfiguracja",
|
||||
"siteAcceptClientConnections": "Akceptuj połączenia klienta",
|
||||
"siteAcceptClientConnectionsDescription": "Pozwól innym urządzeniom połączyć się przez tę instancję Newt jako bramę za pomocą klientów.",
|
||||
"siteAddress": "Adres strony",
|
||||
"siteAddressDescription": "Podaj adres IP hosta, do którego klienci będą się łączyć. Jest to wewnętrzny adres strony w sieci Pangolin dla klientów do adresowania. Musi zawierać się w podsieci organizacji.",
|
||||
"autoLoginExternalIdp": "Automatyczny login z zewnętrznym IDP",
|
||||
"autoLoginExternalIdpDescription": "Natychmiastowe przekierowanie użytkownika do zewnętrznego IDP w celu uwierzytelnienia.",
|
||||
"selectIdp": "Wybierz IDP",
|
||||
"selectIdpPlaceholder": "Wybierz IDP...",
|
||||
"selectIdpRequired": "Proszę wybrać IDP, gdy aktywne jest automatyczne logowanie.",
|
||||
"autoLoginTitle": "Przekierowywanie",
|
||||
"autoLoginDescription": "Przekierowanie do zewnętrznego dostawcy tożsamości w celu uwierzytelnienia.",
|
||||
"autoLoginProcessing": "Przygotowywanie uwierzytelniania...",
|
||||
"autoLoginRedirecting": "Przekierowanie do logowania...",
|
||||
"autoLoginError": "Błąd automatycznego logowania",
|
||||
"autoLoginErrorNoRedirectUrl": "Nie otrzymano URL przekierowania od dostawcy tożsamości.",
|
||||
"autoLoginErrorGeneratingUrl": "Nie udało się wygenerować URL uwierzytelniania."
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -95,7 +94,9 @@
|
||||
"siteNewtTunnelDescription": "A maneira mais fácil de criar um ponto de entrada na sua rede. Nenhuma configuração extra.",
|
||||
"siteWg": "WireGuard Básico",
|
||||
"siteWgDescription": "Use qualquer cliente do WireGuard para estabelecer um túnel. Configuração manual NAT é necessária.",
|
||||
"siteWgDescriptionSaas": "Use qualquer cliente WireGuard para estabelecer um túnel. Configuração manual NAT necessária. SOMENTE FUNCIONA EM NODES AUTO-HOSPEDADOS",
|
||||
"siteLocalDescription": "Recursos locais apenas. Sem túneis.",
|
||||
"siteLocalDescriptionSaas": "Apenas recursos locais. Sem tunelamento. SOMENTE FUNCIONA EM NODES AUTO-HOSPEDADOS",
|
||||
"siteSeeAll": "Ver todos os sites",
|
||||
"siteTunnelDescription": "Determine como você deseja se conectar ao seu site",
|
||||
"siteNewtCredentials": "Credenciais Novas",
|
||||
@@ -167,7 +168,7 @@
|
||||
"siteSelect": "Selecionar site",
|
||||
"siteSearch": "Procurar no site",
|
||||
"siteNotFound": "Nenhum site encontrado.",
|
||||
"siteSelectionDescription": "Este site fornecerá conectividade ao recurso.",
|
||||
"siteSelectionDescription": "Este site fornecerá conectividade ao destino.",
|
||||
"resourceType": "Tipo de Recurso",
|
||||
"resourceTypeDescription": "Determine como você deseja acessar seu recurso",
|
||||
"resourceHTTPSSettings": "Configurações de HTTPS",
|
||||
@@ -198,6 +199,7 @@
|
||||
"general": "Gerais",
|
||||
"generalSettings": "Configurações Gerais",
|
||||
"proxy": "Proxy",
|
||||
"internal": "Interno",
|
||||
"rules": "Regras",
|
||||
"resourceSettingDescription": "Configure as configurações do seu recurso",
|
||||
"resourceSetting": "Configurações do {resourceName}",
|
||||
@@ -491,7 +493,7 @@
|
||||
"targetTlsSniDescription": "O Nome do Servidor TLS para usar para SNI. Deixe vazio para usar o padrão.",
|
||||
"targetTlsSubmit": "Salvar Configurações",
|
||||
"targets": "Configuração de Alvos",
|
||||
"targetsDescription": "Configure alvos para rotear tráfego para seus serviços",
|
||||
"targetsDescription": "Configure alvos para rotear tráfego para seus serviços de backend",
|
||||
"targetStickySessions": "Ativar Sessões Persistentes",
|
||||
"targetStickySessionsDescription": "Manter conexões no mesmo alvo backend durante toda a sessão.",
|
||||
"methodSelect": "Selecionar método",
|
||||
@@ -834,6 +836,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": "Requisitos de senha:",
|
||||
"passwordRequirementLength": "Pelo menos 8 caracteres de comprimento",
|
||||
"passwordRequirementUppercase": "Pelo menos uma letra maiúscula",
|
||||
"passwordRequirementLowercase": "Pelo menos uma letra minúscula",
|
||||
"passwordRequirementNumber": "Pelo menos um número",
|
||||
"passwordRequirementSpecial": "Pelo menos um caractere especial",
|
||||
"passwordRequirementsMet": "✓ Senha atende a todos os requisitos",
|
||||
"passwordStrength": "Força da senha",
|
||||
"passwordStrengthWeak": "Fraca",
|
||||
"passwordStrengthMedium": "Média",
|
||||
"passwordStrengthStrong": "Forte",
|
||||
"passwordRequirements": "Requisitos:",
|
||||
"passwordRequirementLengthText": "8+ caracteres",
|
||||
"passwordRequirementUppercaseText": "Letra maiúscula (A-Z)",
|
||||
"passwordRequirementLowercaseText": "Letra minúscula (a-z)",
|
||||
"passwordRequirementNumberText": "Número (0-9)",
|
||||
"passwordRequirementSpecialText": "Caractere especial (!@#$%...)",
|
||||
"passwordsDoNotMatch": "As palavras-passe não correspondem",
|
||||
"otpEmailRequirementsLength": "O OTP deve ter pelo menos 1 caractere",
|
||||
"otpEmailSent": "OTP Enviado",
|
||||
"otpEmailSentDescription": "Um OTP foi enviado para o seu email",
|
||||
@@ -953,6 +973,7 @@
|
||||
"logoutError": "Erro ao terminar sessão",
|
||||
"signingAs": "Sessão iniciada como",
|
||||
"serverAdmin": "Administrador do Servidor",
|
||||
"managedSelfhosted": "Gerenciado Auto-Hospedado",
|
||||
"otpEnable": "Ativar Autenticação de Dois Fatores",
|
||||
"otpDisable": "Desativar Autenticação de Dois Fatores",
|
||||
"logout": "Terminar Sessão",
|
||||
@@ -968,6 +989,9 @@
|
||||
"actionDeleteSite": "Eliminar Site",
|
||||
"actionGetSite": "Obter Site",
|
||||
"actionListSites": "Listar Sites",
|
||||
"setupToken": "Configuração do Token",
|
||||
"setupTokenDescription": "Digite o token de configuração do console do servidor.",
|
||||
"setupTokenRequired": "Token de configuração é necessário",
|
||||
"actionUpdateSite": "Atualizar Site",
|
||||
"actionListSiteRoles": "Listar Funções Permitidas do Site",
|
||||
"actionCreateResource": "Criar Recurso",
|
||||
@@ -1023,6 +1047,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 +1123,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 +1191,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 +1225,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 +1235,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 +1295,7 @@
|
||||
"createDomainName": "Nome:",
|
||||
"createDomainValue": "Valor:",
|
||||
"createDomainCnameRecords": "Registros CNAME",
|
||||
"createDomainARecords": "Registros A",
|
||||
"createDomainRecordNumber": "Registrar {number}",
|
||||
"createDomainTxtRecords": "Registros TXT",
|
||||
"createDomainSaveTheseRecords": "Salvar Esses Registros",
|
||||
@@ -1273,5 +1303,152 @@
|
||||
"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": "Adicionar intervalos CIDR que podem ser acessados deste site remotamente usando clientes. Use um formato como 10.0.0.0/24. Isso SOMENTE se aplica à conectividade do cliente VPN.",
|
||||
"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",
|
||||
"addNewTarget": "Adicionar Novo Alvo",
|
||||
"targetsList": "Lista de Alvos",
|
||||
"targetErrorDuplicateTargetFound": "Alvo duplicado encontrado",
|
||||
"httpMethod": "Método HTTP",
|
||||
"selectHttpMethod": "Selecionar método HTTP",
|
||||
"domainPickerSubdomainLabel": "Subdomínio",
|
||||
"domainPickerBaseDomainLabel": "Domínio Base",
|
||||
"domainPickerSearchDomains": "Buscar domínios...",
|
||||
"domainPickerNoDomainsFound": "Nenhum domínio encontrado",
|
||||
"domainPickerLoadingDomains": "Carregando domínios...",
|
||||
"domainPickerSelectBaseDomain": "Selecione o domínio base...",
|
||||
"domainPickerNotAvailableForCname": "Não disponível para domínios CNAME",
|
||||
"domainPickerEnterSubdomainOrLeaveBlank": "Digite um subdomínio ou deixe em branco para usar o domínio base.",
|
||||
"domainPickerEnterSubdomainToSearch": "Digite um subdomínio para buscar e selecionar entre os domínios gratuitos disponíveis.",
|
||||
"domainPickerFreeDomains": "Domínios Gratuitos",
|
||||
"domainPickerSearchForAvailableDomains": "Pesquise por domínios disponíveis",
|
||||
"resourceDomain": "Domínio",
|
||||
"resourceEditDomain": "Editar Domínio",
|
||||
"siteName": "Nome do Site",
|
||||
"proxyPort": "Porta",
|
||||
"resourcesTableProxyResources": "Recursos de Proxy",
|
||||
"resourcesTableClientResources": "Recursos do Cliente",
|
||||
"resourcesTableNoProxyResourcesFound": "Nenhum recurso de proxy encontrado.",
|
||||
"resourcesTableNoInternalResourcesFound": "Nenhum recurso interno encontrado.",
|
||||
"resourcesTableDestination": "Destino",
|
||||
"resourcesTableTheseResourcesForUseWith": "Esses recursos são para uso com",
|
||||
"resourcesTableClients": "Clientes",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "e são acessíveis apenas internamente quando conectados com um cliente.",
|
||||
"editInternalResourceDialogEditClientResource": "Editar Recurso do Cliente",
|
||||
"editInternalResourceDialogUpdateResourceProperties": "Atualize as propriedades do recurso e a configuração do alvo para {resourceName}.",
|
||||
"editInternalResourceDialogResourceProperties": "Propriedades do Recurso",
|
||||
"editInternalResourceDialogName": "Nome",
|
||||
"editInternalResourceDialogProtocol": "Protocolo",
|
||||
"editInternalResourceDialogSitePort": "Porta do Site",
|
||||
"editInternalResourceDialogTargetConfiguration": "Configuração do Alvo",
|
||||
"editInternalResourceDialogDestinationIP": "IP de Destino",
|
||||
"editInternalResourceDialogDestinationPort": "Porta de Destino",
|
||||
"editInternalResourceDialogCancel": "Cancelar",
|
||||
"editInternalResourceDialogSaveResource": "Salvar Recurso",
|
||||
"editInternalResourceDialogSuccess": "Sucesso",
|
||||
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Recurso interno atualizado com sucesso",
|
||||
"editInternalResourceDialogError": "Erro",
|
||||
"editInternalResourceDialogFailedToUpdateInternalResource": "Falha ao atualizar recurso interno",
|
||||
"editInternalResourceDialogNameRequired": "Nome é obrigatório",
|
||||
"editInternalResourceDialogNameMaxLength": "Nome deve ser inferior a 255 caracteres",
|
||||
"editInternalResourceDialogProxyPortMin": "Porta de proxy deve ser pelo menos 1",
|
||||
"editInternalResourceDialogProxyPortMax": "Porta de proxy deve ser inferior a 65536",
|
||||
"editInternalResourceDialogInvalidIPAddressFormat": "Formato de endereço IP inválido",
|
||||
"editInternalResourceDialogDestinationPortMin": "Porta de destino deve ser pelo menos 1",
|
||||
"editInternalResourceDialogDestinationPortMax": "Porta de destino deve ser inferior a 65536",
|
||||
"createInternalResourceDialogNoSitesAvailable": "Nenhum Site Disponível",
|
||||
"createInternalResourceDialogNoSitesAvailableDescription": "Você precisa ter pelo menos um site Newt com uma sub-rede configurada para criar recursos internos.",
|
||||
"createInternalResourceDialogClose": "Fechar",
|
||||
"createInternalResourceDialogCreateClientResource": "Criar Recurso do Cliente",
|
||||
"createInternalResourceDialogCreateClientResourceDescription": "Crie um novo recurso que estará acessível aos clientes conectados ao site selecionado.",
|
||||
"createInternalResourceDialogResourceProperties": "Propriedades do Recurso",
|
||||
"createInternalResourceDialogName": "Nome",
|
||||
"createInternalResourceDialogSite": "Site",
|
||||
"createInternalResourceDialogSelectSite": "Selecionar site...",
|
||||
"createInternalResourceDialogSearchSites": "Procurar sites...",
|
||||
"createInternalResourceDialogNoSitesFound": "Nenhum site encontrado.",
|
||||
"createInternalResourceDialogProtocol": "Protocolo",
|
||||
"createInternalResourceDialogTcp": "TCP",
|
||||
"createInternalResourceDialogUdp": "UDP",
|
||||
"createInternalResourceDialogSitePort": "Porta do Site",
|
||||
"createInternalResourceDialogSitePortDescription": "Use esta porta para acessar o recurso no site quando conectado com um cliente.",
|
||||
"createInternalResourceDialogTargetConfiguration": "Configuração do Alvo",
|
||||
"createInternalResourceDialogDestinationIP": "IP de Destino",
|
||||
"createInternalResourceDialogDestinationIPDescription": "O endereço IP do recurso na rede do site.",
|
||||
"createInternalResourceDialogDestinationPort": "Porta de Destino",
|
||||
"createInternalResourceDialogDestinationPortDescription": "A porta no IP de destino onde o recurso está acessível.",
|
||||
"createInternalResourceDialogCancel": "Cancelar",
|
||||
"createInternalResourceDialogCreateResource": "Criar Recurso",
|
||||
"createInternalResourceDialogSuccess": "Sucesso",
|
||||
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Recurso interno criado com sucesso",
|
||||
"createInternalResourceDialogError": "Erro",
|
||||
"createInternalResourceDialogFailedToCreateInternalResource": "Falha ao criar recurso interno",
|
||||
"createInternalResourceDialogNameRequired": "Nome é obrigatório",
|
||||
"createInternalResourceDialogNameMaxLength": "Nome deve ser inferior a 255 caracteres",
|
||||
"createInternalResourceDialogPleaseSelectSite": "Por favor, selecione um site",
|
||||
"createInternalResourceDialogProxyPortMin": "Porta de proxy deve ser pelo menos 1",
|
||||
"createInternalResourceDialogProxyPortMax": "Porta de proxy deve ser inferior a 65536",
|
||||
"createInternalResourceDialogInvalidIPAddressFormat": "Formato de endereço IP inválido",
|
||||
"createInternalResourceDialogDestinationPortMin": "Porta de destino deve ser pelo menos 1",
|
||||
"createInternalResourceDialogDestinationPortMax": "Porta de destino deve ser inferior a 65536",
|
||||
"siteConfiguration": "Configuração",
|
||||
"siteAcceptClientConnections": "Aceitar Conexões de Clientes",
|
||||
"siteAcceptClientConnectionsDescription": "Permitir que outros dispositivos se conectem através desta instância Newt como um gateway usando clientes.",
|
||||
"siteAddress": "Endereço do Site",
|
||||
"siteAddressDescription": "Especificar o endereço IP do host para que os clientes se conectem. Este é o endereço interno do site na rede Pangolin para os clientes endereçarem. Deve estar dentro da sub-rede da Organização.",
|
||||
"autoLoginExternalIdp": "Login Automático com IDP Externo",
|
||||
"autoLoginExternalIdpDescription": "Redirecionar imediatamente o usuário para o IDP externo para autenticação.",
|
||||
"selectIdp": "Selecionar IDP",
|
||||
"selectIdpPlaceholder": "Escolher um IDP...",
|
||||
"selectIdpRequired": "Por favor, selecione um IDP quando o login automático estiver ativado.",
|
||||
"autoLoginTitle": "Redirecionando",
|
||||
"autoLoginDescription": "Redirecionando você para o provedor de identidade externo para autenticação.",
|
||||
"autoLoginProcessing": "Preparando autenticação...",
|
||||
"autoLoginRedirecting": "Redirecionando para login...",
|
||||
"autoLoginError": "Erro de Login Automático",
|
||||
"autoLoginErrorNoRedirectUrl": "Nenhum URL de redirecionamento recebido do provedor de identidade.",
|
||||
"autoLoginErrorGeneratingUrl": "Falha ao gerar URL de autenticação."
|
||||
}
|
||||
|
||||
1017
messages/ru-RU.json
1017
messages/ru-RU.json
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
@@ -95,7 +94,9 @@
|
||||
"siteNewtTunnelDescription": "Ağınıza giriş noktası oluşturmanın en kolay yolu. Ekstra kurulum gerekmez.",
|
||||
"siteWg": "Temel WireGuard",
|
||||
"siteWgDescription": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir.",
|
||||
"siteWgDescriptionSaas": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir. YALNIZCA SELF HOSTED DÜĞÜMLERDE ÇALIŞIR",
|
||||
"siteLocalDescription": "Yalnızca yerel kaynaklar. Tünelleme yok.",
|
||||
"siteLocalDescriptionSaas": "Yalnızca yerel kaynaklar. Tünel yok. YALNIZCA SELF HOSTED DÜĞÜMLERDE ÇALIŞIR",
|
||||
"siteSeeAll": "Tüm Siteleri Gör",
|
||||
"siteTunnelDescription": "Sitenize nasıl bağlanmak istediğinizi belirleyin",
|
||||
"siteNewtCredentials": "Newt Kimlik Bilgileri",
|
||||
@@ -167,7 +168,7 @@
|
||||
"siteSelect": "Site seç",
|
||||
"siteSearch": "Site ara",
|
||||
"siteNotFound": "Herhangi bir site bulunamadı.",
|
||||
"siteSelectionDescription": "Bu site, kaynağa bağlanabilirliği sağlayacaktır.",
|
||||
"siteSelectionDescription": "Bu site hedefe bağlantı sağlayacaktır.",
|
||||
"resourceType": "Kaynak Türü",
|
||||
"resourceTypeDescription": "Kaynağınıza nasıl erişmek istediğinizi belirleyin",
|
||||
"resourceHTTPSSettings": "HTTPS Ayarları",
|
||||
@@ -198,6 +199,7 @@
|
||||
"general": "Genel",
|
||||
"generalSettings": "Genel Ayarlar",
|
||||
"proxy": "Vekil Sunucu",
|
||||
"internal": "Dahili",
|
||||
"rules": "Kurallar",
|
||||
"resourceSettingDescription": "Kaynağınızdaki ayarları yapılandırın",
|
||||
"resourceSetting": "{resourceName} Ayarları",
|
||||
@@ -491,7 +493,7 @@
|
||||
"targetTlsSniDescription": "SNI için kullanılacak TLS Sunucu Adı'",
|
||||
"targetTlsSubmit": "Ayarları Kaydet",
|
||||
"targets": "Hedefler Konfigürasyonu",
|
||||
"targetsDescription": "Trafiği hizmetlerinize yönlendirmek için hedefleri ayarlayın",
|
||||
"targetsDescription": "Trafiği arka uç hizmetlerinize yönlendirmek için hedefleri ayarlayın",
|
||||
"targetStickySessions": "Yapışkan Oturumları Etkinleştir",
|
||||
"targetStickySessionsDescription": "Bağlantıları oturum süresince aynı arka uç hedef üzerinde tutun.",
|
||||
"methodSelect": "Yöntemi Seç",
|
||||
@@ -834,6 +836,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": "Şifre gereksinimleri:",
|
||||
"passwordRequirementLength": "En az 8 karakter uzunluğunda",
|
||||
"passwordRequirementUppercase": "En az bir büyük harf",
|
||||
"passwordRequirementLowercase": "En az bir küçük harf",
|
||||
"passwordRequirementNumber": "En az bir sayı",
|
||||
"passwordRequirementSpecial": "En az bir özel karakter",
|
||||
"passwordRequirementsMet": "✓ Şifre tüm gereksinimleri karşılıyor",
|
||||
"passwordStrength": "Şifre gücü",
|
||||
"passwordStrengthWeak": "Zayıf",
|
||||
"passwordStrengthMedium": "Orta",
|
||||
"passwordStrengthStrong": "Güçlü",
|
||||
"passwordRequirements": "Gereksinimler:",
|
||||
"passwordRequirementLengthText": "8+ karakter",
|
||||
"passwordRequirementUppercaseText": "Büyük harf (A-Z)",
|
||||
"passwordRequirementLowercaseText": "Küçük harf (a-z)",
|
||||
"passwordRequirementNumberText": "Sayı (0-9)",
|
||||
"passwordRequirementSpecialText": "Özel karakter (!@#$%...)",
|
||||
"passwordsDoNotMatch": "Parolalar eşleşmiyor",
|
||||
"otpEmailRequirementsLength": "OTP en az 1 karakter uzunluğunda olmalıdır",
|
||||
"otpEmailSent": "OTP Gönderildi",
|
||||
"otpEmailSentDescription": "E-posta adresinize bir OTP gönderildi",
|
||||
@@ -953,6 +973,7 @@
|
||||
"logoutError": "Çıkış yaparken hata",
|
||||
"signingAs": "Olarak giriş yapıldı",
|
||||
"serverAdmin": "Sunucu Yöneticisi",
|
||||
"managedSelfhosted": "Yönetilen Self-Hosted",
|
||||
"otpEnable": "İki faktörlü özelliğini etkinleştir",
|
||||
"otpDisable": "İki faktörlü özelliğini devre dışı bırak",
|
||||
"logout": "Çıkış Yap",
|
||||
@@ -968,6 +989,9 @@
|
||||
"actionDeleteSite": "Siteyi Sil",
|
||||
"actionGetSite": "Siteyi Al",
|
||||
"actionListSites": "Siteleri Listele",
|
||||
"setupToken": "Kurulum Simgesi",
|
||||
"setupTokenDescription": "Sunucu konsolundan kurulum simgesini girin.",
|
||||
"setupTokenRequired": "Kurulum simgesi gerekli",
|
||||
"actionUpdateSite": "Siteyi Güncelle",
|
||||
"actionListSiteRoles": "İzin Verilen Site Rolleri Listele",
|
||||
"actionCreateResource": "Kaynak Oluştur",
|
||||
@@ -1023,6 +1047,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 +1123,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 +1191,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 +1225,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 +1235,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 +1295,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 +1303,152 @@
|
||||
"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": "Bu siteye uzaktan erişilebilen CIDR aralıklarını ekleyin. 10.0.0.0/24 formatını kullanın. Bu YALNIZCA VPN istemci bağlantıları için geçerlidir.",
|
||||
"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",
|
||||
"addNewTarget": "Yeni Hedef Ekle",
|
||||
"targetsList": "Hedefler Listesi",
|
||||
"targetErrorDuplicateTargetFound": "Yinelenen hedef bulundu",
|
||||
"httpMethod": "HTTP Yöntemi",
|
||||
"selectHttpMethod": "HTTP yöntemini seçin",
|
||||
"domainPickerSubdomainLabel": "Alt Alan Adı",
|
||||
"domainPickerBaseDomainLabel": "Temel Alan Adı",
|
||||
"domainPickerSearchDomains": "Alan adlarını ara...",
|
||||
"domainPickerNoDomainsFound": "Hiçbir alan adı bulunamadı",
|
||||
"domainPickerLoadingDomains": "Alan adları yükleniyor...",
|
||||
"domainPickerSelectBaseDomain": "Temel alan adını seçin...",
|
||||
"domainPickerNotAvailableForCname": "CNAME alan adları için kullanılabilir değil",
|
||||
"domainPickerEnterSubdomainOrLeaveBlank": "Alt alan adını girin veya temel alan adını kullanmak için boş bırakın.",
|
||||
"domainPickerEnterSubdomainToSearch": "Mevcut ücretsiz alan adları arasından aramak ve seçmek için bir alt alan adı girin.",
|
||||
"domainPickerFreeDomains": "Ücretsiz Alan Adları",
|
||||
"domainPickerSearchForAvailableDomains": "Mevcut alan adlarını ara",
|
||||
"resourceDomain": "Alan Adı",
|
||||
"resourceEditDomain": "Alan Adını Düzenle",
|
||||
"siteName": "Site Adı",
|
||||
"proxyPort": "Bağlantı Noktası",
|
||||
"resourcesTableProxyResources": "Proxy Kaynaklar",
|
||||
"resourcesTableClientResources": "İstemci Kaynaklar",
|
||||
"resourcesTableNoProxyResourcesFound": "Hiçbir proxy kaynağı bulunamadı.",
|
||||
"resourcesTableNoInternalResourcesFound": "Hiçbir dahili kaynak bulunamadı.",
|
||||
"resourcesTableDestination": "Hedef",
|
||||
"resourcesTableTheseResourcesForUseWith": "Bu kaynaklar ile kullanılmak için",
|
||||
"resourcesTableClients": "İstemciler",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "veyalnızca bir istemci ile bağlandığında dahili olarak erişilebilir.",
|
||||
"editInternalResourceDialogEditClientResource": "İstemci Kaynağı Düzenleyin",
|
||||
"editInternalResourceDialogUpdateResourceProperties": "{resourceName} için kaynak özelliklerini ve hedef yapılandırmasını güncelleyin.",
|
||||
"editInternalResourceDialogResourceProperties": "Kaynak Özellikleri",
|
||||
"editInternalResourceDialogName": "Ad",
|
||||
"editInternalResourceDialogProtocol": "Protokol",
|
||||
"editInternalResourceDialogSitePort": "Site Bağlantı Noktası",
|
||||
"editInternalResourceDialogTargetConfiguration": "Hedef Yapılandırma",
|
||||
"editInternalResourceDialogDestinationIP": "Hedef IP",
|
||||
"editInternalResourceDialogDestinationPort": "Hedef Bağlantı Noktası",
|
||||
"editInternalResourceDialogCancel": "İptal",
|
||||
"editInternalResourceDialogSaveResource": "Kaynağı Kaydet",
|
||||
"editInternalResourceDialogSuccess": "Başarı",
|
||||
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Dahili kaynak başarıyla güncellendi",
|
||||
"editInternalResourceDialogError": "Hata",
|
||||
"editInternalResourceDialogFailedToUpdateInternalResource": "Dahili kaynak güncellenemedi",
|
||||
"editInternalResourceDialogNameRequired": "Ad gerekli",
|
||||
"editInternalResourceDialogNameMaxLength": "Ad 255 karakterden kısa olmalıdır",
|
||||
"editInternalResourceDialogProxyPortMin": "Proxy bağlantı noktası en az 1 olmalıdır",
|
||||
"editInternalResourceDialogProxyPortMax": "Proxy bağlantı noktası 65536'dan küçük olmalıdır",
|
||||
"editInternalResourceDialogInvalidIPAddressFormat": "Geçersiz IP adresi formatı",
|
||||
"editInternalResourceDialogDestinationPortMin": "Hedef bağlantı noktası en az 1 olmalıdır",
|
||||
"editInternalResourceDialogDestinationPortMax": "Hedef bağlantı noktası 65536'dan küçük olmalıdır",
|
||||
"createInternalResourceDialogNoSitesAvailable": "Site Bulunamadı",
|
||||
"createInternalResourceDialogNoSitesAvailableDescription": "Dahili kaynak oluşturmak için en az bir Newt sitesine ve alt ağa sahip olmalısınız.",
|
||||
"createInternalResourceDialogClose": "Kapat",
|
||||
"createInternalResourceDialogCreateClientResource": "İstemci Kaynağı Oluştur",
|
||||
"createInternalResourceDialogCreateClientResourceDescription": "Seçilen siteye bağlı istemciler için erişilebilir olacak yeni bir kaynak oluşturun.",
|
||||
"createInternalResourceDialogResourceProperties": "Kaynak Özellikleri",
|
||||
"createInternalResourceDialogName": "Ad",
|
||||
"createInternalResourceDialogSite": "Site",
|
||||
"createInternalResourceDialogSelectSite": "Site seç...",
|
||||
"createInternalResourceDialogSearchSites": "Siteleri ara...",
|
||||
"createInternalResourceDialogNoSitesFound": "Site bulunamadı.",
|
||||
"createInternalResourceDialogProtocol": "Protokol",
|
||||
"createInternalResourceDialogTcp": "TCP",
|
||||
"createInternalResourceDialogUdp": "UDP",
|
||||
"createInternalResourceDialogSitePort": "Site Bağlantı Noktası",
|
||||
"createInternalResourceDialogSitePortDescription": "İstemci ile bağlanıldığında site üzerindeki kaynağa erişmek için bu bağlantı noktasını kullanın.",
|
||||
"createInternalResourceDialogTargetConfiguration": "Hedef Yapılandırma",
|
||||
"createInternalResourceDialogDestinationIP": "Hedef IP",
|
||||
"createInternalResourceDialogDestinationIPDescription": "Site ağındaki kaynağın IP adresi.",
|
||||
"createInternalResourceDialogDestinationPort": "Hedef Bağlantı Noktası",
|
||||
"createInternalResourceDialogDestinationPortDescription": "Kaynağa erişilebilecek hedef IP üzerindeki bağlantı noktası.",
|
||||
"createInternalResourceDialogCancel": "İptal",
|
||||
"createInternalResourceDialogCreateResource": "Kaynak Oluştur",
|
||||
"createInternalResourceDialogSuccess": "Başarı",
|
||||
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Dahili kaynak başarıyla oluşturuldu",
|
||||
"createInternalResourceDialogError": "Hata",
|
||||
"createInternalResourceDialogFailedToCreateInternalResource": "Dahili kaynak oluşturulamadı",
|
||||
"createInternalResourceDialogNameRequired": "Ad gerekli",
|
||||
"createInternalResourceDialogNameMaxLength": "Ad 255 karakterden kısa olmalıdır",
|
||||
"createInternalResourceDialogPleaseSelectSite": "Lütfen bir site seçin",
|
||||
"createInternalResourceDialogProxyPortMin": "Proxy bağlantı noktası en az 1 olmalıdır",
|
||||
"createInternalResourceDialogProxyPortMax": "Proxy bağlantı noktası 65536'dan küçük olmalıdır",
|
||||
"createInternalResourceDialogInvalidIPAddressFormat": "Geçersiz IP adresi formatı",
|
||||
"createInternalResourceDialogDestinationPortMin": "Hedef bağlantı noktası en az 1 olmalıdır",
|
||||
"createInternalResourceDialogDestinationPortMax": "Hedef bağlantı noktası 65536'dan küçük olmalıdır",
|
||||
"siteConfiguration": "Yapılandırma",
|
||||
"siteAcceptClientConnections": "İstemci Bağlantılarını Kabul Et",
|
||||
"siteAcceptClientConnectionsDescription": "Bu Newt örneğini bir geçit olarak kullanarak diğer cihazların bağlanmasına izin verin.",
|
||||
"siteAddress": "Site Adresi",
|
||||
"siteAddressDescription": "İstemcilerin bağlanması için hostun IP adresini belirtin. Bu, Pangolin ağındaki sitenin iç adresidir ve istemciler için atlas olmalıdır. Org alt ağına düşmelidir.",
|
||||
"autoLoginExternalIdp": "Harici IDP ile Otomatik Giriş",
|
||||
"autoLoginExternalIdpDescription": "Kullanıcıyı kimlik doğrulama için otomatik olarak harici IDP'ye yönlendirin.",
|
||||
"selectIdp": "IDP Seç",
|
||||
"selectIdpPlaceholder": "IDP seçin...",
|
||||
"selectIdpRequired": "Otomatik giriş etkinleştirildiğinde lütfen bir IDP seçin.",
|
||||
"autoLoginTitle": "Yönlendiriliyor",
|
||||
"autoLoginDescription": "Kimlik doğrulama için harici kimlik sağlayıcıya yönlendiriliyorsunuz.",
|
||||
"autoLoginProcessing": "Kimlik doğrulama hazırlanıyor...",
|
||||
"autoLoginRedirecting": "Girişe yönlendiriliyorsunuz...",
|
||||
"autoLoginError": "Otomatik Giriş Hatası",
|
||||
"autoLoginErrorNoRedirectUrl": "Kimlik sağlayıcıdan yönlendirme URL'si alınamadı.",
|
||||
"autoLoginErrorGeneratingUrl": "Kimlik doğrulama URL'si oluşturulamadı."
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@
|
||||
"siteErrorCreate": "创建站点出错",
|
||||
"siteErrorCreateKeyPair": "找不到密钥对或站点默认值",
|
||||
"siteErrorCreateDefaults": "未找到站点默认值",
|
||||
"siteNameDescription": "这是站点的显示名称。",
|
||||
"method": "方法",
|
||||
"siteMethodDescription": "这是您将如何显示连接。",
|
||||
"siteLearnNewt": "学习如何在您的系统上安装 Newt",
|
||||
@@ -95,7 +94,9 @@
|
||||
"siteNewtTunnelDescription": "最简单的方式来连接到您的网络。不需要任何额外设置。",
|
||||
"siteWg": "基本 WireGuard",
|
||||
"siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。",
|
||||
"siteWgDescriptionSaas": "使用任何WireGuard客户端建立隧道。需要手动配置NAT。仅适用于自托管节点。",
|
||||
"siteLocalDescription": "仅限本地资源。不需要隧道。",
|
||||
"siteLocalDescriptionSaas": "仅本地资源。无需隧道。仅适用于自托管节点。",
|
||||
"siteSeeAll": "查看所有站点",
|
||||
"siteTunnelDescription": "确定如何连接到您的网站",
|
||||
"siteNewtCredentials": "Newt 凭据",
|
||||
@@ -167,7 +168,7 @@
|
||||
"siteSelect": "选择站点",
|
||||
"siteSearch": "搜索站点",
|
||||
"siteNotFound": "未找到站点。",
|
||||
"siteSelectionDescription": "此站点将为资源提供连接。",
|
||||
"siteSelectionDescription": "此站点将为目标提供连接。",
|
||||
"resourceType": "资源类型",
|
||||
"resourceTypeDescription": "确定如何访问您的资源",
|
||||
"resourceHTTPSSettings": "HTTPS 设置",
|
||||
@@ -198,6 +199,7 @@
|
||||
"general": "概览",
|
||||
"generalSettings": "常规设置",
|
||||
"proxy": "代理服务器",
|
||||
"internal": "内部设置",
|
||||
"rules": "规则",
|
||||
"resourceSettingDescription": "配置您资源上的设置",
|
||||
"resourceSetting": "{resourceName} 设置",
|
||||
@@ -491,7 +493,7 @@
|
||||
"targetTlsSniDescription": "SNI使用的 TLS 服务器名称。留空使用默认值。",
|
||||
"targetTlsSubmit": "保存设置",
|
||||
"targets": "目标配置",
|
||||
"targetsDescription": "设置目标来路由流量到您的服务",
|
||||
"targetsDescription": "设置目标来路由流量到您的后端服务",
|
||||
"targetStickySessions": "启用置顶会话",
|
||||
"targetStickySessionsDescription": "将连接保持在同一个后端目标的整个会话中。",
|
||||
"methodSelect": "选择方法",
|
||||
@@ -834,6 +836,24 @@
|
||||
"pincodeRequirementsLength": "PIN码必须是6位数字",
|
||||
"pincodeRequirementsChars": "PIN 必须只包含数字",
|
||||
"passwordRequirementsLength": "密码必须至少 1 个字符长",
|
||||
"passwordRequirementsTitle": "密码要求:",
|
||||
"passwordRequirementLength": "至少8个字符长",
|
||||
"passwordRequirementUppercase": "至少一个大写字母",
|
||||
"passwordRequirementLowercase": "至少一个小写字母",
|
||||
"passwordRequirementNumber": "至少一个数字",
|
||||
"passwordRequirementSpecial": "至少一个特殊字符",
|
||||
"passwordRequirementsMet": "✓ 密码满足所有要求",
|
||||
"passwordStrength": "密码强度",
|
||||
"passwordStrengthWeak": "弱",
|
||||
"passwordStrengthMedium": "中",
|
||||
"passwordStrengthStrong": "强",
|
||||
"passwordRequirements": "要求:",
|
||||
"passwordRequirementLengthText": "8+ 个字符",
|
||||
"passwordRequirementUppercaseText": "大写字母 (A-Z)",
|
||||
"passwordRequirementLowercaseText": "小写字母 (a-z)",
|
||||
"passwordRequirementNumberText": "数字 (0-9)",
|
||||
"passwordRequirementSpecialText": "特殊字符 (!@#$%...)",
|
||||
"passwordsDoNotMatch": "密码不匹配",
|
||||
"otpEmailRequirementsLength": "OTP 必须至少 1 个字符长",
|
||||
"otpEmailSent": "OTP 已发送",
|
||||
"otpEmailSentDescription": "OTP 已经发送到您的电子邮件",
|
||||
@@ -953,6 +973,7 @@
|
||||
"logoutError": "注销错误",
|
||||
"signingAs": "登录为",
|
||||
"serverAdmin": "服务器管理员",
|
||||
"managedSelfhosted": "托管自托管",
|
||||
"otpEnable": "启用双因子认证",
|
||||
"otpDisable": "禁用双因子认证",
|
||||
"logout": "登出",
|
||||
@@ -968,6 +989,9 @@
|
||||
"actionDeleteSite": "删除站点",
|
||||
"actionGetSite": "获取站点",
|
||||
"actionListSites": "站点列表",
|
||||
"setupToken": "设置令牌",
|
||||
"setupTokenDescription": "从服务器控制台输入设置令牌。",
|
||||
"setupTokenRequired": "需要设置令牌",
|
||||
"actionUpdateSite": "更新站点",
|
||||
"actionListSiteRoles": "允许站点角色列表",
|
||||
"actionCreateResource": "创建资源",
|
||||
@@ -1023,6 +1047,11 @@
|
||||
"actionDeleteIdpOrg": "删除 IDP组织策略",
|
||||
"actionListIdpOrgs": "列出 IDP组织",
|
||||
"actionUpdateIdpOrg": "更新 IDP组织",
|
||||
"actionCreateClient": "创建客户端",
|
||||
"actionDeleteClient": "删除客户端",
|
||||
"actionUpdateClient": "更新客户端",
|
||||
"actionListClients": "列出客户端",
|
||||
"actionGetClient": "获取客户端",
|
||||
"noneSelected": "未选择",
|
||||
"orgNotFound2": "未找到组织。",
|
||||
"searchProgress": "搜索中...",
|
||||
@@ -1094,7 +1123,7 @@
|
||||
"sidebarAllUsers": "所有用户",
|
||||
"sidebarIdentityProviders": "身份提供商",
|
||||
"sidebarLicense": "证书",
|
||||
"sidebarClients": "客户",
|
||||
"sidebarClients": "客户端(测试版)",
|
||||
"sidebarDomains": "域",
|
||||
"enableDockerSocket": "启用停靠套接字",
|
||||
"enableDockerSocketDescription": "启用 Docker Socket 发现以填充容器信息。必须向 Newt 提供 Socket 路径。",
|
||||
@@ -1162,7 +1191,7 @@
|
||||
"selectDomainTypeCnameName": "单个域(CNAME)",
|
||||
"selectDomainTypeCnameDescription": "仅此特定域。用于单个子域或特定域条目。",
|
||||
"selectDomainTypeWildcardName": "通配符域",
|
||||
"selectDomainTypeWildcardDescription": "此域及其第一级子域。",
|
||||
"selectDomainTypeWildcardDescription": "此域名及其子域名。",
|
||||
"domainDelegation": "单个域",
|
||||
"selectType": "选择一个类型",
|
||||
"actions": "操作",
|
||||
@@ -1196,7 +1225,7 @@
|
||||
"sidebarExpand": "展开",
|
||||
"newtUpdateAvailable": "更新可用",
|
||||
"newtUpdateAvailableInfo": "新版本的 Newt 已可用。请更新到最新版本以获得最佳体验。",
|
||||
"domainPickerEnterDomain": "输入您的域",
|
||||
"domainPickerEnterDomain": "域名",
|
||||
"domainPickerPlaceholder": "myapp.example.com、api.v1.mydomain.com 或仅 myapp",
|
||||
"domainPickerDescription": "输入资源的完整域名以查看可用选项。",
|
||||
"domainPickerDescriptionSaas": "输入完整域名、子域或名称以查看可用选项。",
|
||||
@@ -1206,7 +1235,7 @@
|
||||
"domainPickerSortAsc": "A-Z",
|
||||
"domainPickerSortDesc": "Z-A",
|
||||
"domainPickerCheckingAvailability": "检查可用性...",
|
||||
"domainPickerNoMatchingDomains": "未找到 \"{userInput}\" 的匹配域。尝试其他域或检查您组织的域设置。",
|
||||
"domainPickerNoMatchingDomains": "未找到匹配的域名。尝试不同的域名或检查您组织的域名设置。",
|
||||
"domainPickerOrganizationDomains": "组织域",
|
||||
"domainPickerProvidedDomains": "提供的域",
|
||||
"domainPickerSubdomain": "子域:{subdomain}",
|
||||
@@ -1266,6 +1295,7 @@
|
||||
"createDomainName": "名称:",
|
||||
"createDomainValue": "值:",
|
||||
"createDomainCnameRecords": "CNAME 记录",
|
||||
"createDomainARecords": "A记录",
|
||||
"createDomainRecordNumber": "记录 {number}",
|
||||
"createDomainTxtRecords": "TXT 记录",
|
||||
"createDomainSaveTheseRecords": "保存这些记录",
|
||||
@@ -1273,5 +1303,152 @@
|
||||
"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": "添加可以通过客户端远程访问该站点的CIDR范围。使用类似10.0.0.0/24的格式。这仅适用于VPN客户端连接。",
|
||||
"resourceEnableProxy": "启用公共代理",
|
||||
"resourceEnableProxyDescription": "启用到此资源的公共代理。这允许外部网络通过开放端口访问资源。需要 Traefik 配置。",
|
||||
"externalProxyEnabled": "外部代理已启用",
|
||||
"addNewTarget": "添加新目标",
|
||||
"targetsList": "目标列表",
|
||||
"targetErrorDuplicateTargetFound": "找到重复的目标",
|
||||
"httpMethod": "HTTP 方法",
|
||||
"selectHttpMethod": "选择 HTTP 方法",
|
||||
"domainPickerSubdomainLabel": "子域名",
|
||||
"domainPickerBaseDomainLabel": "根域名",
|
||||
"domainPickerSearchDomains": "搜索域名...",
|
||||
"domainPickerNoDomainsFound": "未找到域名",
|
||||
"domainPickerLoadingDomains": "加载域名...",
|
||||
"domainPickerSelectBaseDomain": "选择根域名...",
|
||||
"domainPickerNotAvailableForCname": "不适用于CNAME域",
|
||||
"domainPickerEnterSubdomainOrLeaveBlank": "输入子域名或留空以使用根域名。",
|
||||
"domainPickerEnterSubdomainToSearch": "输入一个子域名以搜索并从可用免费域名中选择。",
|
||||
"domainPickerFreeDomains": "免费域名",
|
||||
"domainPickerSearchForAvailableDomains": "搜索可用域名",
|
||||
"resourceDomain": "域名",
|
||||
"resourceEditDomain": "编辑域名",
|
||||
"siteName": "站点名称",
|
||||
"proxyPort": "端口",
|
||||
"resourcesTableProxyResources": "代理资源",
|
||||
"resourcesTableClientResources": "客户端资源",
|
||||
"resourcesTableNoProxyResourcesFound": "未找到代理资源。",
|
||||
"resourcesTableNoInternalResourcesFound": "未找到内部资源。",
|
||||
"resourcesTableDestination": "目标",
|
||||
"resourcesTableTheseResourcesForUseWith": "这些资源供...使用",
|
||||
"resourcesTableClients": "客户端",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "且仅在与客户端连接时可内部访问。",
|
||||
"editInternalResourceDialogEditClientResource": "编辑客户端资源",
|
||||
"editInternalResourceDialogUpdateResourceProperties": "更新{resourceName}的资源属性和目标配置。",
|
||||
"editInternalResourceDialogResourceProperties": "资源属性",
|
||||
"editInternalResourceDialogName": "名称",
|
||||
"editInternalResourceDialogProtocol": "协议",
|
||||
"editInternalResourceDialogSitePort": "站点端口",
|
||||
"editInternalResourceDialogTargetConfiguration": "目标配置",
|
||||
"editInternalResourceDialogDestinationIP": "目标IP",
|
||||
"editInternalResourceDialogDestinationPort": "目标端口",
|
||||
"editInternalResourceDialogCancel": "取消",
|
||||
"editInternalResourceDialogSaveResource": "保存资源",
|
||||
"editInternalResourceDialogSuccess": "成功",
|
||||
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "内部资源更新成功",
|
||||
"editInternalResourceDialogError": "错误",
|
||||
"editInternalResourceDialogFailedToUpdateInternalResource": "更新内部资源失败",
|
||||
"editInternalResourceDialogNameRequired": "名称为必填项",
|
||||
"editInternalResourceDialogNameMaxLength": "名称长度必须小于255个字符",
|
||||
"editInternalResourceDialogProxyPortMin": "代理端口必须至少为1",
|
||||
"editInternalResourceDialogProxyPortMax": "代理端口必须小于65536",
|
||||
"editInternalResourceDialogInvalidIPAddressFormat": "无效的IP地址格式",
|
||||
"editInternalResourceDialogDestinationPortMin": "目标端口必须至少为1",
|
||||
"editInternalResourceDialogDestinationPortMax": "目标端口必须小于65536",
|
||||
"createInternalResourceDialogNoSitesAvailable": "暂无可用站点",
|
||||
"createInternalResourceDialogNoSitesAvailableDescription": "您需要至少配置一个子网的Newt站点来创建内部资源。",
|
||||
"createInternalResourceDialogClose": "关闭",
|
||||
"createInternalResourceDialogCreateClientResource": "创建客户端资源",
|
||||
"createInternalResourceDialogCreateClientResourceDescription": "创建一个新资源,该资源将可供连接到所选站点的客户端访问。",
|
||||
"createInternalResourceDialogResourceProperties": "资源属性",
|
||||
"createInternalResourceDialogName": "名称",
|
||||
"createInternalResourceDialogSite": "站点",
|
||||
"createInternalResourceDialogSelectSite": "选择站点...",
|
||||
"createInternalResourceDialogSearchSites": "搜索站点...",
|
||||
"createInternalResourceDialogNoSitesFound": "未找到站点。",
|
||||
"createInternalResourceDialogProtocol": "协议",
|
||||
"createInternalResourceDialogTcp": "TCP",
|
||||
"createInternalResourceDialogUdp": "UDP",
|
||||
"createInternalResourceDialogSitePort": "站点端口",
|
||||
"createInternalResourceDialogSitePortDescription": "使用此端口在连接到客户端时访问站点上的资源。",
|
||||
"createInternalResourceDialogTargetConfiguration": "目标配置",
|
||||
"createInternalResourceDialogDestinationIP": "目标IP",
|
||||
"createInternalResourceDialogDestinationIPDescription": "站点网络上资源的IP地址。",
|
||||
"createInternalResourceDialogDestinationPort": "目标端口",
|
||||
"createInternalResourceDialogDestinationPortDescription": "资源在目标IP上可访问的端口。",
|
||||
"createInternalResourceDialogCancel": "取消",
|
||||
"createInternalResourceDialogCreateResource": "创建资源",
|
||||
"createInternalResourceDialogSuccess": "成功",
|
||||
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "内部资源创建成功",
|
||||
"createInternalResourceDialogError": "错误",
|
||||
"createInternalResourceDialogFailedToCreateInternalResource": "创建内部资源失败",
|
||||
"createInternalResourceDialogNameRequired": "名称为必填项",
|
||||
"createInternalResourceDialogNameMaxLength": "名称长度必须小于255个字符",
|
||||
"createInternalResourceDialogPleaseSelectSite": "请选择一个站点",
|
||||
"createInternalResourceDialogProxyPortMin": "代理端口必须至少为1",
|
||||
"createInternalResourceDialogProxyPortMax": "代理端口必须小于65536",
|
||||
"createInternalResourceDialogInvalidIPAddressFormat": "无效的IP地址格式",
|
||||
"createInternalResourceDialogDestinationPortMin": "目标端口必须至少为1",
|
||||
"createInternalResourceDialogDestinationPortMax": "目标端口必须小于65536",
|
||||
"siteConfiguration": "配置",
|
||||
"siteAcceptClientConnections": "接受客户端连接",
|
||||
"siteAcceptClientConnectionsDescription": "允许其他设备通过此Newt实例使用客户端作为网关连接。",
|
||||
"siteAddress": "站点地址",
|
||||
"siteAddressDescription": "指定主机的IP地址以供客户端连接。这是Pangolin网络中站点的内部地址,供客户端访问。必须在Org子网内。",
|
||||
"autoLoginExternalIdp": "自动使用外部IDP登录",
|
||||
"autoLoginExternalIdpDescription": "立即将用户重定向到外部IDP进行身份验证。",
|
||||
"selectIdp": "选择IDP",
|
||||
"selectIdpPlaceholder": "选择一个IDP...",
|
||||
"selectIdpRequired": "在启用自动登录时,请选择一个IDP。",
|
||||
"autoLoginTitle": "重定向中",
|
||||
"autoLoginDescription": "正在将您重定向到外部身份提供商进行身份验证。",
|
||||
"autoLoginProcessing": "准备身份验证...",
|
||||
"autoLoginRedirecting": "重定向到登录...",
|
||||
"autoLoginError": "自动登录错误",
|
||||
"autoLoginErrorNoRedirectUrl": "未从身份提供商收到重定向URL。",
|
||||
"autoLoginErrorGeneratingUrl": "生成身份验证URL失败。"
|
||||
}
|
||||
|
||||
2167
package-lock.json
generated
2167
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
84
package.json
84
package.json
@@ -21,8 +21,7 @@
|
||||
"db:clear-migrations": "rm -rf server/migrations",
|
||||
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
|
||||
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
|
||||
"start:sqlite": "DB_TYPE=sqlite NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'",
|
||||
"start:pg": "DB_TYPE=pg NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'",
|
||||
"start": "DB_TYPE=sqlite NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'",
|
||||
"email": "email dev --dir server/emails/templates --port 3005",
|
||||
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs"
|
||||
},
|
||||
@@ -33,32 +32,32 @@
|
||||
"@oslojs/crypto": "1.0.1",
|
||||
"@oslojs/encoding": "1.1.0",
|
||||
"@radix-ui/react-avatar": "1.1.10",
|
||||
"@radix-ui/react-checkbox": "1.3.2",
|
||||
"@radix-ui/react-collapsible": "1.1.11",
|
||||
"@radix-ui/react-dialog": "1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.15",
|
||||
"@radix-ui/react-checkbox": "1.3.3",
|
||||
"@radix-ui/react-collapsible": "1.1.12",
|
||||
"@radix-ui/react-dialog": "1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.16",
|
||||
"@radix-ui/react-icons": "1.3.2",
|
||||
"@radix-ui/react-label": "2.1.7",
|
||||
"@radix-ui/react-popover": "1.1.14",
|
||||
"@radix-ui/react-popover": "1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-radio-group": "1.3.7",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
"@radix-ui/react-select": "2.2.5",
|
||||
"@radix-ui/react-radio-group": "1.3.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "2.2.6",
|
||||
"@radix-ui/react-separator": "1.1.7",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-switch": "1.2.5",
|
||||
"@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",
|
||||
"@radix-ui/react-switch": "1.2.6",
|
||||
"@radix-ui/react-tabs": "1.1.13",
|
||||
"@radix-ui/react-toast": "1.2.15",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@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,11 +68,11 @@
|
||||
"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",
|
||||
"express-rate-limit": "7.5.1",
|
||||
"drizzle-orm": "0.44.4",
|
||||
"eslint": "9.33.0",
|
||||
"eslint-config-next": "15.4.6",
|
||||
"express": "5.1.0",
|
||||
"express-rate-limit": "8.0.1",
|
||||
"glob": "11.0.3",
|
||||
"helmet": "8.1.0",
|
||||
"http-errors": "2.0.0",
|
||||
@@ -82,69 +81,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.7",
|
||||
"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.49.0",
|
||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||
"@tailwindcss/postcss": "^4.1.10",
|
||||
"@tailwindcss/postcss": "^4.1.12",
|
||||
"@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.40.0"
|
||||
},
|
||||
"overrides": {
|
||||
"emblor": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -24,8 +24,8 @@ export const SESSION_COOKIE_EXPIRES =
|
||||
60 *
|
||||
60 *
|
||||
config.getRawConfig().server.dashboard_session_length_hours;
|
||||
export const COOKIE_DOMAIN =
|
||||
"." + new URL(config.getRawConfig().app.dashboard_url).hostname;
|
||||
export const COOKIE_DOMAIN = config.getRawConfig().app.dashboard_url ?
|
||||
"." + new URL(config.getRawConfig().app.dashboard_url!).hostname : undefined;
|
||||
|
||||
export function generateSessionToken(): string {
|
||||
const bytes = new Uint8Array(20);
|
||||
|
||||
@@ -4,6 +4,9 @@ import { resourceSessions, ResourceSession } from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
import axios from "axios";
|
||||
import logger from "@server/logger";
|
||||
import { tokenManager } from "@server/lib/tokenManager";
|
||||
|
||||
export const SESSION_COOKIE_NAME =
|
||||
config.getRawConfig().server.session_cookie_name;
|
||||
@@ -62,6 +65,29 @@ export async function validateResourceSessionToken(
|
||||
token: string,
|
||||
resourceId: number
|
||||
): Promise<ResourceSessionValidationResult> {
|
||||
if (config.isManagedMode()) {
|
||||
try {
|
||||
const response = await axios.post(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/session/validate`, {
|
||||
token: token
|
||||
}, await tokenManager.getAuthHeader());
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error validating resource session token in hybrid mode:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error validating resource session token in hybrid mode:", error);
|
||||
}
|
||||
return { resourceSession: null };
|
||||
}
|
||||
}
|
||||
|
||||
const sessionId = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(token))
|
||||
);
|
||||
|
||||
@@ -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(),
|
||||
@@ -117,7 +124,26 @@ export const exitNodes = pgTable("exitNodes", {
|
||||
publicKey: varchar("publicKey").notNull(),
|
||||
listenPort: integer("listenPort").notNull(),
|
||||
reachableAt: varchar("reachableAt"),
|
||||
maxConnections: integer("maxConnections")
|
||||
maxConnections: integer("maxConnections"),
|
||||
online: boolean("online").notNull().default(false),
|
||||
lastPing: integer("lastPing"),
|
||||
type: text("type").default("gerbil") // gerbil, remoteExitNode
|
||||
});
|
||||
|
||||
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", {
|
||||
@@ -135,6 +161,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 +532,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 +550,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 +569,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 +592,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 +616,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 +669,6 @@ 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>;
|
||||
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
||||
|
||||
277
server/db/queries/verifySessionQueries.ts
Normal file
277
server/db/queries/verifySessionQueries.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
import { db } from "@server/db";
|
||||
import {
|
||||
Resource,
|
||||
ResourcePassword,
|
||||
ResourcePincode,
|
||||
ResourceRule,
|
||||
resourcePassword,
|
||||
resourcePincode,
|
||||
resourceRules,
|
||||
resources,
|
||||
roleResources,
|
||||
sessions,
|
||||
userOrgs,
|
||||
userResources,
|
||||
users
|
||||
} from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import axios from "axios";
|
||||
import config from "@server/lib/config";
|
||||
import logger from "@server/logger";
|
||||
import { tokenManager } from "@server/lib/tokenManager";
|
||||
|
||||
export type ResourceWithAuth = {
|
||||
resource: Resource | null;
|
||||
pincode: ResourcePincode | null;
|
||||
password: ResourcePassword | null;
|
||||
};
|
||||
|
||||
export type UserSessionWithUser = {
|
||||
session: any;
|
||||
user: any;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get resource by domain with pincode and password information
|
||||
*/
|
||||
export async function getResourceByDomain(
|
||||
domain: string
|
||||
): Promise<ResourceWithAuth | null> {
|
||||
if (config.isManagedMode()) {
|
||||
try {
|
||||
const response = await axios.get(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/domain/${domain}`, await tokenManager.getAuthHeader());
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error fetching config in verify session:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error fetching config in verify session:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const [result] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.leftJoin(
|
||||
resourcePincode,
|
||||
eq(resourcePincode.resourceId, resources.resourceId)
|
||||
)
|
||||
.leftJoin(
|
||||
resourcePassword,
|
||||
eq(resourcePassword.resourceId, resources.resourceId)
|
||||
)
|
||||
.where(eq(resources.fullDomain, domain))
|
||||
.limit(1);
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
resource: result.resources,
|
||||
pincode: result.resourcePincode,
|
||||
password: result.resourcePassword
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user session with user information
|
||||
*/
|
||||
export async function getUserSessionWithUser(
|
||||
userSessionId: string
|
||||
): Promise<UserSessionWithUser | null> {
|
||||
if (config.isManagedMode()) {
|
||||
try {
|
||||
const response = await axios.get(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/session/${userSessionId}`, await tokenManager.getAuthHeader());
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error fetching config in verify session:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error fetching config in verify session:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const [res] = await db
|
||||
.select()
|
||||
.from(sessions)
|
||||
.leftJoin(users, eq(users.userId, sessions.userId))
|
||||
.where(eq(sessions.sessionId, userSessionId));
|
||||
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
session: res.session,
|
||||
user: res.user
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user organization role
|
||||
*/
|
||||
export async function getUserOrgRole(userId: string, orgId: string) {
|
||||
if (config.isManagedMode()) {
|
||||
try {
|
||||
const response = await axios.get(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/org/${orgId}/role`, await tokenManager.getAuthHeader());
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error fetching config in verify session:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error fetching config in verify session:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const userOrgRole = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId),
|
||||
eq(userOrgs.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return userOrgRole.length > 0 ? userOrgRole[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if role has access to resource
|
||||
*/
|
||||
export async function getRoleResourceAccess(resourceId: number, roleId: number) {
|
||||
if (config.isManagedMode()) {
|
||||
try {
|
||||
const response = await axios.get(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/role/${roleId}/resource/${resourceId}/access`, await tokenManager.getAuthHeader());
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error fetching config in verify session:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error fetching config in verify session:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const roleResourceAccess = await db
|
||||
.select()
|
||||
.from(roleResources)
|
||||
.where(
|
||||
and(
|
||||
eq(roleResources.resourceId, resourceId),
|
||||
eq(roleResources.roleId, roleId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return roleResourceAccess.length > 0 ? roleResourceAccess[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has direct access to resource
|
||||
*/
|
||||
export async function getUserResourceAccess(userId: string, resourceId: number) {
|
||||
if (config.isManagedMode()) {
|
||||
try {
|
||||
const response = await axios.get(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/resource/${resourceId}/access`, await tokenManager.getAuthHeader());
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error fetching config in verify session:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error fetching config in verify session:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const userResourceAccess = await db
|
||||
.select()
|
||||
.from(userResources)
|
||||
.where(
|
||||
and(
|
||||
eq(userResources.userId, userId),
|
||||
eq(userResources.resourceId, resourceId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return userResourceAccess.length > 0 ? userResourceAccess[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resource rules for a given resource
|
||||
*/
|
||||
export async function getResourceRules(resourceId: number): Promise<ResourceRule[]> {
|
||||
if (config.isManagedMode()) {
|
||||
try {
|
||||
const response = await axios.get(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/rules`, await tokenManager.getAuthHeader());
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error fetching config in verify session:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error fetching config in verify session:", error);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const rules = await db
|
||||
.select()
|
||||
.from(resourceRules)
|
||||
.where(eq(resourceRules.resourceId, resourceId));
|
||||
|
||||
return rules;
|
||||
}
|
||||
@@ -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(),
|
||||
@@ -130,7 +136,26 @@ export const exitNodes = sqliteTable("exitNodes", {
|
||||
publicKey: text("publicKey").notNull(),
|
||||
listenPort: integer("listenPort").notNull(),
|
||||
reachableAt: text("reachableAt"), // this is the internal address of the gerbil http server for command control
|
||||
maxConnections: integer("maxConnections")
|
||||
maxConnections: integer("maxConnections"),
|
||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||
lastPing: integer("lastPing"),
|
||||
type: text("type").default("gerbil") // gerbil, remoteExitNode
|
||||
});
|
||||
|
||||
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", {
|
||||
@@ -154,6 +179,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 +188,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 +211,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 +245,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 +259,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 +712,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>;
|
||||
|
||||
@@ -6,6 +6,11 @@ import logger from "@server/logger";
|
||||
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||
|
||||
function createEmailClient() {
|
||||
if (config.isManagedMode()) {
|
||||
// LETS NOT WORRY ABOUT EMAILS IN HYBRID
|
||||
return;
|
||||
}
|
||||
|
||||
const emailConfig = config.getRawConfig().email;
|
||||
if (!emailConfig) {
|
||||
logger.warn(
|
||||
@@ -18,10 +23,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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
151
server/hybridServer.ts
Normal file
151
server/hybridServer.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import logger from "@server/logger";
|
||||
import config from "@server/lib/config";
|
||||
import { createWebSocketClient } from "./routers/ws/client";
|
||||
import { addPeer, deletePeer } from "./routers/gerbil/peers";
|
||||
import { db, exitNodes } from "./db";
|
||||
import { TraefikConfigManager } from "./lib/traefikConfig";
|
||||
import { tokenManager } from "./lib/tokenManager";
|
||||
import { APP_VERSION } from "./lib/consts";
|
||||
import axios from "axios";
|
||||
|
||||
export async function createHybridClientServer() {
|
||||
logger.info("Starting hybrid client server...");
|
||||
|
||||
// Start the token manager
|
||||
await tokenManager.start();
|
||||
|
||||
const token = await tokenManager.getToken();
|
||||
|
||||
const monitor = new TraefikConfigManager();
|
||||
|
||||
await monitor.start();
|
||||
|
||||
// Create client
|
||||
const client = createWebSocketClient(
|
||||
token,
|
||||
config.getRawConfig().managed!.endpoint!,
|
||||
{
|
||||
reconnectInterval: 5000,
|
||||
pingInterval: 30000,
|
||||
pingTimeout: 10000
|
||||
}
|
||||
);
|
||||
|
||||
// Register message handlers
|
||||
client.registerHandler("remoteExitNode/peers/add", async (message) => {
|
||||
const { publicKey, allowedIps } = message.data;
|
||||
|
||||
// TODO: we are getting the exit node twice here
|
||||
// NOTE: there should only be one gerbil registered so...
|
||||
const [exitNode] = await db.select().from(exitNodes).limit(1);
|
||||
await addPeer(exitNode.exitNodeId, {
|
||||
publicKey: publicKey,
|
||||
allowedIps: allowedIps || []
|
||||
});
|
||||
});
|
||||
|
||||
client.registerHandler("remoteExitNode/peers/remove", async (message) => {
|
||||
const { publicKey } = message.data;
|
||||
|
||||
// TODO: we are getting the exit node twice here
|
||||
// NOTE: there should only be one gerbil registered so...
|
||||
const [exitNode] = await db.select().from(exitNodes).limit(1);
|
||||
await deletePeer(exitNode.exitNodeId, publicKey);
|
||||
});
|
||||
|
||||
// /update-proxy-mapping
|
||||
client.registerHandler("remoteExitNode/update-proxy-mapping", async (message) => {
|
||||
try {
|
||||
const [exitNode] = await db.select().from(exitNodes).limit(1);
|
||||
if (!exitNode) {
|
||||
logger.error("No exit node found for proxy mapping update");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.post(`${exitNode.endpoint}/update-proxy-mapping`, message.data);
|
||||
logger.info(`Successfully updated proxy mapping: ${response.status}`);
|
||||
} catch (error) {
|
||||
// pull data out of the axios error to log
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error updating proxy mapping:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error updating proxy mapping:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// /update-destinations
|
||||
client.registerHandler("remoteExitNode/update-destinations", async (message) => {
|
||||
try {
|
||||
const [exitNode] = await db.select().from(exitNodes).limit(1);
|
||||
if (!exitNode) {
|
||||
logger.error("No exit node found for destinations update");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.post(`${exitNode.endpoint}/update-destinations`, message.data);
|
||||
logger.info(`Successfully updated destinations: ${response.status}`);
|
||||
} catch (error) {
|
||||
// pull data out of the axios error to log
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error updating destinations:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error updating destinations:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.registerHandler("remoteExitNode/traefik/reload", async (message) => {
|
||||
await monitor.HandleTraefikConfig();
|
||||
});
|
||||
|
||||
// Listen to connection events
|
||||
client.on("connect", () => {
|
||||
logger.info("Connected to WebSocket server");
|
||||
client.sendMessage("remoteExitNode/register", {
|
||||
remoteExitNodeVersion: APP_VERSION
|
||||
});
|
||||
});
|
||||
|
||||
client.on("disconnect", () => {
|
||||
logger.info("Disconnected from WebSocket server");
|
||||
});
|
||||
|
||||
client.on("message", (message) => {
|
||||
logger.info(
|
||||
`Received message: ${message.type} ${JSON.stringify(message.data)}`
|
||||
);
|
||||
});
|
||||
|
||||
// Connect to the server
|
||||
try {
|
||||
await client.connect();
|
||||
logger.info("Connection initiated");
|
||||
} catch (error) {
|
||||
logger.error("Failed to connect:", error);
|
||||
}
|
||||
|
||||
// Store the ping interval stop function for cleanup if needed
|
||||
const stopPingInterval = client.sendMessageInterval(
|
||||
"remoteExitNode/ping",
|
||||
{ timestamp: Date.now() / 1000 },
|
||||
60000
|
||||
); // send every minute
|
||||
|
||||
// Return client and cleanup function for potential use
|
||||
return { client, stopPingInterval };
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
#! /usr/bin/env node
|
||||
import "./extendZod.ts";
|
||||
|
||||
import { runSetupFunctions } from "./setup";
|
||||
@@ -6,16 +7,35 @@ import { createNextServer } from "./nextServer";
|
||||
import { createInternalServer } from "./internalServer";
|
||||
import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "@server/db";
|
||||
import { createIntegrationApiServer } from "./integrationApiServer";
|
||||
import { createHybridClientServer } from "./hybridServer";
|
||||
import config from "@server/lib/config";
|
||||
import { setHostMeta } from "@server/lib/hostMeta";
|
||||
import { initTelemetryClient } from "./lib/telemetry.js";
|
||||
import { TraefikConfigManager } from "./lib/traefikConfig.js";
|
||||
|
||||
async function startServers() {
|
||||
await setHostMeta();
|
||||
|
||||
await config.initServer();
|
||||
await runSetupFunctions();
|
||||
|
||||
initTelemetryClient();
|
||||
|
||||
// Start all servers
|
||||
const apiServer = createApiServer();
|
||||
const internalServer = createInternalServer();
|
||||
const nextServer = await createNextServer();
|
||||
|
||||
let hybridClientServer;
|
||||
let nextServer;
|
||||
if (config.isManagedMode()) {
|
||||
hybridClientServer = await createHybridClientServer();
|
||||
} else {
|
||||
nextServer = await createNextServer();
|
||||
if (config.getRawConfig().traefik.file_mode) {
|
||||
const monitor = new TraefikConfigManager();
|
||||
await monitor.start();
|
||||
}
|
||||
}
|
||||
|
||||
let integrationServer;
|
||||
if (config.getRawConfig().flags?.enable_integration_api) {
|
||||
@@ -26,7 +46,8 @@ async function startServers() {
|
||||
apiServer,
|
||||
nextServer,
|
||||
internalServer,
|
||||
integrationServer
|
||||
integrationServer,
|
||||
hybridClientServer
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ||
|
||||
@@ -102,16 +96,18 @@ export class Config {
|
||||
if (!this.rawConfig) {
|
||||
throw new Error("Config not loaded. Call load() first.");
|
||||
}
|
||||
license.setServerSecret(this.rawConfig.server.secret);
|
||||
if (this.rawConfig.managed) {
|
||||
// LETS NOT WORRY ABOUT THE SERVER SECRET WHEN MANAGED
|
||||
return;
|
||||
}
|
||||
license.setServerSecret(this.rawConfig.server.secret!);
|
||||
|
||||
await this.checkKeyStatus();
|
||||
}
|
||||
|
||||
private async checkKeyStatus() {
|
||||
const licenseStatus = await license.check();
|
||||
if (
|
||||
!licenseStatus.isHostLicensed
|
||||
) {
|
||||
if (!licenseStatus.isHostLicensed) {
|
||||
this.checkSupporterKey();
|
||||
}
|
||||
}
|
||||
@@ -153,6 +149,10 @@ export class Config {
|
||||
return false;
|
||||
}
|
||||
|
||||
public isManagedMode() {
|
||||
return typeof this.rawConfig?.managed === "object";
|
||||
}
|
||||
|
||||
public async checkSupporterKey() {
|
||||
const [key] = await db.select().from(supporterKey).limit(1);
|
||||
|
||||
|
||||
@@ -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.9.0";
|
||||
|
||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||
export const __DIRNAME = path.dirname(__FILENAME);
|
||||
|
||||
86
server/lib/exitNodeComms.ts
Normal file
86
server/lib/exitNodeComms.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import axios from "axios";
|
||||
import logger from "@server/logger";
|
||||
import { ExitNode } from "@server/db";
|
||||
|
||||
interface ExitNodeRequest {
|
||||
remoteType: string;
|
||||
localPath: string;
|
||||
method?: "POST" | "DELETE" | "GET" | "PUT";
|
||||
data?: any;
|
||||
queryParams?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to an exit node, handling both remote and local exit nodes
|
||||
* @param exitNode The exit node to send the request to
|
||||
* @param request The request configuration
|
||||
* @returns Promise<any> Response data for local nodes, undefined for remote nodes
|
||||
*/
|
||||
export async function sendToExitNode(
|
||||
exitNode: ExitNode,
|
||||
request: ExitNodeRequest
|
||||
): Promise<any> {
|
||||
if (!exitNode.reachableAt) {
|
||||
throw new Error(
|
||||
`Exit node with ID ${exitNode.exitNodeId} is not reachable`
|
||||
);
|
||||
}
|
||||
|
||||
// Handle local exit node with HTTP API
|
||||
const method = request.method || "POST";
|
||||
let url = `${exitNode.reachableAt}${request.localPath}`;
|
||||
|
||||
// Add query parameters if provided
|
||||
if (request.queryParams) {
|
||||
const params = new URLSearchParams(request.queryParams);
|
||||
url += `?${params.toString()}`;
|
||||
}
|
||||
|
||||
try {
|
||||
let response;
|
||||
|
||||
switch (method) {
|
||||
case "POST":
|
||||
response = await axios.post(url, request.data, {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "DELETE":
|
||||
response = await axios.delete(url);
|
||||
break;
|
||||
case "GET":
|
||||
response = await axios.get(url);
|
||||
break;
|
||||
case "PUT":
|
||||
response = await axios.put(url, request.data, {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported HTTP method: ${method}`);
|
||||
}
|
||||
|
||||
logger.info(`Exit node request successful:`, {
|
||||
method,
|
||||
url,
|
||||
status: response.data.status
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error(
|
||||
`Error making ${method} request (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}`
|
||||
);
|
||||
} else {
|
||||
logger.error(
|
||||
`Error making ${method} request for exit node at ${exitNode.reachableAt}: ${error}`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
59
server/lib/exitNodes/exitNodes.ts
Normal file
59
server/lib/exitNodes/exitNodes.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { db, exitNodes } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { ExitNodePingResult } from "@server/routers/newt";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export async function verifyExitNodeOrgAccess(
|
||||
exitNodeId: number,
|
||||
orgId: string
|
||||
) {
|
||||
const [exitNode] = await db
|
||||
.select()
|
||||
.from(exitNodes)
|
||||
.where(eq(exitNodes.exitNodeId, exitNodeId));
|
||||
|
||||
// For any other type, deny access
|
||||
return { hasAccess: true, exitNode };
|
||||
}
|
||||
|
||||
export async function listExitNodes(orgId: string, filterOnline = false) {
|
||||
// TODO: pick which nodes to send and ping better than just all of them that are not remote
|
||||
const allExitNodes = await db
|
||||
.select({
|
||||
exitNodeId: exitNodes.exitNodeId,
|
||||
name: exitNodes.name,
|
||||
address: exitNodes.address,
|
||||
endpoint: exitNodes.endpoint,
|
||||
publicKey: exitNodes.publicKey,
|
||||
listenPort: exitNodes.listenPort,
|
||||
reachableAt: exitNodes.reachableAt,
|
||||
maxConnections: exitNodes.maxConnections,
|
||||
online: exitNodes.online,
|
||||
lastPing: exitNodes.lastPing,
|
||||
type: exitNodes.type
|
||||
})
|
||||
.from(exitNodes);
|
||||
|
||||
// Filter the nodes. If there are NO remoteExitNodes then do nothing. If there are then remove all of the non-remoteExitNodes
|
||||
if (allExitNodes.length === 0) {
|
||||
logger.warn("No exit nodes found!");
|
||||
return [];
|
||||
}
|
||||
|
||||
return allExitNodes;
|
||||
}
|
||||
|
||||
export function selectBestExitNode(
|
||||
pingResults: ExitNodePingResult[]
|
||||
): ExitNodePingResult | null {
|
||||
if (!pingResults || pingResults.length === 0) {
|
||||
logger.warn("No ping results provided");
|
||||
return null;
|
||||
}
|
||||
|
||||
return pingResults[0];
|
||||
}
|
||||
|
||||
export async function checkExitNodeOrg(exitNodeId: number, orgId: string) {
|
||||
return false;
|
||||
}
|
||||
2
server/lib/exitNodes/index.ts
Normal file
2
server/lib/exitNodes/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./exitNodes";
|
||||
export * from "./shared";
|
||||
30
server/lib/exitNodes/shared.ts
Normal file
30
server/lib/exitNodes/shared.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { db, exitNodes } from "@server/db";
|
||||
import config from "@server/lib/config";
|
||||
import { findNextAvailableCidr } from "@server/lib/ip";
|
||||
|
||||
export async function getNextAvailableSubnet(): Promise<string> {
|
||||
// Get all existing subnets from routes table
|
||||
const existingAddresses = await db
|
||||
.select({
|
||||
address: exitNodes.address
|
||||
})
|
||||
.from(exitNodes);
|
||||
|
||||
const addresses = existingAddresses.map((a) => a.address);
|
||||
let subnet = findNextAvailableCidr(
|
||||
addresses,
|
||||
config.getRawConfig().gerbil.block_size,
|
||||
config.getRawConfig().gerbil.subnet_group
|
||||
);
|
||||
if (!subnet) {
|
||||
throw new Error("No available subnets remaining in space");
|
||||
}
|
||||
|
||||
// replace the last octet with 1
|
||||
subnet =
|
||||
subnet.split(".").slice(0, 3).join(".") +
|
||||
".1" +
|
||||
"/" +
|
||||
subnet.split("/")[1];
|
||||
return subnet;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./response";
|
||||
export { tokenManager, TokenManager } from "./tokenManager";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -17,16 +16,38 @@ export const configSchema = z
|
||||
dashboard_url: z
|
||||
.string()
|
||||
.url()
|
||||
.optional()
|
||||
.pipe(z.string().url())
|
||||
.transform((url) => url.toLowerCase()),
|
||||
.transform((url) => url.toLowerCase())
|
||||
.optional(),
|
||||
log_level: z
|
||||
.enum(["debug", "info", "warn", "error"])
|
||||
.optional()
|
||||
.default("info"),
|
||||
save_logs: z.boolean().optional().default(false),
|
||||
log_failed_attempts: z.boolean().optional().default(false)
|
||||
log_failed_attempts: z.boolean().optional().default(false),
|
||||
telemetry: z
|
||||
.object({
|
||||
anonymous_usage: z.boolean().optional().default(true)
|
||||
})
|
||||
.optional()
|
||||
.default({})
|
||||
}).optional().default({
|
||||
log_level: "info",
|
||||
save_logs: false,
|
||||
log_failed_attempts: false,
|
||||
telemetry: {
|
||||
anonymous_usage: true
|
||||
}
|
||||
}),
|
||||
managed: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
id: z.string().optional(),
|
||||
secret: z.string().optional(),
|
||||
endpoint: z.string().optional().default("https://pangolin.fossorial.io"),
|
||||
redirect_endpoint: z.string().optional()
|
||||
})
|
||||
.optional(),
|
||||
domains: z
|
||||
.record(
|
||||
z.string(),
|
||||
@@ -43,7 +64,7 @@ export const configSchema = z
|
||||
server: z.object({
|
||||
integration_port: portSchema
|
||||
.optional()
|
||||
.default(3003)
|
||||
.default(3004)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema.optional()),
|
||||
external_port: portSchema
|
||||
@@ -108,9 +129,25 @@ export const configSchema = z
|
||||
trust_proxy: z.number().int().gte(0).optional().default(1),
|
||||
secret: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform(getEnvOrYaml("SERVER_SECRET"))
|
||||
.pipe(z.string().min(8))
|
||||
.optional()
|
||||
}).optional().default({
|
||||
integration_port: 3003,
|
||||
external_port: 3000,
|
||||
internal_port: 3001,
|
||||
next_port: 3002,
|
||||
internal_hostname: "pangolin",
|
||||
session_cookie_name: "p_session_token",
|
||||
resource_access_token_param: "p_token",
|
||||
resource_access_token_headers: {
|
||||
id: "P-Access-Token-Id",
|
||||
token: "P-Access-Token"
|
||||
},
|
||||
resource_session_request_param: "resource_session_request_param",
|
||||
dashboard_session_length_hours: 720,
|
||||
resource_session_length_hours: 720,
|
||||
trust_proxy: 1
|
||||
}),
|
||||
postgres: z
|
||||
.object({
|
||||
@@ -130,7 +167,20 @@ export const configSchema = z
|
||||
https_entrypoint: z.string().optional().default("websecure"),
|
||||
additional_middlewares: z.array(z.string()).optional(),
|
||||
cert_resolver: z.string().optional().default("letsencrypt"),
|
||||
prefer_wildcard_cert: z.boolean().optional().default(false)
|
||||
prefer_wildcard_cert: z.boolean().optional().default(false),
|
||||
certificates_path: z.string().default("/var/certificates"),
|
||||
monitor_interval: z.number().default(5000),
|
||||
dynamic_cert_config_path: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("/var/dynamic/cert_config.yml"),
|
||||
dynamic_router_config_path: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("/var/dynamic/router_config.yml"),
|
||||
static_domains: z.array(z.string()).optional().default([]),
|
||||
site_types: z.array(z.string()).optional().default(["newt", "wireguard", "local"]),
|
||||
file_mode: z.boolean().optional().default(false)
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
@@ -213,7 +263,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()
|
||||
@@ -229,9 +282,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) => {
|
||||
@@ -239,6 +305,10 @@ export const configSchema = z
|
||||
if (data.flags?.disable_config_managed_domains) {
|
||||
return true;
|
||||
}
|
||||
// If hybrid is defined, domains are not required
|
||||
if (data.managed) {
|
||||
return true;
|
||||
}
|
||||
if (keys.length === 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -247,6 +317,32 @@ export const configSchema = z
|
||||
{
|
||||
message: "At least one domain must be defined"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// If hybrid is defined, server secret is not required
|
||||
if (data.managed) {
|
||||
return true;
|
||||
}
|
||||
// If hybrid is not defined, server secret must be defined
|
||||
return data.server?.secret !== undefined && data.server.secret.length > 0;
|
||||
},
|
||||
{
|
||||
message: "Server secret must be defined"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// If hybrid is defined, dashboard_url is not required
|
||||
if (data.managed) {
|
||||
return true;
|
||||
}
|
||||
// If hybrid is not defined, dashboard_url must be defined
|
||||
return data.app.dashboard_url !== undefined && data.app.dashboard_url.length > 0;
|
||||
},
|
||||
{
|
||||
message: "Dashboard URL must be defined"
|
||||
}
|
||||
);
|
||||
|
||||
export function readConfigFile() {
|
||||
@@ -274,7 +370,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"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
78
server/lib/remoteCertificates/certificates.ts
Normal file
78
server/lib/remoteCertificates/certificates.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import axios from "axios";
|
||||
import { tokenManager } from "../tokenManager";
|
||||
import logger from "@server/logger";
|
||||
import config from "../config";
|
||||
|
||||
/**
|
||||
* Get valid certificates for the specified domains
|
||||
*/
|
||||
export async function getValidCertificatesForDomainsHybrid(domains: Set<string>): Promise<
|
||||
Array<{
|
||||
id: number;
|
||||
domain: string;
|
||||
certFile: string | null;
|
||||
keyFile: string | null;
|
||||
expiresAt: Date | null;
|
||||
updatedAt?: Date | null;
|
||||
}>
|
||||
> {
|
||||
if (domains.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const domainArray = Array.from(domains);
|
||||
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/certificates/domains`,
|
||||
{
|
||||
params: {
|
||||
domains: domainArray
|
||||
},
|
||||
headers: (await tokenManager.getAuthHeader()).headers
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
logger.error(
|
||||
`Failed to fetch certificates for domains: ${response.status} ${response.statusText}`,
|
||||
{ responseData: response.data, domains: domainArray }
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
// logger.debug(
|
||||
// `Successfully retrieved ${response.data.data?.length || 0} certificates for ${domainArray.length} domains`
|
||||
// );
|
||||
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
// pull data out of the axios error to log
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error getting certificates:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error getting certificates:", error);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getValidCertificatesForDomains(domains: Set<string>): Promise<
|
||||
Array<{
|
||||
id: number;
|
||||
domain: string;
|
||||
certFile: string | null;
|
||||
keyFile: string | null;
|
||||
expiresAt: Date | null;
|
||||
updatedAt?: Date | null;
|
||||
}>
|
||||
> {
|
||||
return []; // stub
|
||||
}
|
||||
1
server/lib/remoteCertificates/index.ts
Normal file
1
server/lib/remoteCertificates/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./certificates";
|
||||
73
server/lib/remoteProxy.ts
Normal file
73
server/lib/remoteProxy.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { Router } from "express";
|
||||
import axios from "axios";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import config from "@server/lib/config";
|
||||
import { tokenManager } from "./tokenManager";
|
||||
|
||||
/**
|
||||
* Proxy function that forwards requests to the remote cloud server
|
||||
*/
|
||||
|
||||
export const proxyToRemote = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
endpoint: string
|
||||
): Promise<any> => {
|
||||
try {
|
||||
const remoteUrl = `${config.getRawConfig().managed?.endpoint?.replace(/\/$/, '')}/api/v1/${endpoint}`;
|
||||
|
||||
logger.debug(`Proxying request to remote server: ${remoteUrl}`);
|
||||
|
||||
// Forward the request to the remote server
|
||||
const response = await axios({
|
||||
method: req.method as any,
|
||||
url: remoteUrl,
|
||||
data: req.body,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(await tokenManager.getAuthHeader()).headers
|
||||
},
|
||||
params: req.query,
|
||||
timeout: 30000, // 30 second timeout
|
||||
validateStatus: () => true // Don't throw on non-2xx status codes
|
||||
});
|
||||
|
||||
logger.debug(`Proxy response: ${JSON.stringify(response.data)}`);
|
||||
|
||||
// Forward the response status and data
|
||||
return res.status(response.status).json(response.data);
|
||||
|
||||
} catch (error) {
|
||||
logger.error("Error proxying request to remote server:", error);
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.SERVICE_UNAVAILABLE,
|
||||
"Remote server is unavailable"
|
||||
)
|
||||
);
|
||||
}
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.REQUEST_TIMEOUT,
|
||||
"Request to remote server timed out"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error communicating with remote server"
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
304
server/lib/telemetry.ts
Normal file
304
server/lib/telemetry.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
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";
|
||||
import { build } from "@server/build";
|
||||
|
||||
class TelemetryClient {
|
||||
private client: PostHog | null = null;
|
||||
private enabled: boolean;
|
||||
private intervalId: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor() {
|
||||
const enabled = config.getRawConfig().app.telemetry.anonymous_usage;
|
||||
this.enabled = enabled;
|
||||
const dev = process.env.ENVIRONMENT !== "prod";
|
||||
|
||||
if (dev) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (build !== "oss") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.enabled) {
|
||||
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) {
|
||||
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;
|
||||
274
server/lib/tokenManager.ts
Normal file
274
server/lib/tokenManager.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import axios from "axios";
|
||||
import config from "@server/lib/config";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export interface TokenResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
data: {
|
||||
token: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Token Manager - Handles automatic token refresh for hybrid server authentication
|
||||
*
|
||||
* Usage throughout the application:
|
||||
* ```typescript
|
||||
* import { tokenManager } from "@server/lib/tokenManager";
|
||||
*
|
||||
* // Get the current valid token
|
||||
* const token = await tokenManager.getToken();
|
||||
*
|
||||
* // Force refresh if needed
|
||||
* await tokenManager.refreshToken();
|
||||
* ```
|
||||
*
|
||||
* The token manager automatically refreshes tokens every 24 hours by default
|
||||
* and is started once in the privateHybridServer.ts file.
|
||||
*/
|
||||
|
||||
export class TokenManager {
|
||||
private token: string | null = null;
|
||||
private refreshInterval: NodeJS.Timeout | null = null;
|
||||
private isRefreshing: boolean = false;
|
||||
private refreshIntervalMs: number;
|
||||
private retryInterval: NodeJS.Timeout | null = null;
|
||||
private retryIntervalMs: number;
|
||||
private tokenAvailablePromise: Promise<void> | null = null;
|
||||
private tokenAvailableResolve: (() => void) | null = null;
|
||||
|
||||
constructor(refreshIntervalMs: number = 24 * 60 * 60 * 1000, retryIntervalMs: number = 5000) {
|
||||
// Default to 24 hours for refresh, 5 seconds for retry
|
||||
this.refreshIntervalMs = refreshIntervalMs;
|
||||
this.retryIntervalMs = retryIntervalMs;
|
||||
this.setupTokenAvailablePromise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up promise that resolves when token becomes available
|
||||
*/
|
||||
private setupTokenAvailablePromise(): void {
|
||||
this.tokenAvailablePromise = new Promise((resolve) => {
|
||||
this.tokenAvailableResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the token available promise
|
||||
*/
|
||||
private resolveTokenAvailable(): void {
|
||||
if (this.tokenAvailableResolve) {
|
||||
this.tokenAvailableResolve();
|
||||
this.tokenAvailableResolve = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the token manager - gets initial token and sets up refresh interval
|
||||
* If initial token fetch fails, keeps retrying every few seconds until successful
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
logger.info("Starting token manager...");
|
||||
|
||||
try {
|
||||
await this.refreshToken();
|
||||
this.setupRefreshInterval();
|
||||
this.resolveTokenAvailable();
|
||||
logger.info("Token manager started successfully");
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to get initial token, will retry in ${this.retryIntervalMs / 1000} seconds:`, error);
|
||||
this.setupRetryInterval();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up retry interval for initial token acquisition
|
||||
*/
|
||||
private setupRetryInterval(): void {
|
||||
if (this.retryInterval) {
|
||||
clearInterval(this.retryInterval);
|
||||
}
|
||||
|
||||
this.retryInterval = setInterval(async () => {
|
||||
try {
|
||||
logger.debug("Retrying initial token acquisition");
|
||||
await this.refreshToken();
|
||||
this.setupRefreshInterval();
|
||||
this.clearRetryInterval();
|
||||
this.resolveTokenAvailable();
|
||||
logger.info("Token manager started successfully after retry");
|
||||
} catch (error) {
|
||||
logger.debug("Token acquisition retry failed, will try again");
|
||||
}
|
||||
}, this.retryIntervalMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear retry interval
|
||||
*/
|
||||
private clearRetryInterval(): void {
|
||||
if (this.retryInterval) {
|
||||
clearInterval(this.retryInterval);
|
||||
this.retryInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the token manager and clear all intervals
|
||||
*/
|
||||
stop(): void {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
this.refreshInterval = null;
|
||||
}
|
||||
this.clearRetryInterval();
|
||||
logger.info("Token manager stopped");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current valid token
|
||||
*/
|
||||
|
||||
// TODO: WE SHOULD NOT BE GETTING A TOKEN EVERY TIME WE REQUEST IT
|
||||
async getToken(): Promise<string> {
|
||||
// If we don't have a token yet, wait for it to become available
|
||||
if (!this.token && this.tokenAvailablePromise) {
|
||||
await this.tokenAvailablePromise;
|
||||
}
|
||||
|
||||
if (!this.token) {
|
||||
if (this.isRefreshing) {
|
||||
// Wait for current refresh to complete
|
||||
await this.waitForRefresh();
|
||||
} else {
|
||||
throw new Error("No valid token available");
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.token) {
|
||||
throw new Error("No valid token available");
|
||||
}
|
||||
|
||||
return this.token;
|
||||
}
|
||||
|
||||
async getAuthHeader() {
|
||||
return {
|
||||
headers: {
|
||||
Authorization: `Bearer ${await this.getToken()}`,
|
||||
"X-CSRF-Token": "x-csrf-protection",
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Force refresh the token
|
||||
*/
|
||||
async refreshToken(): Promise<void> {
|
||||
if (this.isRefreshing) {
|
||||
await this.waitForRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
|
||||
try {
|
||||
const hybridConfig = config.getRawConfig().managed;
|
||||
|
||||
if (
|
||||
!hybridConfig?.id ||
|
||||
!hybridConfig?.secret ||
|
||||
!hybridConfig?.endpoint
|
||||
) {
|
||||
throw new Error("Hybrid configuration is not defined");
|
||||
}
|
||||
|
||||
const tokenEndpoint = `${hybridConfig.endpoint}/api/v1/auth/remoteExitNode/get-token`;
|
||||
|
||||
const tokenData = {
|
||||
remoteExitNodeId: hybridConfig.id,
|
||||
secret: hybridConfig.secret
|
||||
};
|
||||
|
||||
logger.debug("Requesting new token from server");
|
||||
|
||||
const response = await axios.post<TokenResponse>(
|
||||
tokenEndpoint,
|
||||
tokenData,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": "x-csrf-protection"
|
||||
},
|
||||
timeout: 10000 // 10 second timeout
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(
|
||||
`Failed to get token: ${response.data.message}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.data.data.token) {
|
||||
throw new Error("Received empty token from server");
|
||||
}
|
||||
|
||||
this.token = response.data.data.token;
|
||||
logger.debug("Token refreshed successfully");
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error updating proxy mapping:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error updating proxy mapping:", error);
|
||||
}
|
||||
|
||||
throw new Error("Failed to refresh token");
|
||||
} finally {
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up automatic token refresh interval
|
||||
*/
|
||||
private setupRefreshInterval(): void {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
}
|
||||
|
||||
this.refreshInterval = setInterval(async () => {
|
||||
try {
|
||||
logger.debug("Auto-refreshing token");
|
||||
await this.refreshToken();
|
||||
} catch (error) {
|
||||
logger.error("Failed to auto-refresh token:", error);
|
||||
}
|
||||
}, this.refreshIntervalMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for current refresh operation to complete
|
||||
*/
|
||||
private async waitForRefresh(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const checkInterval = setInterval(() => {
|
||||
if (!this.isRefreshing) {
|
||||
clearInterval(checkInterval);
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance for use throughout the application
|
||||
export const tokenManager = new TokenManager();
|
||||
907
server/lib/traefikConfig.ts
Normal file
907
server/lib/traefikConfig.ts
Normal file
@@ -0,0 +1,907 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import config from "@server/lib/config";
|
||||
import logger from "@server/logger";
|
||||
import * as yaml from "js-yaml";
|
||||
import axios from "axios";
|
||||
import { db, exitNodes } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { tokenManager } from "./tokenManager";
|
||||
import {
|
||||
getCurrentExitNodeId,
|
||||
getTraefikConfig
|
||||
} from "@server/routers/traefik";
|
||||
import {
|
||||
getValidCertificatesForDomains,
|
||||
getValidCertificatesForDomainsHybrid
|
||||
} from "./remoteCertificates";
|
||||
|
||||
export class TraefikConfigManager {
|
||||
private intervalId: NodeJS.Timeout | null = null;
|
||||
private isRunning = false;
|
||||
private activeDomains = new Set<string>();
|
||||
private timeoutId: NodeJS.Timeout | null = null;
|
||||
private lastCertificateFetch: Date | null = null;
|
||||
private lastKnownDomains = new Set<string>();
|
||||
private lastLocalCertificateState = new Map<
|
||||
string,
|
||||
{
|
||||
exists: boolean;
|
||||
lastModified: Date | null;
|
||||
expiresAt: Date | null;
|
||||
}
|
||||
>();
|
||||
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Start monitoring certificates
|
||||
*/
|
||||
private scheduleNextExecution(): void {
|
||||
const intervalMs = config.getRawConfig().traefik.monitor_interval;
|
||||
const now = Date.now();
|
||||
const nextExecution = Math.ceil(now / intervalMs) * intervalMs;
|
||||
const delay = nextExecution - now;
|
||||
|
||||
this.timeoutId = setTimeout(async () => {
|
||||
try {
|
||||
await this.HandleTraefikConfig();
|
||||
} catch (error) {
|
||||
logger.error("Error during certificate monitoring:", error);
|
||||
}
|
||||
|
||||
if (this.isRunning) {
|
||||
this.scheduleNextExecution(); // Schedule the next execution
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
logger.info("Certificate monitor is already running");
|
||||
return;
|
||||
}
|
||||
this.isRunning = true;
|
||||
logger.info(`Starting certificate monitor for exit node`);
|
||||
|
||||
// Ensure certificates directory exists
|
||||
await this.ensureDirectoryExists(
|
||||
config.getRawConfig().traefik.certificates_path
|
||||
);
|
||||
|
||||
// Initialize local certificate state
|
||||
this.lastLocalCertificateState = await this.scanLocalCertificateState();
|
||||
logger.info(
|
||||
`Found ${this.lastLocalCertificateState.size} existing certificate directories`
|
||||
);
|
||||
|
||||
// Run initial check
|
||||
await this.HandleTraefikConfig();
|
||||
|
||||
// Start synchronized scheduling
|
||||
this.scheduleNextExecution();
|
||||
|
||||
logger.info(
|
||||
`Certificate monitor started with synchronized ${
|
||||
config.getRawConfig().traefik.monitor_interval
|
||||
}ms interval`
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Stop monitoring certificates
|
||||
*/
|
||||
stop(): void {
|
||||
if (!this.isRunning) {
|
||||
logger.info("Certificate monitor is not running");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
|
||||
this.isRunning = false;
|
||||
logger.info("Certificate monitor stopped");
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan local certificate directories to build current state
|
||||
*/
|
||||
private async scanLocalCertificateState(): Promise<
|
||||
Map<
|
||||
string,
|
||||
{
|
||||
exists: boolean;
|
||||
lastModified: Date | null;
|
||||
expiresAt: Date | null;
|
||||
}
|
||||
>
|
||||
> {
|
||||
const state = new Map();
|
||||
const certsPath = config.getRawConfig().traefik.certificates_path;
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(certsPath)) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const certDirs = fs.readdirSync(certsPath, { withFileTypes: true });
|
||||
|
||||
for (const dirent of certDirs) {
|
||||
if (!dirent.isDirectory()) continue;
|
||||
|
||||
const domain = dirent.name;
|
||||
const domainDir = path.join(certsPath, domain);
|
||||
const certPath = path.join(domainDir, "cert.pem");
|
||||
const keyPath = path.join(domainDir, "key.pem");
|
||||
const lastUpdatePath = path.join(domainDir, ".last_update");
|
||||
|
||||
const certExists = await this.fileExists(certPath);
|
||||
const keyExists = await this.fileExists(keyPath);
|
||||
const lastUpdateExists = await this.fileExists(lastUpdatePath);
|
||||
|
||||
let lastModified: Date | null = null;
|
||||
const expiresAt: Date | null = null;
|
||||
|
||||
if (lastUpdateExists) {
|
||||
try {
|
||||
const lastUpdateStr = fs
|
||||
.readFileSync(lastUpdatePath, "utf8")
|
||||
.trim();
|
||||
lastModified = new Date(lastUpdateStr);
|
||||
} catch {
|
||||
// If we can't read the last update, fall back to file stats
|
||||
try {
|
||||
const stats = fs.statSync(certPath);
|
||||
lastModified = stats.mtime;
|
||||
} catch {
|
||||
lastModified = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.set(domain, {
|
||||
exists: certExists && keyExists,
|
||||
lastModified,
|
||||
expiresAt
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error scanning local certificate state:", error);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we need to fetch certificates from remote
|
||||
*/
|
||||
private shouldFetchCertificates(currentDomains: Set<string>): boolean {
|
||||
// Always fetch on first run
|
||||
if (!this.lastCertificateFetch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fetch if it's been more than 24 hours (for renewals)
|
||||
const dayInMs = 24 * 60 * 60 * 1000;
|
||||
const timeSinceLastFetch =
|
||||
Date.now() - this.lastCertificateFetch.getTime();
|
||||
if (timeSinceLastFetch > dayInMs) {
|
||||
logger.info("Fetching certificates due to 24-hour renewal check");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fetch if domains have changed
|
||||
if (
|
||||
this.lastKnownDomains.size !== currentDomains.size ||
|
||||
!Array.from(this.lastKnownDomains).every((domain) =>
|
||||
currentDomains.has(domain)
|
||||
)
|
||||
) {
|
||||
logger.info("Fetching certificates due to domain changes");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if any local certificates are missing or appear to be outdated
|
||||
for (const domain of currentDomains) {
|
||||
const localState = this.lastLocalCertificateState.get(domain);
|
||||
if (!localState || !localState.exists) {
|
||||
logger.info(
|
||||
`Fetching certificates due to missing local cert for ${domain}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if certificate is expiring soon (within 30 days)
|
||||
if (localState.expiresAt) {
|
||||
const daysUntilExpiry =
|
||||
(localState.expiresAt.getTime() - Date.now()) /
|
||||
(1000 * 60 * 60 * 24);
|
||||
if (daysUntilExpiry < 30) {
|
||||
logger.info(
|
||||
`Fetching certificates due to upcoming expiry for ${domain} (${Math.round(daysUntilExpiry)} days remaining)`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main monitoring logic
|
||||
*/
|
||||
lastActiveDomains: Set<string> = new Set();
|
||||
public async HandleTraefikConfig(): Promise<void> {
|
||||
try {
|
||||
// Get all active domains for this exit node via HTTP call
|
||||
const getTraefikConfig = await this.getTraefikConfig();
|
||||
|
||||
if (!getTraefikConfig) {
|
||||
logger.error(
|
||||
"Failed to fetch active domains from traefik config"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { domains, traefikConfig } = getTraefikConfig;
|
||||
|
||||
// Add static domains from config
|
||||
// const staticDomains = [config.getRawConfig().app.dashboard_url];
|
||||
// staticDomains.forEach((domain) => domains.add(domain));
|
||||
|
||||
// Log if domains changed
|
||||
if (
|
||||
this.lastActiveDomains.size !== domains.size ||
|
||||
!Array.from(this.lastActiveDomains).every((domain) =>
|
||||
domains.has(domain)
|
||||
)
|
||||
) {
|
||||
logger.info(
|
||||
`Active domains changed for exit node: ${Array.from(domains).join(", ")}`
|
||||
);
|
||||
this.lastActiveDomains = new Set(domains);
|
||||
}
|
||||
|
||||
// Scan current local certificate state
|
||||
this.lastLocalCertificateState =
|
||||
await this.scanLocalCertificateState();
|
||||
|
||||
// Only fetch certificates if needed (domain changes, missing certs, or daily renewal check)
|
||||
let validCertificates: Array<{
|
||||
id: number;
|
||||
domain: string;
|
||||
certFile: string | null;
|
||||
keyFile: string | null;
|
||||
expiresAt: Date | null;
|
||||
updatedAt?: Date | null;
|
||||
}> = [];
|
||||
|
||||
if (this.shouldFetchCertificates(domains)) {
|
||||
// Get valid certificates for active domains
|
||||
if (config.isManagedMode()) {
|
||||
validCertificates =
|
||||
await getValidCertificatesForDomainsHybrid(domains);
|
||||
} else {
|
||||
validCertificates =
|
||||
await getValidCertificatesForDomains(domains);
|
||||
}
|
||||
this.lastCertificateFetch = new Date();
|
||||
this.lastKnownDomains = new Set(domains);
|
||||
|
||||
logger.info(
|
||||
`Fetched ${validCertificates.length} certificates from remote`
|
||||
);
|
||||
|
||||
// Download and decrypt new certificates
|
||||
await this.processValidCertificates(validCertificates);
|
||||
} else {
|
||||
const timeSinceLastFetch = this.lastCertificateFetch
|
||||
? Math.round(
|
||||
(Date.now() - this.lastCertificateFetch.getTime()) /
|
||||
(1000 * 60)
|
||||
)
|
||||
: 0;
|
||||
|
||||
// logger.debug(
|
||||
// `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)`
|
||||
// );
|
||||
|
||||
// Still need to ensure config is up to date with existing certificates
|
||||
await this.updateDynamicConfigFromLocalCerts(domains);
|
||||
}
|
||||
|
||||
// Clean up certificates for domains no longer in use
|
||||
await this.cleanupUnusedCertificates(domains);
|
||||
|
||||
// wait 1 second for traefik to pick up the new certificates
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Write traefik config as YAML to a second dynamic config file if changed
|
||||
await this.writeTraefikDynamicConfig(traefikConfig);
|
||||
|
||||
// Send domains to SNI proxy
|
||||
try {
|
||||
let exitNode;
|
||||
if (config.getRawConfig().gerbil.exit_node_name) {
|
||||
const exitNodeName =
|
||||
config.getRawConfig().gerbil.exit_node_name!;
|
||||
[exitNode] = await db
|
||||
.select()
|
||||
.from(exitNodes)
|
||||
.where(eq(exitNodes.name, exitNodeName))
|
||||
.limit(1);
|
||||
} else {
|
||||
[exitNode] = await db.select().from(exitNodes).limit(1);
|
||||
}
|
||||
if (exitNode) {
|
||||
try {
|
||||
await axios.post(
|
||||
`${exitNode.reachableAt}/update-local-snis`,
|
||||
{ fullDomains: Array.from(domains) },
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
} catch (error) {
|
||||
// pull data out of the axios error to log
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error updating local SNI:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error updating local SNI:", error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.error(
|
||||
"No exit node found. Has gerbil registered yet?"
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Failed to post domains to SNI proxy:", err);
|
||||
}
|
||||
|
||||
// Update active domains tracking
|
||||
this.activeDomains = domains;
|
||||
} catch (error) {
|
||||
logger.error("Error in traefik config monitoring cycle:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all domains currently in use from traefik config API
|
||||
*/
|
||||
private async getTraefikConfig(): Promise<{
|
||||
domains: Set<string>;
|
||||
traefikConfig: any;
|
||||
} | null> {
|
||||
let traefikConfig;
|
||||
try {
|
||||
if (config.isManagedMode()) {
|
||||
const resp = await axios.get(
|
||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/traefik-config`,
|
||||
await tokenManager.getAuthHeader()
|
||||
);
|
||||
|
||||
if (resp.status !== 200) {
|
||||
logger.error(
|
||||
`Failed to fetch traefik config: ${resp.status} ${resp.statusText}`,
|
||||
{ responseData: resp.data }
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
traefikConfig = resp.data.data;
|
||||
} else {
|
||||
const currentExitNode = await getCurrentExitNodeId();
|
||||
traefikConfig = await getTraefikConfig(
|
||||
currentExitNode,
|
||||
config.getRawConfig().traefik.site_types
|
||||
);
|
||||
}
|
||||
|
||||
const domains = new Set<string>();
|
||||
|
||||
if (traefikConfig?.http?.routers) {
|
||||
for (const router of Object.values<any>(
|
||||
traefikConfig.http.routers
|
||||
)) {
|
||||
if (router.rule && typeof router.rule === "string") {
|
||||
// Match Host(`domain`)
|
||||
const match = router.rule.match(/Host\(`([^`]+)`\)/);
|
||||
if (match && match[1]) {
|
||||
domains.add(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// logger.debug(
|
||||
// `Successfully retrieved traefik config: ${JSON.stringify(traefikConfig)}`
|
||||
// );
|
||||
|
||||
const badgerMiddlewareName = "badger";
|
||||
if (traefikConfig?.http?.middlewares) {
|
||||
traefikConfig.http.middlewares[badgerMiddlewareName] = {
|
||||
plugin: {
|
||||
[badgerMiddlewareName]: {
|
||||
apiBaseUrl: new URL(
|
||||
"/api/v1",
|
||||
`http://${
|
||||
config.getRawConfig().server
|
||||
.internal_hostname
|
||||
}:${config.getRawConfig().server.internal_port}`
|
||||
).href,
|
||||
userSessionCookieName:
|
||||
config.getRawConfig().server
|
||||
.session_cookie_name,
|
||||
|
||||
// deprecated
|
||||
accessTokenQueryParam:
|
||||
config.getRawConfig().server
|
||||
.resource_access_token_param,
|
||||
|
||||
resourceSessionRequestParam:
|
||||
config.getRawConfig().server
|
||||
.resource_session_request_param
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { domains, traefikConfig };
|
||||
} catch (error) {
|
||||
// pull data out of the axios error to log
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error("Error fetching traefik config:", {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
method: error.config?.method
|
||||
});
|
||||
} else {
|
||||
logger.error("Error fetching traefik config:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write traefik config as YAML to a second dynamic config file if changed
|
||||
*/
|
||||
private async writeTraefikDynamicConfig(traefikConfig: any): Promise<void> {
|
||||
const traefikDynamicConfigPath =
|
||||
config.getRawConfig().traefik.dynamic_router_config_path;
|
||||
let shouldWrite = false;
|
||||
let oldJson = "";
|
||||
if (fs.existsSync(traefikDynamicConfigPath)) {
|
||||
try {
|
||||
const oldContent = fs.readFileSync(
|
||||
traefikDynamicConfigPath,
|
||||
"utf8"
|
||||
);
|
||||
// Try to parse as YAML then JSON.stringify for comparison
|
||||
const oldObj = yaml.load(oldContent);
|
||||
oldJson = JSON.stringify(oldObj);
|
||||
} catch {
|
||||
oldJson = "";
|
||||
}
|
||||
}
|
||||
const newJson = JSON.stringify(traefikConfig);
|
||||
if (oldJson !== newJson) {
|
||||
shouldWrite = true;
|
||||
}
|
||||
if (shouldWrite) {
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
traefikDynamicConfigPath,
|
||||
yaml.dump(traefikConfig, { noRefs: true }),
|
||||
"utf8"
|
||||
);
|
||||
logger.info("Traefik dynamic config updated");
|
||||
} catch (err) {
|
||||
logger.error("Failed to write traefik dynamic config:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update dynamic config from existing local certificates without fetching from remote
|
||||
*/
|
||||
private async updateDynamicConfigFromLocalCerts(
|
||||
domains: Set<string>
|
||||
): Promise<void> {
|
||||
const dynamicConfigPath =
|
||||
config.getRawConfig().traefik.dynamic_cert_config_path;
|
||||
|
||||
// Load existing dynamic config if it exists, otherwise initialize
|
||||
let dynamicConfig: any = { tls: { certificates: [] } };
|
||||
if (fs.existsSync(dynamicConfigPath)) {
|
||||
try {
|
||||
const fileContent = fs.readFileSync(dynamicConfigPath, "utf8");
|
||||
dynamicConfig = yaml.load(fileContent) || dynamicConfig;
|
||||
if (!dynamicConfig.tls)
|
||||
dynamicConfig.tls = { certificates: [] };
|
||||
if (!Array.isArray(dynamicConfig.tls.certificates)) {
|
||||
dynamicConfig.tls.certificates = [];
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Failed to load existing dynamic config:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep a copy of the original config for comparison
|
||||
const originalConfigYaml = yaml.dump(dynamicConfig, { noRefs: true });
|
||||
|
||||
// Clear existing certificates and rebuild from local state
|
||||
dynamicConfig.tls.certificates = [];
|
||||
|
||||
for (const domain of domains) {
|
||||
const localState = this.lastLocalCertificateState.get(domain);
|
||||
if (localState && localState.exists) {
|
||||
const domainDir = path.join(
|
||||
config.getRawConfig().traefik.certificates_path,
|
||||
domain
|
||||
);
|
||||
const certPath = path.join(domainDir, "cert.pem");
|
||||
const keyPath = path.join(domainDir, "key.pem");
|
||||
|
||||
const certEntry = {
|
||||
certFile: certPath,
|
||||
keyFile: keyPath
|
||||
};
|
||||
dynamicConfig.tls.certificates.push(certEntry);
|
||||
}
|
||||
}
|
||||
|
||||
// Only write the config if it has changed
|
||||
const newConfigYaml = yaml.dump(dynamicConfig, { noRefs: true });
|
||||
if (newConfigYaml !== originalConfigYaml) {
|
||||
fs.writeFileSync(dynamicConfigPath, newConfigYaml, "utf8");
|
||||
logger.info("Dynamic cert config updated from local certificates");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process valid certificates - download and decrypt them
|
||||
*/
|
||||
private async processValidCertificates(
|
||||
validCertificates: Array<{
|
||||
id: number;
|
||||
domain: string;
|
||||
certFile: string | null;
|
||||
keyFile: string | null;
|
||||
expiresAt: Date | null;
|
||||
updatedAt?: Date | null;
|
||||
}>
|
||||
): Promise<void> {
|
||||
const dynamicConfigPath =
|
||||
config.getRawConfig().traefik.dynamic_cert_config_path;
|
||||
|
||||
// Load existing dynamic config if it exists, otherwise initialize
|
||||
let dynamicConfig: any = { tls: { certificates: [] } };
|
||||
if (fs.existsSync(dynamicConfigPath)) {
|
||||
try {
|
||||
const fileContent = fs.readFileSync(dynamicConfigPath, "utf8");
|
||||
dynamicConfig = yaml.load(fileContent) || dynamicConfig;
|
||||
if (!dynamicConfig.tls)
|
||||
dynamicConfig.tls = { certificates: [] };
|
||||
if (!Array.isArray(dynamicConfig.tls.certificates)) {
|
||||
dynamicConfig.tls.certificates = [];
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Failed to load existing dynamic config:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep a copy of the original config for comparison
|
||||
const originalConfigYaml = yaml.dump(dynamicConfig, { noRefs: true });
|
||||
|
||||
for (const cert of validCertificates) {
|
||||
try {
|
||||
if (!cert.certFile || !cert.keyFile) {
|
||||
logger.warn(
|
||||
`Certificate for domain ${cert.domain} is missing cert or key file`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const domainDir = path.join(
|
||||
config.getRawConfig().traefik.certificates_path,
|
||||
cert.domain
|
||||
);
|
||||
await this.ensureDirectoryExists(domainDir);
|
||||
|
||||
const certPath = path.join(domainDir, "cert.pem");
|
||||
const keyPath = path.join(domainDir, "key.pem");
|
||||
const lastUpdatePath = path.join(domainDir, ".last_update");
|
||||
|
||||
// Check if we need to update the certificate
|
||||
const shouldUpdate = await this.shouldUpdateCertificate(
|
||||
cert,
|
||||
certPath,
|
||||
keyPath,
|
||||
lastUpdatePath
|
||||
);
|
||||
|
||||
if (shouldUpdate) {
|
||||
logger.info(
|
||||
`Processing certificate for domain: ${cert.domain}`
|
||||
);
|
||||
|
||||
fs.writeFileSync(certPath, cert.certFile, "utf8");
|
||||
fs.writeFileSync(keyPath, cert.keyFile, "utf8");
|
||||
|
||||
// Set appropriate permissions (readable by owner only for key file)
|
||||
fs.chmodSync(certPath, 0o644);
|
||||
fs.chmodSync(keyPath, 0o600);
|
||||
|
||||
// Write/update .last_update file with current timestamp
|
||||
fs.writeFileSync(
|
||||
lastUpdatePath,
|
||||
new Date().toISOString(),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`Certificate updated for domain: ${cert.domain}`
|
||||
);
|
||||
|
||||
// Update local state tracking
|
||||
this.lastLocalCertificateState.set(cert.domain, {
|
||||
exists: true,
|
||||
lastModified: new Date(),
|
||||
expiresAt: cert.expiresAt
|
||||
});
|
||||
}
|
||||
|
||||
// Always ensure the config entry exists and is up to date
|
||||
const certEntry = {
|
||||
certFile: certPath,
|
||||
keyFile: keyPath
|
||||
};
|
||||
// Remove any existing entry for this cert/key path
|
||||
dynamicConfig.tls.certificates =
|
||||
dynamicConfig.tls.certificates.filter(
|
||||
(entry: any) =>
|
||||
entry.certFile !== certEntry.certFile ||
|
||||
entry.keyFile !== certEntry.keyFile
|
||||
);
|
||||
dynamicConfig.tls.certificates.push(certEntry);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error processing certificate for domain ${cert.domain}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Only write the config if it has changed
|
||||
const newConfigYaml = yaml.dump(dynamicConfig, { noRefs: true });
|
||||
if (newConfigYaml !== originalConfigYaml) {
|
||||
fs.writeFileSync(dynamicConfigPath, newConfigYaml, "utf8");
|
||||
logger.info("Dynamic cert config updated");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if certificate should be updated
|
||||
*/
|
||||
private async shouldUpdateCertificate(
|
||||
cert: {
|
||||
id: number;
|
||||
domain: string;
|
||||
expiresAt: Date | null;
|
||||
updatedAt?: Date | null;
|
||||
},
|
||||
certPath: string,
|
||||
keyPath: string,
|
||||
lastUpdatePath: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// If files don't exist, we need to create them
|
||||
const certExists = await this.fileExists(certPath);
|
||||
const keyExists = await this.fileExists(keyPath);
|
||||
const lastUpdateExists = await this.fileExists(lastUpdatePath);
|
||||
|
||||
if (!certExists || !keyExists || !lastUpdateExists) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Read last update time from .last_update file
|
||||
let lastUpdateTime: Date | null = null;
|
||||
try {
|
||||
const lastUpdateStr = fs
|
||||
.readFileSync(lastUpdatePath, "utf8")
|
||||
.trim();
|
||||
lastUpdateTime = new Date(lastUpdateStr);
|
||||
} catch {
|
||||
lastUpdateTime = null;
|
||||
}
|
||||
|
||||
// Use updatedAt from cert, fallback to expiresAt if not present
|
||||
const dbUpdateTime = cert.updatedAt ?? cert.expiresAt;
|
||||
|
||||
if (!dbUpdateTime) {
|
||||
// If no update time in DB, always update
|
||||
return true;
|
||||
}
|
||||
|
||||
// If DB updatedAt is newer than last update file, update
|
||||
if (!lastUpdateTime || dbUpdateTime > lastUpdateTime) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error checking certificate update status for ${cert.domain}:`,
|
||||
error
|
||||
);
|
||||
return true; // When in doubt, update
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up certificates for domains no longer in use
|
||||
*/
|
||||
private async cleanupUnusedCertificates(
|
||||
currentActiveDomains: Set<string>
|
||||
): Promise<void> {
|
||||
try {
|
||||
const certsPath = config.getRawConfig().traefik.certificates_path;
|
||||
const dynamicConfigPath =
|
||||
config.getRawConfig().traefik.dynamic_cert_config_path;
|
||||
|
||||
// Load existing dynamic config if it exists
|
||||
let dynamicConfig: any = { tls: { certificates: [] } };
|
||||
if (fs.existsSync(dynamicConfigPath)) {
|
||||
try {
|
||||
const fileContent = fs.readFileSync(
|
||||
dynamicConfigPath,
|
||||
"utf8"
|
||||
);
|
||||
dynamicConfig = yaml.load(fileContent) || dynamicConfig;
|
||||
if (!dynamicConfig.tls)
|
||||
dynamicConfig.tls = { certificates: [] };
|
||||
if (!Array.isArray(dynamicConfig.tls.certificates)) {
|
||||
dynamicConfig.tls.certificates = [];
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
"Failed to load existing dynamic config:",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const certDirs = fs.readdirSync(certsPath, {
|
||||
withFileTypes: true
|
||||
});
|
||||
|
||||
let configChanged = false;
|
||||
|
||||
for (const dirent of certDirs) {
|
||||
if (!dirent.isDirectory()) continue;
|
||||
|
||||
const dirName = dirent.name;
|
||||
// Only delete if NO current domain is exactly the same or ends with `.${dirName}`
|
||||
const shouldDelete = !Array.from(currentActiveDomains).some(
|
||||
(domain) =>
|
||||
domain === dirName || domain.endsWith(`.${dirName}`)
|
||||
);
|
||||
|
||||
if (shouldDelete) {
|
||||
const domainDir = path.join(certsPath, dirName);
|
||||
logger.info(
|
||||
`Cleaning up unused certificate directory: ${dirName}`
|
||||
);
|
||||
fs.rmSync(domainDir, { recursive: true, force: true });
|
||||
|
||||
// Remove from local state tracking
|
||||
this.lastLocalCertificateState.delete(dirName);
|
||||
|
||||
// Remove from dynamic config
|
||||
const certFilePath = path.join(
|
||||
domainDir,
|
||||
"cert.pem"
|
||||
);
|
||||
const keyFilePath = path.join(
|
||||
domainDir,
|
||||
"key.pem"
|
||||
);
|
||||
const before = dynamicConfig.tls.certificates.length;
|
||||
dynamicConfig.tls.certificates =
|
||||
dynamicConfig.tls.certificates.filter(
|
||||
(entry: any) =>
|
||||
entry.certFile !== certFilePath &&
|
||||
entry.keyFile !== keyFilePath
|
||||
);
|
||||
if (dynamicConfig.tls.certificates.length !== before) {
|
||||
configChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (configChanged) {
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
dynamicConfigPath,
|
||||
yaml.dump(dynamicConfig, { noRefs: true }),
|
||||
"utf8"
|
||||
);
|
||||
logger.info("Dynamic config updated after cleanup");
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
"Failed to update dynamic config after cleanup:",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error during certificate cleanup:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure directory exists
|
||||
*/
|
||||
private async ensureDirectoryExists(dirPath: string): Promise<void> {
|
||||
try {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
} catch (error) {
|
||||
logger.error(`Error creating directory ${dirPath}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file exists
|
||||
*/
|
||||
private async fileExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
fs.accessSync(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a certificate refresh regardless of cache state
|
||||
*/
|
||||
public async forceCertificateRefresh(): Promise<void> {
|
||||
logger.info("Forcing certificate refresh");
|
||||
this.lastCertificateFetch = null;
|
||||
this.lastKnownDomains = new Set();
|
||||
await this.HandleTraefikConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current status
|
||||
*/
|
||||
getStatus(): {
|
||||
isRunning: boolean;
|
||||
activeDomains: string[];
|
||||
monitorInterval: number;
|
||||
lastCertificateFetch: Date | null;
|
||||
localCertificateCount: number;
|
||||
} {
|
||||
return {
|
||||
isRunning: this.isRunning,
|
||||
activeDomains: Array.from(this.activeDomains),
|
||||
monitorInterval:
|
||||
config.getRawConfig().traefik.monitor_interval || 5000,
|
||||
lastCertificateFetch: this.lastCertificateFetch,
|
||||
localCertificateCount: this.lastLocalCertificateState.size
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -27,3 +27,4 @@ export * from "./verifyApiKeyAccess";
|
||||
export * from "./verifyDomainAccess";
|
||||
export * from "./verifyClientsEnabled";
|
||||
export * from "./verifyUserIsOrgOwner";
|
||||
export * from "./verifySiteResourceAccess";
|
||||
|
||||
@@ -10,3 +10,4 @@ export * from "./verifyApiKeySetResourceUsers";
|
||||
export * from "./verifyAccessTokenAccess";
|
||||
export * from "./verifyApiKeyIsRoot";
|
||||
export * from "./verifyApiKeyApiKeyAccess";
|
||||
export * from "./verifyApiKeyClientAccess";
|
||||
|
||||
@@ -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)
|
||||
|
||||
91
server/middlewares/integration/verifyApiKeyClientAccess.ts
Normal file
91
server/middlewares/integration/verifyApiKeyClientAccess.ts
Normal 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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
62
server/middlewares/verifySiteResourceAccess.ts
Normal file
62
server/middlewares/verifySiteResourceAccess.ts
Normal 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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user