mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-25 14:26:39 +00:00
Compare commits
288 Commits
site-targe
...
patch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0a612618e | ||
|
|
56ee68d9f3 | ||
|
|
e81fd3bb31 | ||
|
|
938ca29777 | ||
|
|
92ac2dbac2 | ||
|
|
d3e6decef9 | ||
|
|
579cd9d338 | ||
|
|
3292eafe4a | ||
|
|
042e2c1390 | ||
|
|
e6314bee35 | ||
|
|
cd79e77576 | ||
|
|
1f1c20d637 | ||
|
|
e87b3b1b54 | ||
|
|
a6f7b65625 | ||
|
|
722fa47132 | ||
|
|
f83e290b4c | ||
|
|
11b4047283 | ||
|
|
69b2032a86 | ||
|
|
636298569f | ||
|
|
ed8a282d35 | ||
|
|
3bd5e850e0 | ||
|
|
070f1f9159 | ||
|
|
195644cca5 | ||
|
|
8092c86ecd | ||
|
|
28f33702da | ||
|
|
570632b8be | ||
|
|
f2881e1b31 | ||
|
|
dad35e37ef | ||
|
|
39afabd60e | ||
|
|
dc7e14a34b | ||
|
|
1dca71a779 | ||
|
|
e9494efa8e | ||
|
|
8159a0f13d | ||
|
|
ee9101e738 | ||
|
|
b670e6e3dc | ||
|
|
5e5754fa62 | ||
|
|
5fcf76066f | ||
|
|
601645fa72 | ||
|
|
12765ad675 | ||
|
|
ad3383d23d | ||
|
|
7d5961cf50 | ||
|
|
864aa052f1 | ||
|
|
be16196058 | ||
|
|
8a62f12e8b | ||
|
|
78f464f6ca | ||
|
|
f37eda4739 | ||
|
|
4e106e9e5a | ||
|
|
ccf8e5e6f4 | ||
|
|
9455adf61f | ||
|
|
970ab9818a | ||
|
|
7848cf7141 | ||
|
|
8e5aa9c195 | ||
|
|
a03e9ba7dd | ||
|
|
9e646ba385 | ||
|
|
d9a4f20fe6 | ||
|
|
e659f0e75d | ||
|
|
8891d6239f | ||
|
|
e3bd3fb985 | ||
|
|
54764dfacd | ||
|
|
b156b5ff2d | ||
|
|
d8e547c9a0 | ||
|
|
a0b93377a4 | ||
|
|
e8a6efd079 | ||
|
|
18bb6caf8f | ||
|
|
bc335d15c0 | ||
|
|
2a0d440a34 | ||
|
|
b0500fac29 | ||
|
|
af16e6423a | ||
|
|
ff90471f0f | ||
|
|
bcc501c524 | ||
|
|
c4ef211a3e | ||
|
|
592c0eb7ab | ||
|
|
880a000865 | ||
|
|
a9571f6adf | ||
|
|
ca91f313bc | ||
|
|
76b9753916 | ||
|
|
cd8bbe28bf | ||
|
|
9550c11594 | ||
|
|
7ac21cad25 | ||
|
|
2008a3955a | ||
|
|
f1641c9f3e | ||
|
|
bec5bbd033 | ||
|
|
ae11f72e28 | ||
|
|
6b88cb3920 | ||
|
|
38772111e8 | ||
|
|
14f50c3e66 | ||
|
|
b89a2c9e49 | ||
|
|
2d2eda988c | ||
|
|
432033969b | ||
|
|
1fbf74e1f7 | ||
|
|
93648ff00b | ||
|
|
9513136610 | ||
|
|
43a2a39f8d | ||
|
|
218351de9a | ||
|
|
b92b922eee | ||
|
|
91be4937ee | ||
|
|
19b36a5fae | ||
|
|
bb9ee7dfd2 | ||
|
|
ac0351b525 | ||
|
|
405f5ad7cc | ||
|
|
8cc2712da3 | ||
|
|
c02ac8d1bf | ||
|
|
a1802add19 | ||
|
|
218a6642a2 | ||
|
|
21a83a5755 | ||
|
|
bec75e51f6 | ||
|
|
6c9b445be6 | ||
|
|
06b17fa941 | ||
|
|
e1d4c029e7 | ||
|
|
293fd70ccb | ||
|
|
4ee863db5a | ||
|
|
2717be0fed | ||
|
|
1f312e146f | ||
|
|
b91557ebb0 | ||
|
|
465380b5a3 | ||
|
|
60af901feb | ||
|
|
ea78a654ff | ||
|
|
f28b6ad0a5 | ||
|
|
a3bdab1318 | ||
|
|
f8c5d01e3c | ||
|
|
3ebe218b7f | ||
|
|
7d039ab729 | ||
|
|
b2b6c8c268 | ||
|
|
4950f25063 | ||
|
|
524d6b48d9 | ||
|
|
29fb5735e2 | ||
|
|
247fc85440 | ||
|
|
2b4302572c | ||
|
|
9b28780e62 | ||
|
|
8656f68008 | ||
|
|
15651b6919 | ||
|
|
78d3861382 | ||
|
|
72f19274cd | ||
|
|
adbcd1a2e0 | ||
|
|
5b7727fab4 | ||
|
|
9627dfa90c | ||
|
|
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 | ||
|
|
fb1481c69c | ||
|
|
9557f755a5 | ||
|
|
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 | ||
|
|
e043d0e654 | ||
|
|
21ce678e5b | ||
|
|
5c94887949 | ||
|
|
69a9bcb3da | ||
|
|
2fea091e1f | ||
|
|
24314a103f | ||
|
|
b56db41d0b | ||
|
|
825bff5d60 | ||
|
|
f9184cf489 | ||
|
|
2c96eb7851 | ||
|
|
04ecf41c5a | ||
|
|
6600de7320 | ||
|
|
f7b82f0a7a | ||
|
|
65bdb232f4 | ||
|
|
200e3af384 | ||
|
|
aabfa91f80 | ||
|
|
e5468a7391 | ||
|
|
d5a11edd0c | ||
|
|
fcc86b07ba | ||
|
|
50cf284273 | ||
|
|
aaddde0a9b | ||
|
|
ac87345b7a | ||
|
|
23079d9ac0 | ||
|
|
b573d63648 | ||
|
|
34d705a54e | ||
|
|
b638adedff | ||
|
|
285e24cdc7 | ||
|
|
396e643b06 | ||
|
|
dc50190dc3 | ||
|
|
2c8bf4f18c | ||
|
|
1f6379a7e6 | ||
|
|
ddd8eb1da0 | ||
|
|
3d8869066a | ||
|
|
880a123149 | ||
|
|
39e35bc1d6 | ||
|
|
f219f1e36b | ||
|
|
25ed3d65f8 | ||
|
|
30dbabd73d | ||
|
|
ea2e5bf486 | ||
|
|
b6c2f123e8 | ||
|
|
15f900317a | ||
|
|
22545cac8b |
4
.github/workflows/cicd.yml
vendored
4
.github/workflows/cicd.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: 1.24
|
go-version: 1.24
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/linting.yml
vendored
4
.github/workflows/linting.yml
vendored
@@ -18,10 +18,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/stale-bot.yml
vendored
2
.github/workflows/stale-bot.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v9
|
- uses: actions/stale@v10
|
||||||
with:
|
with:
|
||||||
days-before-stale: 14
|
days-before-stale: 14
|
||||||
days-before-close: 14
|
days-before-close: 14
|
||||||
|
|||||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -11,9 +11,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
|
|||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -26,6 +26,10 @@ next-env.d.ts
|
|||||||
migrations
|
migrations
|
||||||
tsconfig.tsbuildinfo
|
tsconfig.tsbuildinfo
|
||||||
config/config.yml
|
config/config.yml
|
||||||
|
config/postgres
|
||||||
|
config/postgres*
|
||||||
|
config/openapi.yaml
|
||||||
|
config/key
|
||||||
dist
|
dist
|
||||||
.dist
|
.dist
|
||||||
installer
|
installer
|
||||||
@@ -34,4 +38,9 @@ bin
|
|||||||
.secrets
|
.secrets
|
||||||
test_event.json
|
test_event.json
|
||||||
.idea/
|
.idea/
|
||||||
|
public/branding
|
||||||
server/db/index.ts
|
server/db/index.ts
|
||||||
|
server/build.ts
|
||||||
|
postgres/
|
||||||
|
dynamic/
|
||||||
|
certificates/
|
||||||
|
|||||||
@@ -2,17 +2,22 @@ FROM node:22-alpine AS builder
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
ARG BUILD=oss
|
||||||
|
ARG DATABASE=sqlite
|
||||||
|
|
||||||
# COPY package.json package-lock.json ./
|
# COPY package.json package-lock.json ./
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
COPY . .
|
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
|
RUN npm run build:cli
|
||||||
|
|
||||||
FROM node:22-alpine AS runner
|
FROM node:22-alpine AS runner
|
||||||
@@ -38,4 +43,4 @@ COPY server/db/names.json ./dist/names.json
|
|||||||
|
|
||||||
COPY public ./public
|
COPY public ./public
|
||||||
|
|
||||||
CMD ["npm", "run", "start:pg"]
|
CMD ["npm", "run", "start"]
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
FROM node:22-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:22-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>"; \
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest -f Dockerfile.sqlite --push .
|
docker buildx build --build-arg DATABASE=sqlite --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest --push .
|
||||||
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:$(tag) -f Dockerfile.sqlite --push .
|
docker buildx build --build-arg DATABASE=sqlite --platform linux/arm64,linux/amd64 -t fosrl/pangolin:$(tag) --push .
|
||||||
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-latest -f Dockerfile.pg --push .
|
docker buildx build --build-arg DATABASE=pg --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-latest --push .
|
||||||
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-$(tag) -f Dockerfile.pg --push .
|
docker buildx build --build-arg DATABASE=pg --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-$(tag) --push .
|
||||||
|
|
||||||
build-arm:
|
build-arm:
|
||||||
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
||||||
@@ -17,10 +17,10 @@ build-x86:
|
|||||||
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
|
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
|
||||||
|
|
||||||
build-sqlite:
|
build-sqlite:
|
||||||
docker build -t fosrl/pangolin:latest -f Dockerfile.sqlite .
|
docker build --build-arg DATABASE=sqlite -t fosrl/pangolin:latest .
|
||||||
|
|
||||||
build-pg:
|
build-pg:
|
||||||
docker build -t fosrl/pangolin:postgresql-latest -f Dockerfile.pg .
|
docker build --build-arg DATABASE=pg -t fosrl/pangolin:postgresql-latest .
|
||||||
|
|
||||||
test:
|
test:
|
||||||
docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest
|
docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -20,11 +20,11 @@ _Pangolin tunnels your services to the internet so you can access anything from
|
|||||||
Website
|
Website
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://docs.digpangolin.com/self-host/quick-install">
|
<a href="https://docs.digpangolin.com/self-host/quick-install-managed">
|
||||||
Install Guide
|
Quick Install Guide
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="mailto:numbat@fossorial.io">
|
<a href="mailto:contact@fossorial.io">
|
||||||
Contact Us
|
Contact Us
|
||||||
</a>
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
@@ -114,7 +114,7 @@ Easy to use with simple [pay as you go pricing](https://digpangolin.com/pricing)
|
|||||||
|
|
||||||
- Everything you get with self hosted Pangolin, but fully managed for you.
|
- Everything you get with self hosted Pangolin, but fully managed for you.
|
||||||
|
|
||||||
### Hybrid & High Availability
|
### Managed & High Availability
|
||||||
|
|
||||||
Managed control plane, your infrastructure
|
Managed control plane, your infrastructure
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ Managed control plane, your infrastructure
|
|||||||
- Traffic flows through your infra.
|
- Traffic flows through your infra.
|
||||||
- We coordinate failover between your nodes or to Cloud when things go bad.
|
- 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
|
### Full Enterprise On-Premises
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export const setAdminCredentials: CommandModule<{}, SetAdminCredentialsArgs> = {
|
|||||||
},
|
},
|
||||||
handler: async (argv: { email: string; password: string }) => {
|
handler: async (argv: { email: string; password: string }) => {
|
||||||
try {
|
try {
|
||||||
let { email, password } = argv;
|
const { password } = argv;
|
||||||
|
let { email } = argv;
|
||||||
email = email.trim().toLowerCase();
|
email = email.trim().toLowerCase();
|
||||||
|
|
||||||
const parsed = passwordSchema.safeParse(password);
|
const parsed = passwordSchema.safeParse(password);
|
||||||
|
|||||||
Binary file not shown.
46
config/traefik/dynamic_config.yml
Normal file
46
config/traefik/dynamic_config.yml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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}}`)"
|
||||||
|
service: next-service
|
||||||
|
priority: 10
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
|
||||||
|
# API router (handles /api/v1 paths)
|
||||||
|
api-router:
|
||||||
|
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
|
||||||
|
service: api-service
|
||||||
|
priority: 100
|
||||||
|
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:
|
command:
|
||||||
- --reachableAt=http://gerbil:3003
|
- --reachableAt=http://gerbil:3003
|
||||||
- --generateAndSaveKeyTo=/var/config/key
|
- --generateAndSaveKeyTo=/var/config/key
|
||||||
- --remoteConfig=http://pangolin:3001/api/v1/gerbil/get-config
|
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||||
- --reportBandwidthTo=http://pangolin:3001/api/v1/gerbil/receive-bandwidth
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./config/:/var/config
|
- ./config/:/var/config
|
||||||
cap_add:
|
cap_add:
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ services:
|
|||||||
POSTGRES_DB: postgres # Default database name
|
POSTGRES_DB: postgres # Default database name
|
||||||
POSTGRES_USER: postgres # Default user
|
POSTGRES_USER: postgres # Default user
|
||||||
POSTGRES_PASSWORD: password # Default password (change for production!)
|
POSTGRES_PASSWORD: password # Default password (change for production!)
|
||||||
|
volumes:
|
||||||
|
- ./config/postgres:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432" # Map host port 5432 to container port 5432
|
- "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
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ esbuild
|
|||||||
packagePath: getPackagePaths(),
|
packagePath: getPackagePaths(),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
sourcemap: true,
|
sourcemap: "external",
|
||||||
target: "node22",
|
target: "node22",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -37,15 +37,28 @@ type DynamicConfig struct {
|
|||||||
} `yaml:"http"`
|
} `yaml:"http"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigValues holds the extracted configuration values
|
// TraefikConfigValues holds the extracted configuration values
|
||||||
type ConfigValues struct {
|
type TraefikConfigValues struct {
|
||||||
DashboardDomain string
|
DashboardDomain string
|
||||||
LetsEncryptEmail string
|
LetsEncryptEmail string
|
||||||
BadgerVersion 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
|
// 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
|
// Read main config file
|
||||||
mainConfigData, err := os.ReadFile(mainConfigPath)
|
mainConfigData, err := os.ReadFile(mainConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -57,48 +70,33 @@ func ReadTraefikConfig(mainConfigPath, dynamicConfigPath string) (*ConfigValues,
|
|||||||
return nil, fmt.Errorf("error parsing main config file: %w", err)
|
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
|
// Extract values
|
||||||
values := &ConfigValues{
|
values := &TraefikConfigValues{
|
||||||
BadgerVersion: mainConfig.Experimental.Plugins.Badger.Version,
|
BadgerVersion: mainConfig.Experimental.Plugins.Badger.Version,
|
||||||
LetsEncryptEmail: mainConfig.CertificatesResolvers.LetsEncrypt.Acme.Email,
|
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
|
return values, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractDomainFromRule extracts the domain from a router rule
|
func ReadAppConfig(configPath string) (*AppConfigValues, error) {
|
||||||
func extractDomainFromRule(rule string) string {
|
// Read config file
|
||||||
// Look for the Host(`mydomain.com`) pattern
|
configData, err := os.ReadFile(configPath)
|
||||||
if start := findPattern(rule, "Host(`"); start != -1 {
|
if err != nil {
|
||||||
end := findPattern(rule[start:], "`)")
|
return nil, fmt.Errorf("error reading config file: %w", err)
|
||||||
if end != -1 {
|
|
||||||
return rule[start+6 : start+end]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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
|
// findPattern finds the start of a pattern in a string
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
# To see all available options, please visit the docs:
|
# To see all available options, please visit the docs:
|
||||||
# https://docs.digpangolin.com/self-host/dns-and-networking
|
# 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:
|
app:
|
||||||
dashboard_url: "https://{{.DashboardDomain}}"
|
dashboard_url: "https://{{.DashboardDomain}}"
|
||||||
log_level: "info"
|
log_level: "info"
|
||||||
|
telemetry:
|
||||||
|
anonymous_usage: true
|
||||||
|
|
||||||
domains:
|
domains:
|
||||||
domain1:
|
domain1:
|
||||||
@@ -17,11 +28,6 @@ server:
|
|||||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||||
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||||
credentials: false
|
credentials: false
|
||||||
|
|
||||||
gerbil:
|
|
||||||
start_port: 51820
|
|
||||||
base_endpoint: "{{.DashboardDomain}}"
|
|
||||||
|
|
||||||
{{if .EnableEmail}}
|
{{if .EnableEmail}}
|
||||||
email:
|
email:
|
||||||
smtp_host: "{{.EmailSMTPHost}}"
|
smtp_host: "{{.EmailSMTPHost}}"
|
||||||
@@ -30,9 +36,9 @@ email:
|
|||||||
smtp_pass: "{{.EmailSMTPPass}}"
|
smtp_pass: "{{.EmailSMTPPass}}"
|
||||||
no_reply: "{{.EmailNoReply}}"
|
no_reply: "{{.EmailNoReply}}"
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
flags:
|
flags:
|
||||||
require_email_verification: {{.EnableEmail}}
|
require_email_verification: {{.EnableEmail}}
|
||||||
disable_signup_without_invite: true
|
disable_signup_without_invite: true
|
||||||
disable_user_create_org: false
|
disable_user_create_org: false
|
||||||
allow_raw_resources: true
|
allow_raw_resources: true
|
||||||
|
{{end}}
|
||||||
@@ -6,6 +6,8 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
|
- pangolin-data:/var/certificates
|
||||||
|
- pangolin-data:/var/dynamic
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
||||||
interval: "10s"
|
interval: "10s"
|
||||||
@@ -22,8 +24,7 @@ services:
|
|||||||
command:
|
command:
|
||||||
- --reachableAt=http://gerbil:3003
|
- --reachableAt=http://gerbil:3003
|
||||||
- --generateAndSaveKeyTo=/var/config/key
|
- --generateAndSaveKeyTo=/var/config/key
|
||||||
- --remoteConfig=http://pangolin:3001/api/v1/gerbil/get-config
|
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||||
- --reportBandwidthTo=http://pangolin:3001/api/v1/gerbil/receive-bandwidth
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./config/:/var/config
|
- ./config/:/var/config
|
||||||
cap_add:
|
cap_add:
|
||||||
@@ -32,8 +33,8 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 51820:51820/udp
|
- 51820:51820/udp
|
||||||
- 21820:21820/udp
|
- 21820:21820/udp
|
||||||
- 443:443 # Port for traefik because of the network_mode
|
- 443:{{if .HybridMode}}8443{{else}}443{{end}}
|
||||||
- 80:80 # Port for traefik because of the network_mode
|
- 80:80
|
||||||
{{end}}
|
{{end}}
|
||||||
traefik:
|
traefik:
|
||||||
image: docker.io/traefik:v3.5
|
image: docker.io/traefik:v3.5
|
||||||
@@ -55,9 +56,15 @@ services:
|
|||||||
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
||||||
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
||||||
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
||||||
|
# Shared volume for certificates and dynamic config in file mode
|
||||||
|
- pangolin-data:/var/certificates:ro
|
||||||
|
- pangolin-data:/var/dynamic:ro
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
name: pangolin
|
name: pangolin
|
||||||
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pangolin-data:
|
||||||
|
|||||||
@@ -3,12 +3,17 @@ api:
|
|||||||
dashboard: true
|
dashboard: true
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
|
{{if not .HybridMode}}
|
||||||
http:
|
http:
|
||||||
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
||||||
pollInterval: "5s"
|
pollInterval: "5s"
|
||||||
file:
|
file:
|
||||||
filename: "/etc/traefik/dynamic_config.yml"
|
filename: "/etc/traefik/dynamic_config.yml"
|
||||||
|
{{else}}
|
||||||
|
file:
|
||||||
|
directory: "/var/dynamic"
|
||||||
|
watch: true
|
||||||
|
{{end}}
|
||||||
experimental:
|
experimental:
|
||||||
plugins:
|
plugins:
|
||||||
badger:
|
badger:
|
||||||
@@ -22,7 +27,7 @@ log:
|
|||||||
maxBackups: 3
|
maxBackups: 3
|
||||||
maxAge: 3
|
maxAge: 3
|
||||||
compress: true
|
compress: true
|
||||||
|
{{if not .HybridMode}}
|
||||||
certificatesResolvers:
|
certificatesResolvers:
|
||||||
letsencrypt:
|
letsencrypt:
|
||||||
acme:
|
acme:
|
||||||
@@ -31,18 +36,25 @@ certificatesResolvers:
|
|||||||
email: "{{.LetsEncryptEmail}}"
|
email: "{{.LetsEncryptEmail}}"
|
||||||
storage: "/letsencrypt/acme.json"
|
storage: "/letsencrypt/acme.json"
|
||||||
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||||
|
{{end}}
|
||||||
entryPoints:
|
entryPoints:
|
||||||
web:
|
web:
|
||||||
address: ":80"
|
address: ":80"
|
||||||
websecure:
|
websecure:
|
||||||
address: ":443"
|
address: ":443"
|
||||||
|
{{if .HybridMode}} proxyProtocol:
|
||||||
|
trustedIPs:
|
||||||
|
- 0.0.0.0/0
|
||||||
|
- ::1/128{{end}}
|
||||||
transport:
|
transport:
|
||||||
respondingTimeouts:
|
respondingTimeouts:
|
||||||
readTimeout: "30m"
|
readTimeout: "30m"
|
||||||
http:
|
{{if not .HybridMode}} http:
|
||||||
tls:
|
tls:
|
||||||
certResolver: "letsencrypt"
|
certResolver: "letsencrypt"{{end}}
|
||||||
|
|
||||||
serversTransport:
|
serversTransport:
|
||||||
insecureSkipVerify: true
|
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)
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@ module installer
|
|||||||
go 1.24
|
go 1.24
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/term v0.33.0
|
golang.org/x/term v0.34.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/sys v0.34.0 // indirect
|
require golang.org/x/sys v0.35.0 // indirect
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
799
install/main.go
799
install/main.go
@@ -10,17 +10,12 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"golang.org/x/term"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DO NOT EDIT THIS FUNCTION; IT MATCHED BY REGEX IN CICD
|
// DO NOT EDIT THIS FUNCTION; IT MATCHED BY REGEX IN CICD
|
||||||
@@ -52,6 +47,9 @@ type Config struct {
|
|||||||
TraefikBouncerKey string
|
TraefikBouncerKey string
|
||||||
DoCrowdsecInstall bool
|
DoCrowdsecInstall bool
|
||||||
Secret string
|
Secret string
|
||||||
|
HybridMode bool
|
||||||
|
HybridId string
|
||||||
|
HybridSecret string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SupportedContainer string
|
type SupportedContainer string
|
||||||
@@ -67,15 +65,9 @@ func main() {
|
|||||||
|
|
||||||
fmt.Println("Welcome to the Pangolin installer!")
|
fmt.Println("Welcome to the Pangolin installer!")
|
||||||
fmt.Println("This installer will help you set up Pangolin on your server.")
|
fmt.Println("This installer will help you set up Pangolin on your server.")
|
||||||
fmt.Println("")
|
fmt.Println("\nPlease make sure you have the following prerequisites:")
|
||||||
fmt.Println("Please make sure you have the following prerequisites:")
|
|
||||||
fmt.Println("- Open TCP ports 80 and 443 and UDP ports 51820 and 21820 on your VPS and firewall.")
|
fmt.Println("- Open TCP ports 80 and 443 and UDP ports 51820 and 21820 on your VPS and firewall.")
|
||||||
fmt.Println("- Point your domain to the VPS IP with A records.")
|
fmt.Println("\nLets get started!")
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println("https://docs.digpangolin.com/self-host/dns-and-networking")
|
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println("Lets get started!")
|
|
||||||
fmt.Println("")
|
|
||||||
|
|
||||||
if os.Geteuid() == 0 { // WE NEED TO BE SUDO TO CHECK THIS
|
if os.Geteuid() == 0 { // WE NEED TO BE SUDO TO CHECK THIS
|
||||||
for _, p := range []int{80, 443} {
|
for _, p := range []int{80, 443} {
|
||||||
@@ -89,6 +81,162 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
|
||||||
|
// check if there is already a config file
|
||||||
|
if _, err := os.Stat("config/config.yml"); err != nil {
|
||||||
|
config = collectUserInput(reader)
|
||||||
|
|
||||||
|
loadVersions(&config)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
moveFile("config/docker-compose.yml", "docker-compose.yml")
|
||||||
|
|
||||||
|
fmt.Println("\nConfiguration files created successfully!")
|
||||||
|
|
||||||
|
fmt.Println("\n=== Starting installation ===")
|
||||||
|
|
||||||
|
if readBool(reader, "Would you like to install and start the containers?", true) {
|
||||||
|
|
||||||
|
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 Pangolin!")
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error reading config: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|
||||||
|
// print the values and check if they are right
|
||||||
|
fmt.Println("Detected values:")
|
||||||
|
fmt.Printf("Dashboard Domain: %s\n", config.DashboardDomain)
|
||||||
|
fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail)
|
||||||
|
fmt.Printf("Badger Version: %s\n", config.BadgerVersion)
|
||||||
|
|
||||||
|
if !readBool(reader, "Are these values correct?", true) {
|
||||||
|
config = collectUserInput(reader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.DoCrowdsecInstall = true
|
||||||
|
installCrowdsec(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.HybridMode {
|
||||||
|
// Setup Token Section
|
||||||
|
fmt.Println("\n=== Setup Token ===")
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If containers weren't started or token wasn't found, show instructions
|
||||||
|
if !containersStarted {
|
||||||
|
showSetupTokenInstructions(config.InstallationContainerType, config.DashboardDomain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker")
|
||||||
|
|
||||||
chosenContainer := Docker
|
chosenContainer := Docker
|
||||||
@@ -152,200 +300,7 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
var config Config
|
return chosenContainer
|
||||||
config.InstallationContainerType = chosenContainer
|
|
||||||
|
|
||||||
// check if there is already a config file
|
|
||||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
|
||||||
config = collectUserInput(reader)
|
|
||||||
|
|
||||||
loadVersions(&config)
|
|
||||||
config.DoCrowdsecInstall = false
|
|
||||||
config.Secret = generateRandomSecretKey()
|
|
||||||
|
|
||||||
if err := createConfigFiles(config); err != nil {
|
|
||||||
fmt.Printf("Error creating config files: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
moveFile("config/docker-compose.yml", "docker-compose.yml")
|
|
||||||
|
|
||||||
if !isDockerInstalled() && runtime.GOOS == "linux" && chosenContainer == Docker {
|
|
||||||
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
|
||||||
installDocker()
|
|
||||||
// try to start docker service but ignore errors
|
|
||||||
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("\n=== Starting installation ===")
|
|
||||||
|
|
||||||
if (isDockerInstalled() && chosenContainer == Docker) ||
|
|
||||||
(isPodmanInstalled() && chosenContainer == Podman) {
|
|
||||||
if readBool(reader, "Would you like to install and start the containers?", true) {
|
|
||||||
if err := pullContainers(chosenContainer); err != nil {
|
|
||||||
fmt.Println("Error: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := startContainers(chosenContainer); err != nil {
|
|
||||||
fmt.Println("Error: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Println("Looks like you already installed, so I am going to do the setup...")
|
|
||||||
|
|
||||||
// Read existing config to get DashboardDomain
|
|
||||||
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Warning: Could not read existing config: %v\n", err)
|
|
||||||
fmt.Println("You may need to manually enter your domain information.")
|
|
||||||
config = collectUserInput(reader)
|
|
||||||
} else {
|
|
||||||
config.DashboardDomain = traefikConfig.DashboardDomain
|
|
||||||
config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail
|
|
||||||
config.BadgerVersion = traefikConfig.BadgerVersion
|
|
||||||
|
|
||||||
// Show detected values and allow user to confirm or re-enter
|
|
||||||
fmt.Println("Detected existing configuration:")
|
|
||||||
fmt.Printf("Dashboard Domain: %s\n", config.DashboardDomain)
|
|
||||||
fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail)
|
|
||||||
fmt.Printf("Badger Version: %s\n", config.BadgerVersion)
|
|
||||||
|
|
||||||
if !readBool(reader, "Are these values correct?", true) {
|
|
||||||
config = collectUserInput(reader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !checkIsCrowdsecInstalledInCompose() {
|
|
||||||
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")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error reading config: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
config.DashboardDomain = traefikConfig.DashboardDomain
|
|
||||||
config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail
|
|
||||||
config.BadgerVersion = traefikConfig.BadgerVersion
|
|
||||||
|
|
||||||
// print the values and check if they are right
|
|
||||||
fmt.Println("Detected values:")
|
|
||||||
fmt.Printf("Dashboard Domain: %s\n", config.DashboardDomain)
|
|
||||||
fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail)
|
|
||||||
fmt.Printf("Badger Version: %s\n", config.BadgerVersion)
|
|
||||||
|
|
||||||
if !readBool(reader, "Are these values correct?", true) {
|
|
||||||
config = collectUserInput(reader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.DoCrowdsecInstall = true
|
|
||||||
installCrowdsec(config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup Token Section
|
|
||||||
fmt.Println("\n=== Setup Token ===")
|
|
||||||
|
|
||||||
// Check if containers were started during this installation
|
|
||||||
containersStarted := false
|
|
||||||
if (isDockerInstalled() && chosenContainer == Docker) ||
|
|
||||||
(isPodmanInstalled() && chosenContainer == Podman) {
|
|
||||||
// Try to fetch and display the token if containers are running
|
|
||||||
containersStarted = true
|
|
||||||
printSetupToken(chosenContainer, config.DashboardDomain)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If containers weren't started or token wasn't found, show instructions
|
|
||||||
if !containersStarted {
|
|
||||||
showSetupTokenInstructions(chosenContainer, config.DashboardDomain)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Installation complete!")
|
|
||||||
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ""
|
|
||||||
}
|
|
||||||
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 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectUserInput(reader *bufio.Reader) Config {
|
func collectUserInput(reader *bufio.Reader) Config {
|
||||||
@@ -353,43 +308,73 @@ func collectUserInput(reader *bufio.Reader) Config {
|
|||||||
|
|
||||||
// Basic configuration
|
// Basic configuration
|
||||||
fmt.Println("\n=== Basic Configuration ===")
|
fmt.Println("\n=== Basic Configuration ===")
|
||||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
for {
|
||||||
|
response := readString(reader, "Do you want to install Pangolin as a cloud-managed (beta) node? (yes/no)", "")
|
||||||
// Set default dashboard domain after base domain is collected
|
if strings.EqualFold(response, "yes") || strings.EqualFold(response, "y") {
|
||||||
defaultDashboardDomain := ""
|
config.HybridMode = true
|
||||||
if config.BaseDomain != "" {
|
break
|
||||||
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
} else if strings.EqualFold(response, "no") || strings.EqualFold(response, "n") {
|
||||||
|
config.HybridMode = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Println("Please answer 'yes' or 'no'")
|
||||||
}
|
}
|
||||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
|
||||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
if config.HybridMode {
|
||||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
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)
|
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", 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.DashboardDomain == "" {
|
if config.DashboardDomain == "" {
|
||||||
fmt.Println("Error: Dashboard Domain name is required")
|
fmt.Println("Error: Dashboard Domain name is required")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if config.LetsEncryptEmail == "" {
|
|
||||||
fmt.Println("Error: Let's Encrypt email is required")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
@@ -419,6 +404,11 @@ func createConfigFiles(config Config) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the hybrid does not need the dynamic config
|
||||||
|
if config.HybridMode && strings.Contains(path, "dynamic_config.yml") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// skip .DS_Store
|
// skip .DS_Store
|
||||||
if strings.Contains(path, ".DS_Store") {
|
if strings.Contains(path, ".DS_Store") {
|
||||||
return nil
|
return nil
|
||||||
@@ -470,297 +460,6 @@ func createConfigFiles(config Config) error {
|
|||||||
return nil
|
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 {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyFile(src, dst string) error {
|
func copyFile(src, dst string) error {
|
||||||
source, err := os.Open(src)
|
source, err := os.Open(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -786,34 +485,6 @@ func moveFile(src, dst string) error {
|
|||||||
return os.Remove(src)
|
return os.Remove(src)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 printSetupToken(containerType SupportedContainer, dashboardDomain string) {
|
func printSetupToken(containerType SupportedContainer, dashboardDomain string) {
|
||||||
fmt.Println("Waiting for Pangolin to generate setup token...")
|
fmt.Println("Waiting for Pangolin to generate setup token...")
|
||||||
|
|
||||||
@@ -938,3 +609,19 @@ func checkPortsAvailable(port int) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"setupCreate": "Създайте своя организация, сайт и ресурси",
|
"setupCreate": "Създайте своя организация, сайт и ресурси",
|
||||||
"setupNewOrg": "Нова организация",
|
"setupNewOrg": "Нова организация",
|
||||||
"setupCreateOrg": "Създай организация",
|
"setupCreateOrg": "Създаване на организация",
|
||||||
"setupCreateResources": "Създай ресурси",
|
"setupCreateResources": "Създаване на ресурси",
|
||||||
"setupOrgName": "Име на организацията",
|
"setupOrgName": "Име на организацията",
|
||||||
"orgDisplayName": "Това е публичното име на вашата организация.",
|
"orgDisplayName": "Това е публичното име на вашата организация.",
|
||||||
"orgId": "Идентификатор на организация",
|
"orgId": "Идентификатор на организация",
|
||||||
@@ -12,12 +12,12 @@
|
|||||||
"componentsErrorNoMember": "В момента не сте част от организация.",
|
"componentsErrorNoMember": "В момента не сте част от организация.",
|
||||||
"welcome": "Добре дошли!",
|
"welcome": "Добре дошли!",
|
||||||
"welcomeTo": "Добре дошли в",
|
"welcomeTo": "Добре дошли в",
|
||||||
"componentsCreateOrg": "Създай организация",
|
"componentsCreateOrg": "Създаване на организация",
|
||||||
"componentsMember": "You're a member of {count, plural, =0 {no organization} one {one organization} other {# organizations}}.",
|
"componentsMember": "Вие сте част от {count, plural, =0 {нула организации} one {една организация} other {# организации}}.",
|
||||||
"componentsInvalidKey": "Invalid or expired license keys detected. Follow license terms to continue using all features.",
|
"componentsInvalidKey": "Засечен е невалиден или изтекъл лиценз. Проверете лицензионните условия, за да се възползвате от всички функционалности.",
|
||||||
"dismiss": "Dismiss",
|
"dismiss": "Отхвърляне",
|
||||||
"componentsLicenseViolation": "License Violation: This server is using {usedSites} sites which exceeds its licensed limit of {maxSites} sites. Follow license terms to continue using all features.",
|
"componentsLicenseViolation": "Нарушение на лиценза: Сървърът използва {usedSites} сайта, което надвишава лицензионния лимит от {maxSites} сайта. Проверете лицензионните условия, за да се възползвате от всички функционалности.",
|
||||||
"componentsSupporterMessage": "Thank you for supporting Pangolin as a {tier}!",
|
"componentsSupporterMessage": "Благодарим ви, че подкрепяте Pangolin като {tier}!",
|
||||||
"inviteErrorNotValid": "We're sorry, but it looks like the invite you're trying to access has not been accepted or is no longer valid.",
|
"inviteErrorNotValid": "We're sorry, but it looks like the invite you're trying to access has not been accepted or is no longer valid.",
|
||||||
"inviteErrorUser": "We're sorry, but it looks like the invite you're trying to access is not for this user.",
|
"inviteErrorUser": "We're sorry, but it looks like the invite you're trying to access is not for this user.",
|
||||||
"inviteLoginUser": "Please make sure you're logged in as the correct user.",
|
"inviteLoginUser": "Please make sure you're logged in as the correct user.",
|
||||||
@@ -29,24 +29,24 @@
|
|||||||
"inviteNotAccepted": "Invite Not Accepted",
|
"inviteNotAccepted": "Invite Not Accepted",
|
||||||
"authCreateAccount": "Create an account to get started",
|
"authCreateAccount": "Create an account to get started",
|
||||||
"authNoAccount": "Don't have an account?",
|
"authNoAccount": "Don't have an account?",
|
||||||
"email": "Email",
|
"email": "Имейл",
|
||||||
"password": "Password",
|
"password": "Парола",
|
||||||
"confirmPassword": "Confirm Password",
|
"confirmPassword": "Потвърждение на паролата",
|
||||||
"createAccount": "Create Account",
|
"createAccount": "Създаване на профил",
|
||||||
"viewSettings": "View settings",
|
"viewSettings": "Преглед на настройките",
|
||||||
"delete": "Delete",
|
"delete": "Изтриване",
|
||||||
"name": "Name",
|
"name": "Име",
|
||||||
"online": "Online",
|
"online": "На линия",
|
||||||
"offline": "Offline",
|
"offline": "Извън линия",
|
||||||
"site": "Site",
|
"site": "Сайт",
|
||||||
"dataIn": "Data In",
|
"dataIn": "Входящ трафик",
|
||||||
"dataOut": "Data Out",
|
"dataOut": "Изходящ трафик",
|
||||||
"connectionType": "Connection Type",
|
"connectionType": "Вид на връзката",
|
||||||
"tunnelType": "Tunnel Type",
|
"tunnelType": "Вид на тунела",
|
||||||
"local": "Local",
|
"local": "Локална",
|
||||||
"edit": "Edit",
|
"edit": "Редактиране",
|
||||||
"siteConfirmDelete": "Confirm Delete Site",
|
"siteConfirmDelete": "Потвърждение на изтриване на сайта",
|
||||||
"siteDelete": "Delete Site",
|
"siteDelete": "Изтриване на сайта",
|
||||||
"siteMessageRemove": "Once removed, the site will no longer be accessible. All resources and targets associated with the site will also be removed.",
|
"siteMessageRemove": "Once removed, the site will no longer be accessible. All resources and targets associated with the site will also be removed.",
|
||||||
"siteMessageConfirm": "To confirm, please type the name of the site below.",
|
"siteMessageConfirm": "To confirm, please type the name of the site below.",
|
||||||
"siteQuestionRemove": "Are you sure you want to remove the site {selectedSite} from the organization?",
|
"siteQuestionRemove": "Are you sure you want to remove the site {selectedSite} from the organization?",
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
"siteErrorDelete": "Error deleting site",
|
"siteErrorDelete": "Error deleting site",
|
||||||
"siteErrorUpdate": "Failed to update site",
|
"siteErrorUpdate": "Failed to update site",
|
||||||
"siteErrorUpdateDescription": "An error occurred while updating the site.",
|
"siteErrorUpdateDescription": "An error occurred while updating the site.",
|
||||||
"siteUpdated": "Site updated",
|
"siteUpdated": "Сайтът е обновен",
|
||||||
"siteUpdatedDescription": "The site has been updated.",
|
"siteUpdatedDescription": "The site has been updated.",
|
||||||
"siteGeneralDescription": "Configure the general settings for this site",
|
"siteGeneralDescription": "Configure the general settings for this site",
|
||||||
"siteSettingDescription": "Configure the settings on your site",
|
"siteSettingDescription": "Configure the settings on your site",
|
||||||
@@ -94,7 +94,9 @@
|
|||||||
"siteNewtTunnelDescription": "Easiest way to create an entrypoint into your network. No extra setup.",
|
"siteNewtTunnelDescription": "Easiest way to create an entrypoint into your network. No extra setup.",
|
||||||
"siteWg": "Basic WireGuard",
|
"siteWg": "Basic WireGuard",
|
||||||
"siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
|
"siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
|
||||||
|
"siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES",
|
||||||
"siteLocalDescription": "Local resources only. No tunneling.",
|
"siteLocalDescription": "Local resources only. No tunneling.",
|
||||||
|
"siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES",
|
||||||
"siteSeeAll": "See All Sites",
|
"siteSeeAll": "See All Sites",
|
||||||
"siteTunnelDescription": "Determine how you want to connect to your site",
|
"siteTunnelDescription": "Determine how you want to connect to your site",
|
||||||
"siteNewtCredentials": "Newt Credentials",
|
"siteNewtCredentials": "Newt Credentials",
|
||||||
@@ -166,7 +168,7 @@
|
|||||||
"siteSelect": "Select site",
|
"siteSelect": "Select site",
|
||||||
"siteSearch": "Search site",
|
"siteSearch": "Search site",
|
||||||
"siteNotFound": "No site found.",
|
"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",
|
"resourceType": "Resource Type",
|
||||||
"resourceTypeDescription": "Determine how you want to access your resource",
|
"resourceTypeDescription": "Determine how you want to access your resource",
|
||||||
"resourceHTTPSSettings": "HTTPS Settings",
|
"resourceHTTPSSettings": "HTTPS Settings",
|
||||||
@@ -197,11 +199,13 @@
|
|||||||
"general": "General",
|
"general": "General",
|
||||||
"generalSettings": "General Settings",
|
"generalSettings": "General Settings",
|
||||||
"proxy": "Proxy",
|
"proxy": "Proxy",
|
||||||
|
"internal": "Internal",
|
||||||
"rules": "Rules",
|
"rules": "Rules",
|
||||||
"resourceSettingDescription": "Configure the settings on your resource",
|
"resourceSettingDescription": "Configure the settings on your resource",
|
||||||
"resourceSetting": "{resourceName} Settings",
|
"resourceSetting": "{resourceName} Settings",
|
||||||
"alwaysAllow": "Always Allow",
|
"alwaysAllow": "Always Allow",
|
||||||
"alwaysDeny": "Always Deny",
|
"alwaysDeny": "Always Deny",
|
||||||
|
"passToAuth": "Pass to Auth",
|
||||||
"orgSettingsDescription": "Configure your organization's general settings",
|
"orgSettingsDescription": "Configure your organization's general settings",
|
||||||
"orgGeneralSettings": "Organization Settings",
|
"orgGeneralSettings": "Organization Settings",
|
||||||
"orgGeneralSettingsDescription": "Manage your organization details and configuration",
|
"orgGeneralSettingsDescription": "Manage your organization details and configuration",
|
||||||
@@ -490,7 +494,7 @@
|
|||||||
"targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.",
|
"targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.",
|
||||||
"targetTlsSubmit": "Save Settings",
|
"targetTlsSubmit": "Save Settings",
|
||||||
"targets": "Targets Configuration",
|
"targets": "Targets Configuration",
|
||||||
"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",
|
"targetStickySessions": "Enable Sticky Sessions",
|
||||||
"targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.",
|
"targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.",
|
||||||
"methodSelect": "Select method",
|
"methodSelect": "Select method",
|
||||||
@@ -542,6 +546,7 @@
|
|||||||
"rulesActions": "Actions",
|
"rulesActions": "Actions",
|
||||||
"rulesActionAlwaysAllow": "Always Allow: Bypass all authentication methods",
|
"rulesActionAlwaysAllow": "Always Allow: Bypass all authentication methods",
|
||||||
"rulesActionAlwaysDeny": "Always Deny: Block all requests; no authentication can be attempted",
|
"rulesActionAlwaysDeny": "Always Deny: Block all requests; no authentication can be attempted",
|
||||||
|
"rulesActionPassToAuth": "Pass to Auth: Allow authentication methods to be attempted",
|
||||||
"rulesMatchCriteria": "Matching Criteria",
|
"rulesMatchCriteria": "Matching Criteria",
|
||||||
"rulesMatchCriteriaIpAddress": "Match a specific IP address",
|
"rulesMatchCriteriaIpAddress": "Match a specific IP address",
|
||||||
"rulesMatchCriteriaIpAddressRange": "Match a range of IP addresses in CIDR notation",
|
"rulesMatchCriteriaIpAddressRange": "Match a range of IP addresses in CIDR notation",
|
||||||
@@ -970,6 +975,7 @@
|
|||||||
"logoutError": "Error logging out",
|
"logoutError": "Error logging out",
|
||||||
"signingAs": "Signed in as",
|
"signingAs": "Signed in as",
|
||||||
"serverAdmin": "Server Admin",
|
"serverAdmin": "Server Admin",
|
||||||
|
"managedSelfhosted": "Managed Self-Hosted",
|
||||||
"otpEnable": "Enable Two-factor",
|
"otpEnable": "Enable Two-factor",
|
||||||
"otpDisable": "Disable Two-factor",
|
"otpDisable": "Disable Two-factor",
|
||||||
"logout": "Log Out",
|
"logout": "Log Out",
|
||||||
@@ -986,7 +992,7 @@
|
|||||||
"actionGetSite": "Get Site",
|
"actionGetSite": "Get Site",
|
||||||
"actionListSites": "List Sites",
|
"actionListSites": "List Sites",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "Setup Token",
|
||||||
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
"setupTokenDescription": "Enter the setup token from the server console.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "Setup token is required",
|
||||||
"actionUpdateSite": "Update Site",
|
"actionUpdateSite": "Update Site",
|
||||||
"actionListSiteRoles": "List Allowed Site Roles",
|
"actionListSiteRoles": "List Allowed Site Roles",
|
||||||
@@ -1048,6 +1054,12 @@
|
|||||||
"actionUpdateClient": "Update Client",
|
"actionUpdateClient": "Update Client",
|
||||||
"actionListClients": "List Clients",
|
"actionListClients": "List Clients",
|
||||||
"actionGetClient": "Get Client",
|
"actionGetClient": "Get Client",
|
||||||
|
"actionCreateSiteResource": "Create Site Resource",
|
||||||
|
"actionDeleteSiteResource": "Delete Site Resource",
|
||||||
|
"actionGetSiteResource": "Get Site Resource",
|
||||||
|
"actionListSiteResources": "List Site Resources",
|
||||||
|
"actionUpdateSiteResource": "Update Site Resource",
|
||||||
|
"actionListInvitations": "List Invitations",
|
||||||
"noneSelected": "None selected",
|
"noneSelected": "None selected",
|
||||||
"orgNotFound2": "No organizations found.",
|
"orgNotFound2": "No organizations found.",
|
||||||
"searchProgress": "Search...",
|
"searchProgress": "Search...",
|
||||||
@@ -1344,5 +1356,145 @@
|
|||||||
"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.",
|
"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",
|
"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.",
|
"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"
|
"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.",
|
||||||
|
"managedSelfHosted": {
|
||||||
|
"title": "Managed Self-Hosted",
|
||||||
|
"description": "More reliable and low-maintenance self-hosted Pangolin server with extra bells and whistles",
|
||||||
|
"introTitle": "Managed Self-Hosted Pangolin",
|
||||||
|
"introDescription": "is a deployment option designed for people who want simplicity and extra reliability while still keeping their data private and self-hosted.",
|
||||||
|
"introDetail": "With this option, you still run your own Pangolin node — your tunnels, SSL termination, and traffic all stay on your server. The difference is that management and monitoring are handled through our cloud dashboard, which unlocks a number of benefits:",
|
||||||
|
"benefitSimplerOperations": {
|
||||||
|
"title": "Simpler operations",
|
||||||
|
"description": "No need to run your own mail server or set up complex alerting. You'll get health checks and downtime alerts out of the box."
|
||||||
|
},
|
||||||
|
"benefitAutomaticUpdates": {
|
||||||
|
"title": "Automatic updates",
|
||||||
|
"description": "The cloud dashboard evolves quickly, so you get new features and bug fixes without having to manually pull new containers every time."
|
||||||
|
},
|
||||||
|
"benefitLessMaintenance": {
|
||||||
|
"title": "Less maintenance",
|
||||||
|
"description": "No database migrations, backups, or extra infrastructure to manage. We handle that in the cloud."
|
||||||
|
},
|
||||||
|
"benefitCloudFailover": {
|
||||||
|
"title": "Cloud failover",
|
||||||
|
"description": "If your node goes down, your tunnels can temporarily fail over to our cloud points of presence until you bring it back online."
|
||||||
|
},
|
||||||
|
"benefitHighAvailability": {
|
||||||
|
"title": "High availability (PoPs)",
|
||||||
|
"description": "You can also attach multiple nodes to your account for redundancy and better performance."
|
||||||
|
},
|
||||||
|
"benefitFutureEnhancements": {
|
||||||
|
"title": "Future enhancements",
|
||||||
|
"description": "We're planning to add more analytics, alerting, and management tools to make your deployment even more robust."
|
||||||
|
},
|
||||||
|
"docsAlert": {
|
||||||
|
"text": "Learn more about the Managed Self-Hosted option in our",
|
||||||
|
"documentation": "documentation"
|
||||||
|
},
|
||||||
|
"convertButton": "Convert This Node to Managed Self-Hosted"
|
||||||
|
},
|
||||||
|
"internationaldomaindetected": "International Domain Detected",
|
||||||
|
"willbestoredas": "Will be stored as:"
|
||||||
}
|
}
|
||||||
@@ -10,8 +10,8 @@
|
|||||||
"setupErrorIdentifier": "ID organizace je již použito. Zvolte prosím jiné.",
|
"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.",
|
"componentsErrorNoMemberCreate": "Zatím nejste členem žádné organizace. Abyste mohli začít, vytvořte si organizaci.",
|
||||||
"componentsErrorNoMember": "Zatím nejste členem žádných organizací.",
|
"componentsErrorNoMember": "Zatím nejste členem žádných organizací.",
|
||||||
"welcome": "Welcome!",
|
"welcome": "Vítejte!",
|
||||||
"welcomeTo": "Welcome to",
|
"welcomeTo": "Vítejte v",
|
||||||
"componentsCreateOrg": "Vytvořte organizaci",
|
"componentsCreateOrg": "Vytvořte organizaci",
|
||||||
"componentsMember": "Jste členem {count, plural, =0 {0 organizací} one {1 organizace} other {# organizací}}.",
|
"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.",
|
"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.",
|
||||||
@@ -62,129 +62,131 @@
|
|||||||
"method": "Způsob",
|
"method": "Způsob",
|
||||||
"siteMethodDescription": "Tímto způsobem budete vystavovat spojení.",
|
"siteMethodDescription": "Tímto způsobem budete vystavovat spojení.",
|
||||||
"siteLearnNewt": "Naučte se, jak nainstalovat Newt na svůj systém",
|
"siteLearnNewt": "Naučte se, jak nainstalovat Newt na svůj systém",
|
||||||
"siteSeeConfigOnce": "You will only be able to see the configuration once.",
|
"siteSeeConfigOnce": "Konfiguraci uvidíte pouze jednou.",
|
||||||
"siteLoadWGConfig": "Loading WireGuard configuration...",
|
"siteLoadWGConfig": "Načítání konfigurace WireGuard...",
|
||||||
"siteDocker": "Expand for Docker Deployment Details",
|
"siteDocker": "Rozbalit pro detaily nasazení v Dockeru",
|
||||||
"toggle": "Toggle",
|
"toggle": "Přepínač",
|
||||||
"dockerCompose": "Docker Compose",
|
"dockerCompose": "Docker Compose",
|
||||||
"dockerRun": "Docker Run",
|
"dockerRun": "Docker Run",
|
||||||
"siteLearnLocal": "Local sites do not tunnel, learn more",
|
"siteLearnLocal": "Místní lokality se netunelují, dozvědět se více",
|
||||||
"siteConfirmCopy": "I have copied the config",
|
"siteConfirmCopy": "Konfiguraci jsem zkopíroval",
|
||||||
"searchSitesProgress": "Search sites...",
|
"searchSitesProgress": "Hledat lokality...",
|
||||||
"siteAdd": "Add Site",
|
"siteAdd": "Přidat lokalitu",
|
||||||
"siteInstallNewt": "Install Newt",
|
"siteInstallNewt": "Nainstalovat Newt",
|
||||||
"siteInstallNewtDescription": "Get Newt running on your system",
|
"siteInstallNewtDescription": "Spustit Newt na vašem systému",
|
||||||
"WgConfiguration": "WireGuard Configuration",
|
"WgConfiguration": "Konfigurace WireGuard",
|
||||||
"WgConfigurationDescription": "Use the following configuration to connect to your network",
|
"WgConfigurationDescription": "Použijte následující konfiguraci pro připojení k vaší síti",
|
||||||
"operatingSystem": "Operating System",
|
"operatingSystem": "Operační systém",
|
||||||
"commands": "Commands",
|
"commands": "Příkazy",
|
||||||
"recommended": "Recommended",
|
"recommended": "Doporučeno",
|
||||||
"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.",
|
"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": "Runs in Docker",
|
"siteRunsInDocker": "Běží v Dockeru",
|
||||||
"siteRunsInShell": "Runs in shell on macOS, Linux, and Windows",
|
"siteRunsInShell": "Běží v shellu na macOS, Linuxu a Windows",
|
||||||
"siteErrorDelete": "Error deleting site",
|
"siteErrorDelete": "Chyba při odstraňování lokality",
|
||||||
"siteErrorUpdate": "Failed to update site",
|
"siteErrorUpdate": "Nepodařilo se upravit lokalitu",
|
||||||
"siteErrorUpdateDescription": "An error occurred while updating the site.",
|
"siteErrorUpdateDescription": "Při úpravě lokality došlo k chybě.",
|
||||||
"siteUpdated": "Site updated",
|
"siteUpdated": "Lokalita upravena",
|
||||||
"siteUpdatedDescription": "The site has been updated.",
|
"siteUpdatedDescription": "Lokalita byla upravena.",
|
||||||
"siteGeneralDescription": "Configure the general settings for this site",
|
"siteGeneralDescription": "Upravte obecná nastavení pro tuto lokalitu",
|
||||||
"siteSettingDescription": "Configure the settings on your site",
|
"siteSettingDescription": "Upravte nastavení vaší lokality",
|
||||||
"siteSetting": "{siteName} Settings",
|
"siteSetting": "Nastavení {siteName}",
|
||||||
"siteNewtTunnel": "Newt Tunnel (Recommended)",
|
"siteNewtTunnel": "Tunel Newt (doporučeno)",
|
||||||
"siteNewtTunnelDescription": "Easiest way to create an entrypoint into your network. No extra setup.",
|
"siteNewtTunnelDescription": "Nejjednodušší způsob, jak vytvořit vstupní bod do vaší sítě. Žádné další nastavení.",
|
||||||
"siteWg": "Basic WireGuard",
|
"siteWg": "Základní WireGuard",
|
||||||
"siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
|
"siteWgDescription": "Použijte jakéhokoli klienta WireGuard abyste sestavili tunel. Vyžaduje se ruční nastavení NAT.",
|
||||||
"siteLocalDescription": "Local resources only. No tunneling.",
|
"siteWgDescriptionSaas": "Použijte jakéhokoli klienta WireGuard abyste sestavili tunel. Vyžaduje se ruční nastavení NAT. FUNGUJE POUZE NA SELF-HOSTED SERVERECH",
|
||||||
"siteSeeAll": "See All Sites",
|
"siteLocalDescription": "Pouze lokální zdroje. Žádný tunel.",
|
||||||
"siteTunnelDescription": "Determine how you want to connect to your site",
|
"siteLocalDescriptionSaas": "Pouze lokální zdroje. Žádný tunel. FUNGUJE POUZE NA SELF-HOSTED SERVERECH",
|
||||||
"siteNewtCredentials": "Newt Credentials",
|
"siteSeeAll": "Zobrazit všechny lokality",
|
||||||
"siteNewtCredentialsDescription": "This is how Newt will authenticate with the server",
|
"siteTunnelDescription": "Určete jak se chcete připojit k vaší lokalitě",
|
||||||
"siteCredentialsSave": "Save Your Credentials",
|
"siteNewtCredentials": "Přihlašovací údaje Newt",
|
||||||
"siteCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
|
"siteNewtCredentialsDescription": "Tímto způsobem se bude Newt autentizovat na serveru",
|
||||||
"siteInfo": "Site Information",
|
"siteCredentialsSave": "Uložit přihlašovací údaje",
|
||||||
"status": "Status",
|
"siteCredentialsSaveDescription": "Toto nastavení uvidíte pouze jednou. Ujistěte se, že jej zkopírujete na bezpečné místo.",
|
||||||
"shareTitle": "Manage Share Links",
|
"siteInfo": "Údaje o lokalitě",
|
||||||
"shareDescription": "Create shareable links to grant temporary or permanent access to your resources",
|
"status": "Stav",
|
||||||
"shareSearch": "Search share links...",
|
"shareTitle": "Spravovat sdílení odkazů",
|
||||||
"shareCreate": "Create Share Link",
|
"shareDescription": "Vytvořte odkazy, abyste udělili dočasný nebo trvalý přístup k vašim zdrojům",
|
||||||
"shareErrorDelete": "Failed to delete link",
|
"shareSearch": "Hledat sdílené odkazy...",
|
||||||
"shareErrorDeleteMessage": "An error occurred deleting link",
|
"shareCreate": "Vytvořit odkaz",
|
||||||
"shareDeleted": "Link deleted",
|
"shareErrorDelete": "Nepodařilo se odstranit odkaz",
|
||||||
"shareDeletedDescription": "The link has been deleted",
|
"shareErrorDeleteMessage": "Došlo k chybě při odstraňování odkazu",
|
||||||
"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.",
|
"shareDeleted": "Odkaz odstraněn",
|
||||||
"accessToken": "Access Token",
|
"shareDeletedDescription": "Odkaz byl odstraněn",
|
||||||
"usageExamples": "Usage Examples",
|
"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.",
|
||||||
"tokenId": "Token ID",
|
"accessToken": "Přístupový token",
|
||||||
"requestHeades": "Request Headers",
|
"usageExamples": "Příklady použití",
|
||||||
"queryParameter": "Query Parameter",
|
"tokenId": "ID tokenu",
|
||||||
"importantNote": "Important Note",
|
"requestHeades": "Hlavičky požadavku",
|
||||||
"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.",
|
"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",
|
"token": "Token",
|
||||||
"shareTokenSecurety": "Keep your access token secure. Do not share it in publicly accessible areas or client-side code.",
|
"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": "Failed to fetch resources",
|
"shareErrorFetchResource": "Nepodařilo se načíst zdroje",
|
||||||
"shareErrorFetchResourceDescription": "An error occurred while fetching the resources",
|
"shareErrorFetchResourceDescription": "Při načítání zdrojů došlo k chybě",
|
||||||
"shareErrorCreate": "Failed to create share link",
|
"shareErrorCreate": "Nepodařilo se vytvořit odkaz",
|
||||||
"shareErrorCreateDescription": "An error occurred while creating the share link",
|
"shareErrorCreateDescription": "Při vytváření odkazu došlo k chybě",
|
||||||
"shareCreateDescription": "Anyone with this link can access the resource",
|
"shareCreateDescription": "Kdokoliv s tímto odkazem může přistupovat ke zdroji",
|
||||||
"shareTitleOptional": "Title (optional)",
|
"shareTitleOptional": "Název (volitelné)",
|
||||||
"expireIn": "Expire In",
|
"expireIn": "Platnost vyprší za",
|
||||||
"neverExpire": "Never expire",
|
"neverExpire": "Nikdy nevyprší",
|
||||||
"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.",
|
"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": "You will only be able to see this linkonce. Make sure to copy it.",
|
"shareSeeOnce": "Tento odkaz uvidíte pouze jednou. Ujistěte se, že jste jej zkopírovali.",
|
||||||
"shareAccessHint": "Anyone with this link can access the resource. Share it with care.",
|
"shareAccessHint": "Kdokoli s tímto odkazem může přistupovat ke zdroji. Sdílejte jej s rozvahou.",
|
||||||
"shareTokenUsage": "See Access Token Usage",
|
"shareTokenUsage": "Zobrazit využití přístupového tokenu",
|
||||||
"createLink": "Create Link",
|
"createLink": "Vytvořit odkaz",
|
||||||
"resourcesNotFound": "No resources found",
|
"resourcesNotFound": "Nebyly nalezeny žádné zdroje",
|
||||||
"resourceSearch": "Search resources",
|
"resourceSearch": "Vyhledat zdroje",
|
||||||
"openMenu": "Open menu",
|
"openMenu": "Otevřít nabídku",
|
||||||
"resource": "Resource",
|
"resource": "Zdroj",
|
||||||
"title": "Title",
|
"title": "Název",
|
||||||
"created": "Created",
|
"created": "Vytvořeno",
|
||||||
"expires": "Expires",
|
"expires": "Vyprší",
|
||||||
"never": "Never",
|
"never": "Nikdy",
|
||||||
"shareErrorSelectResource": "Please select a resource",
|
"shareErrorSelectResource": "Zvolte prosím zdroj",
|
||||||
"resourceTitle": "Manage Resources",
|
"resourceTitle": "Spravovat zdroje",
|
||||||
"resourceDescription": "Create secure proxies to your private applications",
|
"resourceDescription": "Vytvořte bezpečné proxy služby pro přístup k privátním aplikacím",
|
||||||
"resourcesSearch": "Search resources...",
|
"resourcesSearch": "Prohledat zdroje...",
|
||||||
"resourceAdd": "Add Resource",
|
"resourceAdd": "Přidat zdroj",
|
||||||
"resourceErrorDelte": "Error deleting resource",
|
"resourceErrorDelte": "Chyba při odstraňování zdroje",
|
||||||
"authentication": "Authentication",
|
"authentication": "Autentifikace",
|
||||||
"protected": "Protected",
|
"protected": "Chráněno",
|
||||||
"notProtected": "Not Protected",
|
"notProtected": "Nechráněno",
|
||||||
"resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.",
|
"resourceMessageRemove": "Jakmile zdroj odstraníte, nebude dostupný. Všechny související služby a cíle budou také odstraněny.",
|
||||||
"resourceMessageConfirm": "To confirm, please type the name of the resource below.",
|
"resourceMessageConfirm": "Pro potvrzení zadejte prosím název zdroje.",
|
||||||
"resourceQuestionRemove": "Are you sure you want to remove the resource {selectedResource} from the organization?",
|
"resourceQuestionRemove": "Opravdu chcete odstranit zdroj {selectedResource} z organizace?",
|
||||||
"resourceHTTP": "HTTPS Resource",
|
"resourceHTTP": "Zdroj HTTPS",
|
||||||
"resourceHTTPDescription": "Proxy requests to your app over HTTPS using a subdomain or base domain.",
|
"resourceHTTPDescription": "Proxy requests to your app over HTTPS using a subdomain or base domain.",
|
||||||
"resourceRaw": "Raw TCP/UDP Resource",
|
"resourceRaw": "Raw TCP/UDP Resource",
|
||||||
"resourceRawDescription": "Proxy requests to your app over TCP/UDP using a port number.",
|
"resourceRawDescription": "Proxy requests to your app over TCP/UDP using a port number.",
|
||||||
"resourceCreate": "Create Resource",
|
"resourceCreate": "Vytvořit zdroj",
|
||||||
"resourceCreateDescription": "Follow the steps below to create a new resource",
|
"resourceCreateDescription": "Postupujte podle níže uvedených kroků, abyste vytvořili a připojili nový zdroj",
|
||||||
"resourceSeeAll": "See All Resources",
|
"resourceSeeAll": "Zobrazit všechny zdroje",
|
||||||
"resourceInfo": "Resource Information",
|
"resourceInfo": "Informace o zdroji",
|
||||||
"resourceNameDescription": "This is the display name for the resource.",
|
"resourceNameDescription": "Toto je zobrazovaný název zdroje.",
|
||||||
"siteSelect": "Select site",
|
"siteSelect": "Vybrat lokalitu",
|
||||||
"siteSearch": "Search site",
|
"siteSearch": "Hledat lokalitu",
|
||||||
"siteNotFound": "No site found.",
|
"siteNotFound": "Nebyla nalezena žádná lokalita.",
|
||||||
"siteSelectionDescription": "This site will provide connectivity to the resource.",
|
"siteSelectionDescription": "Tato lokalita poskytne připojení k cíli.",
|
||||||
"resourceType": "Resource Type",
|
"resourceType": "Typ zdroje",
|
||||||
"resourceTypeDescription": "Determine how you want to access your resource",
|
"resourceTypeDescription": "Určete, jak chcete přistupovat ke svému zdroji",
|
||||||
"resourceHTTPSSettings": "HTTPS Settings",
|
"resourceHTTPSSettings": "Nastavení HTTPS",
|
||||||
"resourceHTTPSSettingsDescription": "Configure how your resource will be accessed over HTTPS",
|
"resourceHTTPSSettingsDescription": "Nakonfigurujte, jak bude váš zdroj přístupný přes HTTPS",
|
||||||
"domainType": "Domain Type",
|
"domainType": "Typ domény",
|
||||||
"subdomain": "Subdomain",
|
"subdomain": "Subdoména",
|
||||||
"baseDomain": "Base Domain",
|
"baseDomain": "Základní doména",
|
||||||
"subdomnainDescription": "The subdomain where your resource will be accessible.",
|
"subdomnainDescription": "Subdoména, kde bude váš zdroj přístupný.",
|
||||||
"resourceRawSettings": "TCP/UDP Settings",
|
"resourceRawSettings": "Nastavení TCP/UDP",
|
||||||
"resourceRawSettingsDescription": "Configure how your resource will be accessed over TCP/UDP",
|
"resourceRawSettingsDescription": "Nakonfigurujte, jak bude váš dokument přístupný přes TCP/UDP",
|
||||||
"protocol": "Protocol",
|
"protocol": "Protokol",
|
||||||
"protocolSelect": "Select a protocol",
|
"protocolSelect": "Vybrat protokol",
|
||||||
"resourcePortNumber": "Port Number",
|
"resourcePortNumber": "Číslo portu",
|
||||||
"resourcePortNumberDescription": "The external port number to proxy requests.",
|
"resourcePortNumberDescription": "Externí port k požadavkům proxy serveru.",
|
||||||
"cancel": "Cancel",
|
"cancel": "Zrušit",
|
||||||
"resourceConfig": "Configuration Snippets",
|
"resourceConfig": "Konfigurační snippety",
|
||||||
"resourceConfigDescription": "Copy and paste these configuration snippets to set up your TCP/UDP resource",
|
"resourceConfigDescription": "Zkopírujte a vložte tyto konfigurační snippety pro nastavení TCP/UDP zdroje",
|
||||||
"resourceAddEntrypoints": "Traefik: Add Entrypoints",
|
"resourceAddEntrypoints": "Traefik: Přidat vstupní body",
|
||||||
"resourceExposePorts": "Gerbil: Expose Ports in Docker Compose",
|
"resourceExposePorts": "Gerbil: Expose Ports in Docker Compose",
|
||||||
"resourceLearnRaw": "Learn how to configure TCP/UDP resources",
|
"resourceLearnRaw": "Learn how to configure TCP/UDP resources",
|
||||||
"resourceBack": "Back to Resources",
|
"resourceBack": "Back to Resources",
|
||||||
@@ -197,11 +199,13 @@
|
|||||||
"general": "General",
|
"general": "General",
|
||||||
"generalSettings": "General Settings",
|
"generalSettings": "General Settings",
|
||||||
"proxy": "Proxy",
|
"proxy": "Proxy",
|
||||||
|
"internal": "Internal",
|
||||||
"rules": "Rules",
|
"rules": "Rules",
|
||||||
"resourceSettingDescription": "Configure the settings on your resource",
|
"resourceSettingDescription": "Configure the settings on your resource",
|
||||||
"resourceSetting": "{resourceName} Settings",
|
"resourceSetting": "{resourceName} Settings",
|
||||||
"alwaysAllow": "Always Allow",
|
"alwaysAllow": "Always Allow",
|
||||||
"alwaysDeny": "Always Deny",
|
"alwaysDeny": "Always Deny",
|
||||||
|
"passToAuth": "Pass to Auth",
|
||||||
"orgSettingsDescription": "Configure your organization's general settings",
|
"orgSettingsDescription": "Configure your organization's general settings",
|
||||||
"orgGeneralSettings": "Organization Settings",
|
"orgGeneralSettings": "Organization Settings",
|
||||||
"orgGeneralSettingsDescription": "Manage your organization details and configuration",
|
"orgGeneralSettingsDescription": "Manage your organization details and configuration",
|
||||||
@@ -490,7 +494,7 @@
|
|||||||
"targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.",
|
"targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.",
|
||||||
"targetTlsSubmit": "Save Settings",
|
"targetTlsSubmit": "Save Settings",
|
||||||
"targets": "Targets Configuration",
|
"targets": "Targets Configuration",
|
||||||
"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",
|
"targetStickySessions": "Enable Sticky Sessions",
|
||||||
"targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.",
|
"targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.",
|
||||||
"methodSelect": "Select method",
|
"methodSelect": "Select method",
|
||||||
@@ -542,6 +546,7 @@
|
|||||||
"rulesActions": "Actions",
|
"rulesActions": "Actions",
|
||||||
"rulesActionAlwaysAllow": "Always Allow: Bypass all authentication methods",
|
"rulesActionAlwaysAllow": "Always Allow: Bypass all authentication methods",
|
||||||
"rulesActionAlwaysDeny": "Always Deny: Block all requests; no authentication can be attempted",
|
"rulesActionAlwaysDeny": "Always Deny: Block all requests; no authentication can be attempted",
|
||||||
|
"rulesActionPassToAuth": "Pass to Auth: Allow authentication methods to be attempted",
|
||||||
"rulesMatchCriteria": "Matching Criteria",
|
"rulesMatchCriteria": "Matching Criteria",
|
||||||
"rulesMatchCriteriaIpAddress": "Match a specific IP address",
|
"rulesMatchCriteriaIpAddress": "Match a specific IP address",
|
||||||
"rulesMatchCriteriaIpAddressRange": "Match a range of IP addresses in CIDR notation",
|
"rulesMatchCriteriaIpAddressRange": "Match a range of IP addresses in CIDR notation",
|
||||||
@@ -970,6 +975,7 @@
|
|||||||
"logoutError": "Error logging out",
|
"logoutError": "Error logging out",
|
||||||
"signingAs": "Signed in as",
|
"signingAs": "Signed in as",
|
||||||
"serverAdmin": "Server Admin",
|
"serverAdmin": "Server Admin",
|
||||||
|
"managedSelfhosted": "Managed Self-Hosted",
|
||||||
"otpEnable": "Enable Two-factor",
|
"otpEnable": "Enable Two-factor",
|
||||||
"otpDisable": "Disable Two-factor",
|
"otpDisable": "Disable Two-factor",
|
||||||
"logout": "Log Out",
|
"logout": "Log Out",
|
||||||
@@ -986,7 +992,7 @@
|
|||||||
"actionGetSite": "Get Site",
|
"actionGetSite": "Get Site",
|
||||||
"actionListSites": "List Sites",
|
"actionListSites": "List Sites",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "Setup Token",
|
||||||
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
"setupTokenDescription": "Enter the setup token from the server console.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "Setup token is required",
|
||||||
"actionUpdateSite": "Update Site",
|
"actionUpdateSite": "Update Site",
|
||||||
"actionListSiteRoles": "List Allowed Site Roles",
|
"actionListSiteRoles": "List Allowed Site Roles",
|
||||||
@@ -1048,6 +1054,12 @@
|
|||||||
"actionUpdateClient": "Update Client",
|
"actionUpdateClient": "Update Client",
|
||||||
"actionListClients": "List Clients",
|
"actionListClients": "List Clients",
|
||||||
"actionGetClient": "Get Client",
|
"actionGetClient": "Get Client",
|
||||||
|
"actionCreateSiteResource": "Create Site Resource",
|
||||||
|
"actionDeleteSiteResource": "Delete Site Resource",
|
||||||
|
"actionGetSiteResource": "Get Site Resource",
|
||||||
|
"actionListSiteResources": "List Site Resources",
|
||||||
|
"actionUpdateSiteResource": "Update Site Resource",
|
||||||
|
"actionListInvitations": "List Invitations",
|
||||||
"noneSelected": "None selected",
|
"noneSelected": "None selected",
|
||||||
"orgNotFound2": "No organizations found.",
|
"orgNotFound2": "No organizations found.",
|
||||||
"searchProgress": "Search...",
|
"searchProgress": "Search...",
|
||||||
@@ -1344,5 +1356,145 @@
|
|||||||
"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.",
|
"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",
|
"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.",
|
"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"
|
"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.",
|
||||||
|
"managedSelfHosted": {
|
||||||
|
"title": "Managed Self-Hosted",
|
||||||
|
"description": "More reliable and low-maintenance self-hosted Pangolin server with extra bells and whistles",
|
||||||
|
"introTitle": "Managed Self-Hosted Pangolin",
|
||||||
|
"introDescription": "is a deployment option designed for people who want simplicity and extra reliability while still keeping their data private and self-hosted.",
|
||||||
|
"introDetail": "With this option, you still run your own Pangolin node — your tunnels, SSL termination, and traffic all stay on your server. The difference is that management and monitoring are handled through our cloud dashboard, which unlocks a number of benefits:",
|
||||||
|
"benefitSimplerOperations": {
|
||||||
|
"title": "Simpler operations",
|
||||||
|
"description": "No need to run your own mail server or set up complex alerting. You'll get health checks and downtime alerts out of the box."
|
||||||
|
},
|
||||||
|
"benefitAutomaticUpdates": {
|
||||||
|
"title": "Automatic updates",
|
||||||
|
"description": "The cloud dashboard evolves quickly, so you get new features and bug fixes without having to manually pull new containers every time."
|
||||||
|
},
|
||||||
|
"benefitLessMaintenance": {
|
||||||
|
"title": "Less maintenance",
|
||||||
|
"description": "No database migrations, backups, or extra infrastructure to manage. We handle that in the cloud."
|
||||||
|
},
|
||||||
|
"benefitCloudFailover": {
|
||||||
|
"title": "Cloud failover",
|
||||||
|
"description": "If your node goes down, your tunnels can temporarily fail over to our cloud points of presence until you bring it back online."
|
||||||
|
},
|
||||||
|
"benefitHighAvailability": {
|
||||||
|
"title": "High availability (PoPs)",
|
||||||
|
"description": "You can also attach multiple nodes to your account for redundancy and better performance."
|
||||||
|
},
|
||||||
|
"benefitFutureEnhancements": {
|
||||||
|
"title": "Future enhancements",
|
||||||
|
"description": "We're planning to add more analytics, alerting, and management tools to make your deployment even more robust."
|
||||||
|
},
|
||||||
|
"docsAlert": {
|
||||||
|
"text": "Learn more about the Managed Self-Hosted option in our",
|
||||||
|
"documentation": "documentation"
|
||||||
|
},
|
||||||
|
"convertButton": "Convert This Node to Managed Self-Hosted"
|
||||||
|
},
|
||||||
|
"internationaldomaindetected": "International Domain Detected",
|
||||||
|
"willbestoredas": "Will be stored as:"
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,9 @@
|
|||||||
"siteNewtTunnelDescription": "Einfachster Weg, einen Zugriffspunkt zu deinem Netzwerk zu erstellen. Keine zusätzliche Einrichtung erforderlich.",
|
"siteNewtTunnelDescription": "Einfachster Weg, einen Zugriffspunkt zu deinem Netzwerk zu erstellen. Keine zusätzliche Einrichtung erforderlich.",
|
||||||
"siteWg": "Einfacher WireGuard Tunnel",
|
"siteWg": "Einfacher WireGuard Tunnel",
|
||||||
"siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.",
|
"siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.",
|
||||||
|
"siteWgDescriptionSaas": "Verwenden Sie jeden WireGuard-Client, um einen Tunnel zu erstellen. Manuelles NAT-Setup erforderlich. FUNKTIONIERT NUR BEI SELBSTGEHOSTETEN KNOTEN",
|
||||||
"siteLocalDescription": "Nur lokale Ressourcen. Kein Tunneling.",
|
"siteLocalDescription": "Nur lokale Ressourcen. Kein Tunneling.",
|
||||||
|
"siteLocalDescriptionSaas": "Nur lokale Ressourcen. Keine Tunneldurchführung. FUNKTIONIERT NUR BEI SELBSTGEHOSTETEN KNOTEN",
|
||||||
"siteSeeAll": "Alle Standorte anzeigen",
|
"siteSeeAll": "Alle Standorte anzeigen",
|
||||||
"siteTunnelDescription": "Lege fest, wie du dich mit deinem Standort verbinden möchtest",
|
"siteTunnelDescription": "Lege fest, wie du dich mit deinem Standort verbinden möchtest",
|
||||||
"siteNewtCredentials": "Neue Newt Zugangsdaten",
|
"siteNewtCredentials": "Neue Newt Zugangsdaten",
|
||||||
@@ -166,7 +168,7 @@
|
|||||||
"siteSelect": "Standort auswählen",
|
"siteSelect": "Standort auswählen",
|
||||||
"siteSearch": "Standorte durchsuchen",
|
"siteSearch": "Standorte durchsuchen",
|
||||||
"siteNotFound": "Keinen Standort gefunden.",
|
"siteNotFound": "Keinen Standort gefunden.",
|
||||||
"siteSelectionDescription": "Dieser Standort wird die Verbindung zu der Ressource herstellen.",
|
"siteSelectionDescription": "Dieser Standort wird die Verbindung zum Ziel herstellen.",
|
||||||
"resourceType": "Ressourcentyp",
|
"resourceType": "Ressourcentyp",
|
||||||
"resourceTypeDescription": "Legen Sie fest, wie Sie auf Ihre Ressource zugreifen möchten",
|
"resourceTypeDescription": "Legen Sie fest, wie Sie auf Ihre Ressource zugreifen möchten",
|
||||||
"resourceHTTPSSettings": "HTTPS-Einstellungen",
|
"resourceHTTPSSettings": "HTTPS-Einstellungen",
|
||||||
@@ -197,11 +199,13 @@
|
|||||||
"general": "Allgemein",
|
"general": "Allgemein",
|
||||||
"generalSettings": "Allgemeine Einstellungen",
|
"generalSettings": "Allgemeine Einstellungen",
|
||||||
"proxy": "Proxy",
|
"proxy": "Proxy",
|
||||||
|
"internal": "Intern",
|
||||||
"rules": "Regeln",
|
"rules": "Regeln",
|
||||||
"resourceSettingDescription": "Konfigurieren Sie die Einstellungen Ihrer Ressource",
|
"resourceSettingDescription": "Konfigurieren Sie die Einstellungen Ihrer Ressource",
|
||||||
"resourceSetting": "{resourceName} Einstellungen",
|
"resourceSetting": "{resourceName} Einstellungen",
|
||||||
"alwaysAllow": "Immer erlauben",
|
"alwaysAllow": "Immer erlauben",
|
||||||
"alwaysDeny": "Immer ablehnen",
|
"alwaysDeny": "Immer ablehnen",
|
||||||
|
"passToAuth": "Weiterleiten zur Authentifizierung",
|
||||||
"orgSettingsDescription": "Konfiguriere die allgemeinen Einstellungen deiner Organisation",
|
"orgSettingsDescription": "Konfiguriere die allgemeinen Einstellungen deiner Organisation",
|
||||||
"orgGeneralSettings": "Organisations-Einstellungen",
|
"orgGeneralSettings": "Organisations-Einstellungen",
|
||||||
"orgGeneralSettingsDescription": "Organisationsdetails und Konfiguration verwalten",
|
"orgGeneralSettingsDescription": "Organisationsdetails und Konfiguration verwalten",
|
||||||
@@ -490,7 +494,7 @@
|
|||||||
"targetTlsSniDescription": "Der zu verwendende TLS-Servername für SNI. Leer lassen, um den Standard zu verwenden.",
|
"targetTlsSniDescription": "Der zu verwendende TLS-Servername für SNI. Leer lassen, um den Standard zu verwenden.",
|
||||||
"targetTlsSubmit": "Einstellungen speichern",
|
"targetTlsSubmit": "Einstellungen speichern",
|
||||||
"targets": "Ziel-Konfiguration",
|
"targets": "Ziel-Konfiguration",
|
||||||
"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",
|
"targetStickySessions": "Sticky Sessions aktivieren",
|
||||||
"targetStickySessionsDescription": "Verbindungen für die gesamte Sitzung auf demselben Backend-Ziel halten.",
|
"targetStickySessionsDescription": "Verbindungen für die gesamte Sitzung auf demselben Backend-Ziel halten.",
|
||||||
"methodSelect": "Methode auswählen",
|
"methodSelect": "Methode auswählen",
|
||||||
@@ -542,6 +546,7 @@
|
|||||||
"rulesActions": "Aktionen",
|
"rulesActions": "Aktionen",
|
||||||
"rulesActionAlwaysAllow": "Immer erlauben: Alle Authentifizierungsmethoden umgehen",
|
"rulesActionAlwaysAllow": "Immer erlauben: Alle Authentifizierungsmethoden umgehen",
|
||||||
"rulesActionAlwaysDeny": "Immer verweigern: Alle Anfragen blockieren; keine Authentifizierung möglich",
|
"rulesActionAlwaysDeny": "Immer verweigern: Alle Anfragen blockieren; keine Authentifizierung möglich",
|
||||||
|
"rulesActionPassToAuth": "Weiterleiten zur Authentifizierung: Erlaubt das Versuchen von Authentifizierungsmethoden",
|
||||||
"rulesMatchCriteria": "Übereinstimmungskriterien",
|
"rulesMatchCriteria": "Übereinstimmungskriterien",
|
||||||
"rulesMatchCriteriaIpAddress": "Mit einer bestimmten IP-Adresse übereinstimmen",
|
"rulesMatchCriteriaIpAddress": "Mit einer bestimmten IP-Adresse übereinstimmen",
|
||||||
"rulesMatchCriteriaIpAddressRange": "Mit einem IP-Adressbereich in CIDR-Notation übereinstimmen",
|
"rulesMatchCriteriaIpAddressRange": "Mit einem IP-Adressbereich in CIDR-Notation übereinstimmen",
|
||||||
@@ -833,24 +838,24 @@
|
|||||||
"pincodeRequirementsLength": "PIN muss genau 6 Ziffern lang sein",
|
"pincodeRequirementsLength": "PIN muss genau 6 Ziffern lang sein",
|
||||||
"pincodeRequirementsChars": "PIN darf nur Zahlen enthalten",
|
"pincodeRequirementsChars": "PIN darf nur Zahlen enthalten",
|
||||||
"passwordRequirementsLength": "Passwort muss mindestens 1 Zeichen lang sein",
|
"passwordRequirementsLength": "Passwort muss mindestens 1 Zeichen lang sein",
|
||||||
"passwordRequirementsTitle": "Password requirements:",
|
"passwordRequirementsTitle": "Passwortanforderungen:",
|
||||||
"passwordRequirementLength": "At least 8 characters long",
|
"passwordRequirementLength": "Mindestens 8 Zeichen lang",
|
||||||
"passwordRequirementUppercase": "At least one uppercase letter",
|
"passwordRequirementUppercase": "Mindestens ein Großbuchstabe",
|
||||||
"passwordRequirementLowercase": "At least one lowercase letter",
|
"passwordRequirementLowercase": "Mindestens ein Kleinbuchstabe",
|
||||||
"passwordRequirementNumber": "At least one number",
|
"passwordRequirementNumber": "Mindestens eine Zahl",
|
||||||
"passwordRequirementSpecial": "At least one special character",
|
"passwordRequirementSpecial": "Mindestens ein Sonderzeichen",
|
||||||
"passwordRequirementsMet": "✓ Password meets all requirements",
|
"passwordRequirementsMet": "✓ Passwort erfüllt alle Anforderungen",
|
||||||
"passwordStrength": "Password strength",
|
"passwordStrength": "Passwortstärke",
|
||||||
"passwordStrengthWeak": "Weak",
|
"passwordStrengthWeak": "Schwach",
|
||||||
"passwordStrengthMedium": "Medium",
|
"passwordStrengthMedium": "Mittel",
|
||||||
"passwordStrengthStrong": "Strong",
|
"passwordStrengthStrong": "Stark",
|
||||||
"passwordRequirements": "Requirements:",
|
"passwordRequirements": "Anforderungen:",
|
||||||
"passwordRequirementLengthText": "8+ characters",
|
"passwordRequirementLengthText": "8+ Zeichen",
|
||||||
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
"passwordRequirementUppercaseText": "Großbuchstabe (A-Z)",
|
||||||
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
"passwordRequirementLowercaseText": "Kleinbuchstabe (a-z)",
|
||||||
"passwordRequirementNumberText": "Number (0-9)",
|
"passwordRequirementNumberText": "Zahl (0-9)",
|
||||||
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
"passwordRequirementSpecialText": "Sonderzeichen (!@#$%...)",
|
||||||
"passwordsDoNotMatch": "Passwords do not match",
|
"passwordsDoNotMatch": "Passwörter stimmen nicht überein",
|
||||||
"otpEmailRequirementsLength": "OTP muss mindestens 1 Zeichen lang sein",
|
"otpEmailRequirementsLength": "OTP muss mindestens 1 Zeichen lang sein",
|
||||||
"otpEmailSent": "OTP gesendet",
|
"otpEmailSent": "OTP gesendet",
|
||||||
"otpEmailSentDescription": "Ein OTP wurde an Ihre E-Mail gesendet",
|
"otpEmailSentDescription": "Ein OTP wurde an Ihre E-Mail gesendet",
|
||||||
@@ -970,6 +975,7 @@
|
|||||||
"logoutError": "Fehler beim Abmelden",
|
"logoutError": "Fehler beim Abmelden",
|
||||||
"signingAs": "Angemeldet als",
|
"signingAs": "Angemeldet als",
|
||||||
"serverAdmin": "Server-Administrator",
|
"serverAdmin": "Server-Administrator",
|
||||||
|
"managedSelfhosted": "Verwaltetes Selbsthosted",
|
||||||
"otpEnable": "Zwei-Faktor aktivieren",
|
"otpEnable": "Zwei-Faktor aktivieren",
|
||||||
"otpDisable": "Zwei-Faktor deaktivieren",
|
"otpDisable": "Zwei-Faktor deaktivieren",
|
||||||
"logout": "Abmelden",
|
"logout": "Abmelden",
|
||||||
@@ -985,9 +991,9 @@
|
|||||||
"actionDeleteSite": "Standort löschen",
|
"actionDeleteSite": "Standort löschen",
|
||||||
"actionGetSite": "Standort abrufen",
|
"actionGetSite": "Standort abrufen",
|
||||||
"actionListSites": "Standorte auflisten",
|
"actionListSites": "Standorte auflisten",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "Setup-Token",
|
||||||
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
"setupTokenDescription": "Geben Sie das Setup-Token von der Serverkonsole ein.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "Setup-Token ist erforderlich",
|
||||||
"actionUpdateSite": "Standorte aktualisieren",
|
"actionUpdateSite": "Standorte aktualisieren",
|
||||||
"actionListSiteRoles": "Erlaubte Standort-Rollen auflisten",
|
"actionListSiteRoles": "Erlaubte Standort-Rollen auflisten",
|
||||||
"actionCreateResource": "Ressource erstellen",
|
"actionCreateResource": "Ressource erstellen",
|
||||||
@@ -1048,6 +1054,12 @@
|
|||||||
"actionUpdateClient": "Kunde aktualisieren",
|
"actionUpdateClient": "Kunde aktualisieren",
|
||||||
"actionListClients": "Kunden auflisten",
|
"actionListClients": "Kunden auflisten",
|
||||||
"actionGetClient": "Kunde holen",
|
"actionGetClient": "Kunde holen",
|
||||||
|
"actionCreateSiteResource": "Site-Ressource erstellen",
|
||||||
|
"actionDeleteSiteResource": "Site-Ressource löschen",
|
||||||
|
"actionGetSiteResource": "Site-Ressource abrufen",
|
||||||
|
"actionListSiteResources": "Site-Ressourcen auflisten",
|
||||||
|
"actionUpdateSiteResource": "Site-Ressource aktualisieren",
|
||||||
|
"actionListInvitations": "Einladungen auflisten",
|
||||||
"noneSelected": "Keine ausgewählt",
|
"noneSelected": "Keine ausgewählt",
|
||||||
"orgNotFound2": "Keine Organisationen gefunden.",
|
"orgNotFound2": "Keine Organisationen gefunden.",
|
||||||
"searchProgress": "Suche...",
|
"searchProgress": "Suche...",
|
||||||
@@ -1341,8 +1353,148 @@
|
|||||||
"olmErrorFetchLatest": "Beim Abrufen der neuesten Olm-Veröffentlichung ist ein Fehler aufgetreten.",
|
"olmErrorFetchLatest": "Beim Abrufen der neuesten Olm-Veröffentlichung ist ein Fehler aufgetreten.",
|
||||||
"remoteSubnets": "Remote-Subnetze",
|
"remoteSubnets": "Remote-Subnetze",
|
||||||
"enterCidrRange": "Geben Sie den CIDR-Bereich ein",
|
"enterCidrRange": "Geben Sie den CIDR-Bereich ein",
|
||||||
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
"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",
|
"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.",
|
"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"
|
"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.",
|
||||||
|
"managedSelfHosted": {
|
||||||
|
"title": "Verwaltetes Selbsthosted",
|
||||||
|
"description": "Zuverlässiger und wartungsarmer Pangolin Server mit zusätzlichen Glocken und Pfeifen",
|
||||||
|
"introTitle": "Verwalteter selbstgehosteter Pangolin",
|
||||||
|
"introDescription": "ist eine Deployment-Option, die für Personen konzipiert wurde, die Einfachheit und zusätzliche Zuverlässigkeit wünschen, während sie ihre Daten privat und selbstgehostet halten.",
|
||||||
|
"introDetail": "Mit dieser Option haben Sie immer noch Ihren eigenen Pangolin-Knoten – Ihre Tunnel, SSL-Terminierung und Traffic bleiben auf Ihrem Server. Der Unterschied besteht darin, dass Verwaltung und Überwachung über unser Cloud-Dashboard abgewickelt werden, das eine Reihe von Vorteilen freischaltet:",
|
||||||
|
"benefitSimplerOperations": {
|
||||||
|
"title": "Einfachere Operationen",
|
||||||
|
"description": "Sie brauchen keinen eigenen Mail-Server auszuführen oder komplexe Warnungen einzurichten. Sie erhalten Gesundheitschecks und Ausfallwarnungen aus dem Box."
|
||||||
|
},
|
||||||
|
"benefitAutomaticUpdates": {
|
||||||
|
"title": "Automatische Updates",
|
||||||
|
"description": "Das Cloud-Dashboard entwickelt sich schnell, so dass Sie neue Funktionen und Fehlerbehebungen erhalten, ohne jedes Mal neue Container manuell ziehen zu müssen."
|
||||||
|
},
|
||||||
|
"benefitLessMaintenance": {
|
||||||
|
"title": "Weniger Wartung",
|
||||||
|
"description": "Keine Datenbankmigrationen, Sicherungen oder zusätzliche Infrastruktur zum Verwalten. Wir kümmern uns um das in der Cloud."
|
||||||
|
},
|
||||||
|
"benefitCloudFailover": {
|
||||||
|
"title": "Cloud-Ausfall",
|
||||||
|
"description": "Wenn Ihr Knoten runtergeht, können Ihre Tunnel vorübergehend an unsere Cloud-Punkte scheitern, bis Sie ihn wieder online bringen."
|
||||||
|
},
|
||||||
|
"benefitHighAvailability": {
|
||||||
|
"title": "Hohe Verfügbarkeit (PoPs)",
|
||||||
|
"description": "Sie können auch mehrere Knoten an Ihr Konto anhängen, um Redundanz und bessere Leistung zu erzielen."
|
||||||
|
},
|
||||||
|
"benefitFutureEnhancements": {
|
||||||
|
"title": "Zukünftige Verbesserungen",
|
||||||
|
"description": "Wir planen weitere Analyse-, Alarm- und Management-Tools hinzuzufügen, um Ihren Einsatz noch robuster zu machen."
|
||||||
|
},
|
||||||
|
"docsAlert": {
|
||||||
|
"text": "Erfahren Sie mehr über die Managed Self-Hosted Option in unserer",
|
||||||
|
"documentation": "dokumentation"
|
||||||
|
},
|
||||||
|
"convertButton": "Diesen Knoten in Managed Self-Hosted umwandeln"
|
||||||
|
},
|
||||||
|
"internationaldomaindetected": "Internationale Domain erkannt",
|
||||||
|
"willbestoredas": "Wird gespeichert als:"
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,9 @@
|
|||||||
"siteNewtTunnelDescription": "Easiest way to create an entrypoint into your network. No extra setup.",
|
"siteNewtTunnelDescription": "Easiest way to create an entrypoint into your network. No extra setup.",
|
||||||
"siteWg": "Basic WireGuard",
|
"siteWg": "Basic WireGuard",
|
||||||
"siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
|
"siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
|
||||||
|
"siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES",
|
||||||
"siteLocalDescription": "Local resources only. No tunneling.",
|
"siteLocalDescription": "Local resources only. No tunneling.",
|
||||||
|
"siteLocalDescriptionSaas": "Local resources only. No tunneling. ONLY WORKS ON SELF HOSTED NODES",
|
||||||
"siteSeeAll": "See All Sites",
|
"siteSeeAll": "See All Sites",
|
||||||
"siteTunnelDescription": "Determine how you want to connect to your site",
|
"siteTunnelDescription": "Determine how you want to connect to your site",
|
||||||
"siteNewtCredentials": "Newt Credentials",
|
"siteNewtCredentials": "Newt Credentials",
|
||||||
@@ -203,6 +205,7 @@
|
|||||||
"resourceSetting": "{resourceName} Settings",
|
"resourceSetting": "{resourceName} Settings",
|
||||||
"alwaysAllow": "Always Allow",
|
"alwaysAllow": "Always Allow",
|
||||||
"alwaysDeny": "Always Deny",
|
"alwaysDeny": "Always Deny",
|
||||||
|
"passToAuth": "Pass to Auth",
|
||||||
"orgSettingsDescription": "Configure your organization's general settings",
|
"orgSettingsDescription": "Configure your organization's general settings",
|
||||||
"orgGeneralSettings": "Organization Settings",
|
"orgGeneralSettings": "Organization Settings",
|
||||||
"orgGeneralSettingsDescription": "Manage your organization details and configuration",
|
"orgGeneralSettingsDescription": "Manage your organization details and configuration",
|
||||||
@@ -543,6 +546,7 @@
|
|||||||
"rulesActions": "Actions",
|
"rulesActions": "Actions",
|
||||||
"rulesActionAlwaysAllow": "Always Allow: Bypass all authentication methods",
|
"rulesActionAlwaysAllow": "Always Allow: Bypass all authentication methods",
|
||||||
"rulesActionAlwaysDeny": "Always Deny: Block all requests; no authentication can be attempted",
|
"rulesActionAlwaysDeny": "Always Deny: Block all requests; no authentication can be attempted",
|
||||||
|
"rulesActionPassToAuth": "Pass to Auth: Allow authentication methods to be attempted",
|
||||||
"rulesMatchCriteria": "Matching Criteria",
|
"rulesMatchCriteria": "Matching Criteria",
|
||||||
"rulesMatchCriteriaIpAddress": "Match a specific IP address",
|
"rulesMatchCriteriaIpAddress": "Match a specific IP address",
|
||||||
"rulesMatchCriteriaIpAddressRange": "Match a range of IP addresses in CIDR notation",
|
"rulesMatchCriteriaIpAddressRange": "Match a range of IP addresses in CIDR notation",
|
||||||
@@ -971,6 +975,7 @@
|
|||||||
"logoutError": "Error logging out",
|
"logoutError": "Error logging out",
|
||||||
"signingAs": "Signed in as",
|
"signingAs": "Signed in as",
|
||||||
"serverAdmin": "Server Admin",
|
"serverAdmin": "Server Admin",
|
||||||
|
"managedSelfhosted": "Managed Self-Hosted",
|
||||||
"otpEnable": "Enable Two-factor",
|
"otpEnable": "Enable Two-factor",
|
||||||
"otpDisable": "Disable Two-factor",
|
"otpDisable": "Disable Two-factor",
|
||||||
"logout": "Log Out",
|
"logout": "Log Out",
|
||||||
@@ -1049,6 +1054,12 @@
|
|||||||
"actionUpdateClient": "Update Client",
|
"actionUpdateClient": "Update Client",
|
||||||
"actionListClients": "List Clients",
|
"actionListClients": "List Clients",
|
||||||
"actionGetClient": "Get Client",
|
"actionGetClient": "Get Client",
|
||||||
|
"actionCreateSiteResource": "Create Site Resource",
|
||||||
|
"actionDeleteSiteResource": "Delete Site Resource",
|
||||||
|
"actionGetSiteResource": "Get Site Resource",
|
||||||
|
"actionListSiteResources": "List Site Resources",
|
||||||
|
"actionUpdateSiteResource": "Update Site Resource",
|
||||||
|
"actionListInvitations": "List Invitations",
|
||||||
"noneSelected": "None selected",
|
"noneSelected": "None selected",
|
||||||
"orgNotFound2": "No organizations found.",
|
"orgNotFound2": "No organizations found.",
|
||||||
"searchProgress": "Search...",
|
"searchProgress": "Search...",
|
||||||
@@ -1435,7 +1446,7 @@
|
|||||||
"siteAcceptClientConnections": "Accept Client Connections",
|
"siteAcceptClientConnections": "Accept Client Connections",
|
||||||
"siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.",
|
"siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.",
|
||||||
"siteAddress": "Site Address",
|
"siteAddress": "Site Address",
|
||||||
"siteAddressDescription": "Specify the IP address of the host for clients to connect to.",
|
"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",
|
"autoLoginExternalIdp": "Auto Login with External IDP",
|
||||||
"autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.",
|
"autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.",
|
||||||
"selectIdp": "Select IDP",
|
"selectIdp": "Select IDP",
|
||||||
@@ -1447,5 +1458,43 @@
|
|||||||
"autoLoginRedirecting": "Redirecting to login...",
|
"autoLoginRedirecting": "Redirecting to login...",
|
||||||
"autoLoginError": "Auto Login Error",
|
"autoLoginError": "Auto Login Error",
|
||||||
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
||||||
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL."
|
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
|
||||||
|
"managedSelfHosted": {
|
||||||
|
"title": "Managed Self-Hosted",
|
||||||
|
"description": "More reliable and low-maintenance self-hosted Pangolin server with extra bells and whistles",
|
||||||
|
"introTitle": "Managed Self-Hosted Pangolin",
|
||||||
|
"introDescription": "is a deployment option designed for people who want simplicity and extra reliability while still keeping their data private and self-hosted.",
|
||||||
|
"introDetail": "With this option, you still run your own Pangolin node — your tunnels, SSL termination, and traffic all stay on your server. The difference is that management and monitoring are handled through our cloud dashboard, which unlocks a number of benefits:",
|
||||||
|
"benefitSimplerOperations": {
|
||||||
|
"title": "Simpler operations",
|
||||||
|
"description": "No need to run your own mail server or set up complex alerting. You'll get health checks and downtime alerts out of the box."
|
||||||
|
},
|
||||||
|
"benefitAutomaticUpdates": {
|
||||||
|
"title": "Automatic updates",
|
||||||
|
"description": "The cloud dashboard evolves quickly, so you get new features and bug fixes without having to manually pull new containers every time."
|
||||||
|
},
|
||||||
|
"benefitLessMaintenance": {
|
||||||
|
"title": "Less maintenance",
|
||||||
|
"description": "No database migrations, backups, or extra infrastructure to manage. We handle that in the cloud."
|
||||||
|
},
|
||||||
|
"benefitCloudFailover": {
|
||||||
|
"title": "Cloud failover",
|
||||||
|
"description": "If your node goes down, your tunnels can temporarily fail over to our cloud points of presence until you bring it back online."
|
||||||
|
},
|
||||||
|
"benefitHighAvailability": {
|
||||||
|
"title": "High availability (PoPs)",
|
||||||
|
"description": "You can also attach multiple nodes to your account for redundancy and better performance."
|
||||||
|
},
|
||||||
|
"benefitFutureEnhancements": {
|
||||||
|
"title": "Future enhancements",
|
||||||
|
"description": "We're planning to add more analytics, alerting, and management tools to make your deployment even more robust."
|
||||||
|
},
|
||||||
|
"docsAlert": {
|
||||||
|
"text": "Learn more about the Managed Self-Hosted option in our",
|
||||||
|
"documentation": "documentation"
|
||||||
|
},
|
||||||
|
"convertButton": "Convert This Node to Managed Self-Hosted"
|
||||||
|
},
|
||||||
|
"internationaldomaindetected": "International Domain Detected",
|
||||||
|
"willbestoredas": "Will be stored as:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,9 @@
|
|||||||
"siteNewtTunnelDescription": "La forma más fácil de crear un punto de entrada en tu red. Sin configuración adicional.",
|
"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",
|
"siteWg": "Wirex Guardia Básica",
|
||||||
"siteWgDescription": "Utilice cualquier cliente Wirex Guard para establecer un túnel. Se requiere una configuración manual de NAT.",
|
"siteWgDescription": "Utilice cualquier cliente Wirex Guard para establecer un túnel. Se requiere una configuración manual de NAT.",
|
||||||
|
"siteWgDescriptionSaas": "Utilice cualquier cliente de WireGuard para establecer un túnel. Se requiere configuración manual de NAT. SOLO FUNCIONA EN NODOS AUTOGESTIONADOS",
|
||||||
"siteLocalDescription": "Solo recursos locales. Sin túneles.",
|
"siteLocalDescription": "Solo recursos locales. Sin túneles.",
|
||||||
|
"siteLocalDescriptionSaas": "Solo recursos locales. Sin túneles. SOLO FUNCIONA EN NODOS AUTOGESTIONADOS",
|
||||||
"siteSeeAll": "Ver todos los sitios",
|
"siteSeeAll": "Ver todos los sitios",
|
||||||
"siteTunnelDescription": "Determina cómo quieres conectarte a tu sitio",
|
"siteTunnelDescription": "Determina cómo quieres conectarte a tu sitio",
|
||||||
"siteNewtCredentials": "Credenciales nuevas",
|
"siteNewtCredentials": "Credenciales nuevas",
|
||||||
@@ -166,7 +168,7 @@
|
|||||||
"siteSelect": "Seleccionar sitio",
|
"siteSelect": "Seleccionar sitio",
|
||||||
"siteSearch": "Buscar sitio",
|
"siteSearch": "Buscar sitio",
|
||||||
"siteNotFound": "Sitio no encontrado.",
|
"siteNotFound": "Sitio no encontrado.",
|
||||||
"siteSelectionDescription": "Este sitio proporcionará conectividad al recurso.",
|
"siteSelectionDescription": "Este sitio proporcionará conectividad al objetivo.",
|
||||||
"resourceType": "Tipo de recurso",
|
"resourceType": "Tipo de recurso",
|
||||||
"resourceTypeDescription": "Determina cómo quieres acceder a tu recurso",
|
"resourceTypeDescription": "Determina cómo quieres acceder a tu recurso",
|
||||||
"resourceHTTPSSettings": "Configuración HTTPS",
|
"resourceHTTPSSettings": "Configuración HTTPS",
|
||||||
@@ -197,11 +199,13 @@
|
|||||||
"general": "General",
|
"general": "General",
|
||||||
"generalSettings": "Configuración General",
|
"generalSettings": "Configuración General",
|
||||||
"proxy": "Proxy",
|
"proxy": "Proxy",
|
||||||
|
"internal": "Interno",
|
||||||
"rules": "Reglas",
|
"rules": "Reglas",
|
||||||
"resourceSettingDescription": "Configure la configuración de su recurso",
|
"resourceSettingDescription": "Configure la configuración de su recurso",
|
||||||
"resourceSetting": "Ajustes {resourceName}",
|
"resourceSetting": "Ajustes {resourceName}",
|
||||||
"alwaysAllow": "Permitir siempre",
|
"alwaysAllow": "Permitir siempre",
|
||||||
"alwaysDeny": "Denegar siempre",
|
"alwaysDeny": "Denegar siempre",
|
||||||
|
"passToAuth": "Pasar a Autenticación",
|
||||||
"orgSettingsDescription": "Configurar la configuración general de su organización",
|
"orgSettingsDescription": "Configurar la configuración general de su organización",
|
||||||
"orgGeneralSettings": "Configuración de la organización",
|
"orgGeneralSettings": "Configuración de la organización",
|
||||||
"orgGeneralSettingsDescription": "Administra los detalles y la configuración de tu organización",
|
"orgGeneralSettingsDescription": "Administra los detalles y la configuración de tu organización",
|
||||||
@@ -542,6 +546,7 @@
|
|||||||
"rulesActions": "Acciones",
|
"rulesActions": "Acciones",
|
||||||
"rulesActionAlwaysAllow": "Permitir siempre: pasar todos los métodos de autenticación",
|
"rulesActionAlwaysAllow": "Permitir siempre: pasar todos los métodos de autenticación",
|
||||||
"rulesActionAlwaysDeny": "Denegar siempre: Bloquear todas las peticiones; no se puede intentar autenticación",
|
"rulesActionAlwaysDeny": "Denegar siempre: Bloquear todas las peticiones; no se puede intentar autenticación",
|
||||||
|
"rulesActionPassToAuth": "Pasar a Autenticación: Permitir que se intenten los métodos de autenticación",
|
||||||
"rulesMatchCriteria": "Criterios coincidentes",
|
"rulesMatchCriteria": "Criterios coincidentes",
|
||||||
"rulesMatchCriteriaIpAddress": "Coincidir con una dirección IP específica",
|
"rulesMatchCriteriaIpAddress": "Coincidir con una dirección IP específica",
|
||||||
"rulesMatchCriteriaIpAddressRange": "Coincide con un rango de direcciones IP en notación CIDR",
|
"rulesMatchCriteriaIpAddressRange": "Coincide con un rango de direcciones IP en notación CIDR",
|
||||||
@@ -833,24 +838,24 @@
|
|||||||
"pincodeRequirementsLength": "El PIN debe tener exactamente 6 dígitos",
|
"pincodeRequirementsLength": "El PIN debe tener exactamente 6 dígitos",
|
||||||
"pincodeRequirementsChars": "El PIN sólo debe contener números",
|
"pincodeRequirementsChars": "El PIN sólo debe contener números",
|
||||||
"passwordRequirementsLength": "La contraseña debe tener al menos 1 carácter",
|
"passwordRequirementsLength": "La contraseña debe tener al menos 1 carácter",
|
||||||
"passwordRequirementsTitle": "Password requirements:",
|
"passwordRequirementsTitle": "Requisitos de la contraseña:",
|
||||||
"passwordRequirementLength": "At least 8 characters long",
|
"passwordRequirementLength": "Al menos 8 caracteres de largo",
|
||||||
"passwordRequirementUppercase": "At least one uppercase letter",
|
"passwordRequirementUppercase": "Al menos una letra mayúscula",
|
||||||
"passwordRequirementLowercase": "At least one lowercase letter",
|
"passwordRequirementLowercase": "Al menos una letra minúscula",
|
||||||
"passwordRequirementNumber": "At least one number",
|
"passwordRequirementNumber": "Al menos un número",
|
||||||
"passwordRequirementSpecial": "At least one special character",
|
"passwordRequirementSpecial": "Al menos un carácter especial",
|
||||||
"passwordRequirementsMet": "✓ Password meets all requirements",
|
"passwordRequirementsMet": "✓ La contraseña cumple con todos los requisitos",
|
||||||
"passwordStrength": "Password strength",
|
"passwordStrength": "Seguridad de la contraseña",
|
||||||
"passwordStrengthWeak": "Weak",
|
"passwordStrengthWeak": "Débil",
|
||||||
"passwordStrengthMedium": "Medium",
|
"passwordStrengthMedium": "Media",
|
||||||
"passwordStrengthStrong": "Strong",
|
"passwordStrengthStrong": "Fuerte",
|
||||||
"passwordRequirements": "Requirements:",
|
"passwordRequirements": "Requisitos:",
|
||||||
"passwordRequirementLengthText": "8+ characters",
|
"passwordRequirementLengthText": "8+ caracteres",
|
||||||
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
"passwordRequirementUppercaseText": "Letra mayúscula (A-Z)",
|
||||||
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
"passwordRequirementLowercaseText": "Letra minúscula (a-z)",
|
||||||
"passwordRequirementNumberText": "Number (0-9)",
|
"passwordRequirementNumberText": "Número (0-9)",
|
||||||
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
"passwordRequirementSpecialText": "Caracter especial (!@#$%...)",
|
||||||
"passwordsDoNotMatch": "Passwords do not match",
|
"passwordsDoNotMatch": "Las contraseñas no coinciden",
|
||||||
"otpEmailRequirementsLength": "OTP debe tener al menos 1 carácter",
|
"otpEmailRequirementsLength": "OTP debe tener al menos 1 carácter",
|
||||||
"otpEmailSent": "OTP enviado",
|
"otpEmailSent": "OTP enviado",
|
||||||
"otpEmailSentDescription": "Un OTP ha sido enviado a tu correo electrónico",
|
"otpEmailSentDescription": "Un OTP ha sido enviado a tu correo electrónico",
|
||||||
@@ -970,6 +975,7 @@
|
|||||||
"logoutError": "Error al cerrar sesión",
|
"logoutError": "Error al cerrar sesión",
|
||||||
"signingAs": "Conectado como",
|
"signingAs": "Conectado como",
|
||||||
"serverAdmin": "Admin Servidor",
|
"serverAdmin": "Admin Servidor",
|
||||||
|
"managedSelfhosted": "Autogestionado",
|
||||||
"otpEnable": "Activar doble factor",
|
"otpEnable": "Activar doble factor",
|
||||||
"otpDisable": "Desactivar doble factor",
|
"otpDisable": "Desactivar doble factor",
|
||||||
"logout": "Cerrar sesión",
|
"logout": "Cerrar sesión",
|
||||||
@@ -985,9 +991,9 @@
|
|||||||
"actionDeleteSite": "Eliminar sitio",
|
"actionDeleteSite": "Eliminar sitio",
|
||||||
"actionGetSite": "Obtener sitio",
|
"actionGetSite": "Obtener sitio",
|
||||||
"actionListSites": "Listar sitios",
|
"actionListSites": "Listar sitios",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "Configuración de token",
|
||||||
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
"setupTokenDescription": "Ingrese el token de configuración desde la consola del servidor.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "Se requiere el token de configuración",
|
||||||
"actionUpdateSite": "Actualizar sitio",
|
"actionUpdateSite": "Actualizar sitio",
|
||||||
"actionListSiteRoles": "Lista de roles permitidos del sitio",
|
"actionListSiteRoles": "Lista de roles permitidos del sitio",
|
||||||
"actionCreateResource": "Crear Recurso",
|
"actionCreateResource": "Crear Recurso",
|
||||||
@@ -1048,6 +1054,12 @@
|
|||||||
"actionUpdateClient": "Actualizar cliente",
|
"actionUpdateClient": "Actualizar cliente",
|
||||||
"actionListClients": "Listar clientes",
|
"actionListClients": "Listar clientes",
|
||||||
"actionGetClient": "Obtener cliente",
|
"actionGetClient": "Obtener cliente",
|
||||||
|
"actionCreateSiteResource": "Crear Recurso del Sitio",
|
||||||
|
"actionDeleteSiteResource": "Eliminar recurso del sitio",
|
||||||
|
"actionGetSiteResource": "Obtener recurso del sitio",
|
||||||
|
"actionListSiteResources": "Listar recursos del sitio",
|
||||||
|
"actionUpdateSiteResource": "Actualizar recurso del sitio",
|
||||||
|
"actionListInvitations": "Listar invitaciones",
|
||||||
"noneSelected": "Ninguno seleccionado",
|
"noneSelected": "Ninguno seleccionado",
|
||||||
"orgNotFound2": "No se encontraron organizaciones.",
|
"orgNotFound2": "No se encontraron organizaciones.",
|
||||||
"searchProgress": "Buscar...",
|
"searchProgress": "Buscar...",
|
||||||
@@ -1341,8 +1353,148 @@
|
|||||||
"olmErrorFetchLatest": "Se ha producido un error al recuperar la última versión de Olm.",
|
"olmErrorFetchLatest": "Se ha producido un error al recuperar la última versión de Olm.",
|
||||||
"remoteSubnets": "Subredes remotas",
|
"remoteSubnets": "Subredes remotas",
|
||||||
"enterCidrRange": "Ingresa el rango CIDR",
|
"enterCidrRange": "Ingresa el rango CIDR",
|
||||||
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
"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",
|
"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.",
|
"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"
|
"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.",
|
||||||
|
"managedSelfHosted": {
|
||||||
|
"title": "Autogestionado",
|
||||||
|
"description": "Servidor Pangolin autoalojado más fiable y de bajo mantenimiento con campanas y silbidos extra",
|
||||||
|
"introTitle": "Pangolin autogestionado",
|
||||||
|
"introDescription": "es una opción de despliegue diseñada para personas que quieren simplicidad y fiabilidad extra mientras mantienen sus datos privados y autoalojados.",
|
||||||
|
"introDetail": "Con esta opción, todavía ejecuta su propio nodo Pangolin, sus túneles, terminación SSL y tráfico permanecen en su servidor. La diferencia es que la gestión y el control se gestionan a través de nuestro panel de control en la nube, que desbloquea una serie de ventajas:",
|
||||||
|
"benefitSimplerOperations": {
|
||||||
|
"title": "Operaciones simples",
|
||||||
|
"description": "No necesitas ejecutar tu propio servidor de correo o configurar alertas complejas. Recibirás cheques de salud y alertas de tiempo de inactividad."
|
||||||
|
},
|
||||||
|
"benefitAutomaticUpdates": {
|
||||||
|
"title": "Actualizaciones automáticas",
|
||||||
|
"description": "El tablero de la nube evolucionará rápidamente, por lo que obtendrá nuevas características y correcciones de errores sin tener que extraer manualmente nuevos contenedores cada vez."
|
||||||
|
},
|
||||||
|
"benefitLessMaintenance": {
|
||||||
|
"title": "Menos mantenimiento",
|
||||||
|
"description": "No hay migraciones de base de datos, copias de seguridad o infraestructura extra para administrar. Lo manejamos en la nube."
|
||||||
|
},
|
||||||
|
"benefitCloudFailover": {
|
||||||
|
"title": "Fallo en la nube",
|
||||||
|
"description": "Si tu nodo cae, tus túneles pueden fallar temporalmente a nuestros puntos de presencia en la nube hasta que lo vuelvas a conectar."
|
||||||
|
},
|
||||||
|
"benefitHighAvailability": {
|
||||||
|
"title": "Alta disponibilidad (PoPs)",
|
||||||
|
"description": "También puede adjuntar múltiples nodos a su cuenta para redundancia y mejor rendimiento."
|
||||||
|
},
|
||||||
|
"benefitFutureEnhancements": {
|
||||||
|
"title": "Mejoras futuras",
|
||||||
|
"description": "Estamos planeando añadir más herramientas analíticas, alertas y de administración para hacer su despliegue aún más robusto."
|
||||||
|
},
|
||||||
|
"docsAlert": {
|
||||||
|
"text": "Aprenda más acerca de la opción de autoalojamiento administrado en nuestra",
|
||||||
|
"documentation": "documentación"
|
||||||
|
},
|
||||||
|
"convertButton": "Convierte este nodo a autoalojado administrado"
|
||||||
|
},
|
||||||
|
"internationaldomaindetected": "Dominio Internacional detectado",
|
||||||
|
"willbestoredas": "Se almacenará como:"
|
||||||
}
|
}
|
||||||
@@ -94,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.",
|
"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",
|
"siteWg": "WireGuard basique",
|
||||||
"siteWgDescription": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise.",
|
"siteWgDescription": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise.",
|
||||||
|
"siteWgDescriptionSaas": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES",
|
||||||
"siteLocalDescription": "Ressources locales seulement. Pas de tunneling.",
|
"siteLocalDescription": "Ressources locales seulement. Pas de tunneling.",
|
||||||
|
"siteLocalDescriptionSaas": "Ressources locales uniquement. Pas de tunneling. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES",
|
||||||
"siteSeeAll": "Voir tous les sites",
|
"siteSeeAll": "Voir tous les sites",
|
||||||
"siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre site",
|
"siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre site",
|
||||||
"siteNewtCredentials": "Identifiants Newt",
|
"siteNewtCredentials": "Identifiants Newt",
|
||||||
@@ -166,7 +168,7 @@
|
|||||||
"siteSelect": "Sélectionner un site",
|
"siteSelect": "Sélectionner un site",
|
||||||
"siteSearch": "Chercher un site",
|
"siteSearch": "Chercher un site",
|
||||||
"siteNotFound": "Aucun site trouvé.",
|
"siteNotFound": "Aucun site trouvé.",
|
||||||
"siteSelectionDescription": "Ce site fournira la connectivité à la ressource.",
|
"siteSelectionDescription": "Ce site fournira la connectivité à la cible.",
|
||||||
"resourceType": "Type de ressource",
|
"resourceType": "Type de ressource",
|
||||||
"resourceTypeDescription": "Déterminer comment vous voulez accéder à votre ressource",
|
"resourceTypeDescription": "Déterminer comment vous voulez accéder à votre ressource",
|
||||||
"resourceHTTPSSettings": "Paramètres HTTPS",
|
"resourceHTTPSSettings": "Paramètres HTTPS",
|
||||||
@@ -197,11 +199,13 @@
|
|||||||
"general": "Généraux",
|
"general": "Généraux",
|
||||||
"generalSettings": "Paramètres généraux",
|
"generalSettings": "Paramètres généraux",
|
||||||
"proxy": "Proxy",
|
"proxy": "Proxy",
|
||||||
|
"internal": "Interne",
|
||||||
"rules": "Règles",
|
"rules": "Règles",
|
||||||
"resourceSettingDescription": "Configurer les paramètres de votre ressource",
|
"resourceSettingDescription": "Configurer les paramètres de votre ressource",
|
||||||
"resourceSetting": "Réglages {resourceName}",
|
"resourceSetting": "Réglages {resourceName}",
|
||||||
"alwaysAllow": "Toujours autoriser",
|
"alwaysAllow": "Toujours autoriser",
|
||||||
"alwaysDeny": "Toujours refuser",
|
"alwaysDeny": "Toujours refuser",
|
||||||
|
"passToAuth": "Paser à l'authentification",
|
||||||
"orgSettingsDescription": "Configurer les paramètres généraux de votre organisation",
|
"orgSettingsDescription": "Configurer les paramètres généraux de votre organisation",
|
||||||
"orgGeneralSettings": "Paramètres de l'organisation",
|
"orgGeneralSettings": "Paramètres de l'organisation",
|
||||||
"orgGeneralSettingsDescription": "Gérer les détails et la configuration de votre organisation",
|
"orgGeneralSettingsDescription": "Gérer les détails et la configuration de votre organisation",
|
||||||
@@ -490,7 +494,7 @@
|
|||||||
"targetTlsSniDescription": "Le nom de serveur TLS à utiliser pour SNI. Laissez vide pour utiliser la valeur par défaut.",
|
"targetTlsSniDescription": "Le nom de serveur TLS à utiliser pour SNI. Laissez vide pour utiliser la valeur par défaut.",
|
||||||
"targetTlsSubmit": "Enregistrer les paramètres",
|
"targetTlsSubmit": "Enregistrer les paramètres",
|
||||||
"targets": "Configuration des cibles",
|
"targets": "Configuration des cibles",
|
||||||
"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",
|
"targetStickySessions": "Activer les sessions persistantes",
|
||||||
"targetStickySessionsDescription": "Maintenir les connexions sur la même cible backend pendant toute leur session.",
|
"targetStickySessionsDescription": "Maintenir les connexions sur la même cible backend pendant toute leur session.",
|
||||||
"methodSelect": "Sélectionner la méthode",
|
"methodSelect": "Sélectionner la méthode",
|
||||||
@@ -542,6 +546,7 @@
|
|||||||
"rulesActions": "Actions",
|
"rulesActions": "Actions",
|
||||||
"rulesActionAlwaysAllow": "Toujours autoriser : Contourner toutes les méthodes d'authentification",
|
"rulesActionAlwaysAllow": "Toujours autoriser : Contourner toutes les méthodes d'authentification",
|
||||||
"rulesActionAlwaysDeny": "Toujours refuser : Bloquer toutes les requêtes ; aucune authentification ne peut être tentée",
|
"rulesActionAlwaysDeny": "Toujours refuser : Bloquer toutes les requêtes ; aucune authentification ne peut être tentée",
|
||||||
|
"rulesActionPassToAuth": "Passer à l'authentification : Autoriser les méthodes d'authentification à être tentées",
|
||||||
"rulesMatchCriteria": "Critères de correspondance",
|
"rulesMatchCriteria": "Critères de correspondance",
|
||||||
"rulesMatchCriteriaIpAddress": "Correspondre à une adresse IP spécifique",
|
"rulesMatchCriteriaIpAddress": "Correspondre à une adresse IP spécifique",
|
||||||
"rulesMatchCriteriaIpAddressRange": "Correspondre à une plage d'adresses IP en notation CIDR",
|
"rulesMatchCriteriaIpAddressRange": "Correspondre à une plage d'adresses IP en notation CIDR",
|
||||||
@@ -833,24 +838,24 @@
|
|||||||
"pincodeRequirementsLength": "Le code PIN doit comporter exactement 6 chiffres",
|
"pincodeRequirementsLength": "Le code PIN doit comporter exactement 6 chiffres",
|
||||||
"pincodeRequirementsChars": "Le code PIN ne doit contenir que des chiffres",
|
"pincodeRequirementsChars": "Le code PIN ne doit contenir que des chiffres",
|
||||||
"passwordRequirementsLength": "Le mot de passe doit comporter au moins 1 caractère",
|
"passwordRequirementsLength": "Le mot de passe doit comporter au moins 1 caractère",
|
||||||
"passwordRequirementsTitle": "Password requirements:",
|
"passwordRequirementsTitle": "Exigences relatives au mot de passe :",
|
||||||
"passwordRequirementLength": "At least 8 characters long",
|
"passwordRequirementLength": "Au moins 8 caractères",
|
||||||
"passwordRequirementUppercase": "At least one uppercase letter",
|
"passwordRequirementUppercase": "Au moins une lettre majuscule",
|
||||||
"passwordRequirementLowercase": "At least one lowercase letter",
|
"passwordRequirementLowercase": "Au moins une lettre minuscule",
|
||||||
"passwordRequirementNumber": "At least one number",
|
"passwordRequirementNumber": "Au moins un chiffre",
|
||||||
"passwordRequirementSpecial": "At least one special character",
|
"passwordRequirementSpecial": "Au moins un caractère spécial",
|
||||||
"passwordRequirementsMet": "✓ Password meets all requirements",
|
"passwordRequirementsMet": "✓ Le mot de passe répond à toutes les exigences",
|
||||||
"passwordStrength": "Password strength",
|
"passwordStrength": "Solidité du mot de passe",
|
||||||
"passwordStrengthWeak": "Weak",
|
"passwordStrengthWeak": "Faible",
|
||||||
"passwordStrengthMedium": "Medium",
|
"passwordStrengthMedium": "Moyen",
|
||||||
"passwordStrengthStrong": "Strong",
|
"passwordStrengthStrong": "Fort",
|
||||||
"passwordRequirements": "Requirements:",
|
"passwordRequirements": "Exigences :",
|
||||||
"passwordRequirementLengthText": "8+ characters",
|
"passwordRequirementLengthText": "8+ caractères",
|
||||||
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
"passwordRequirementUppercaseText": "Lettre majuscule (A-Z)",
|
||||||
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
"passwordRequirementLowercaseText": "Lettre minuscule (a-z)",
|
||||||
"passwordRequirementNumberText": "Number (0-9)",
|
"passwordRequirementNumberText": "Nombre (0-9)",
|
||||||
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
"passwordRequirementSpecialText": "Caractère spécial (!@#$%...)",
|
||||||
"passwordsDoNotMatch": "Passwords do not match",
|
"passwordsDoNotMatch": "Les mots de passe ne correspondent pas",
|
||||||
"otpEmailRequirementsLength": "L'OTP doit comporter au moins 1 caractère",
|
"otpEmailRequirementsLength": "L'OTP doit comporter au moins 1 caractère",
|
||||||
"otpEmailSent": "OTP envoyé",
|
"otpEmailSent": "OTP envoyé",
|
||||||
"otpEmailSentDescription": "Un OTP a été envoyé à votre e-mail",
|
"otpEmailSentDescription": "Un OTP a été envoyé à votre e-mail",
|
||||||
@@ -970,6 +975,7 @@
|
|||||||
"logoutError": "Erreur lors de la déconnexion",
|
"logoutError": "Erreur lors de la déconnexion",
|
||||||
"signingAs": "Connecté en tant que",
|
"signingAs": "Connecté en tant que",
|
||||||
"serverAdmin": "Admin Serveur",
|
"serverAdmin": "Admin Serveur",
|
||||||
|
"managedSelfhosted": "Gestion autonome",
|
||||||
"otpEnable": "Activer l'authentification à deux facteurs",
|
"otpEnable": "Activer l'authentification à deux facteurs",
|
||||||
"otpDisable": "Désactiver l'authentification à deux facteurs",
|
"otpDisable": "Désactiver l'authentification à deux facteurs",
|
||||||
"logout": "Déconnexion",
|
"logout": "Déconnexion",
|
||||||
@@ -985,9 +991,9 @@
|
|||||||
"actionDeleteSite": "Supprimer un site",
|
"actionDeleteSite": "Supprimer un site",
|
||||||
"actionGetSite": "Obtenir un site",
|
"actionGetSite": "Obtenir un site",
|
||||||
"actionListSites": "Lister les sites",
|
"actionListSites": "Lister les sites",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "Jeton de configuration",
|
||||||
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
"setupTokenDescription": "Entrez le jeton de configuration depuis la console du serveur.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "Le jeton de configuration est requis.",
|
||||||
"actionUpdateSite": "Mettre à jour un site",
|
"actionUpdateSite": "Mettre à jour un site",
|
||||||
"actionListSiteRoles": "Lister les rôles autorisés du site",
|
"actionListSiteRoles": "Lister les rôles autorisés du site",
|
||||||
"actionCreateResource": "Créer une ressource",
|
"actionCreateResource": "Créer une ressource",
|
||||||
@@ -1048,6 +1054,12 @@
|
|||||||
"actionUpdateClient": "Mettre à jour le client",
|
"actionUpdateClient": "Mettre à jour le client",
|
||||||
"actionListClients": "Liste des clients",
|
"actionListClients": "Liste des clients",
|
||||||
"actionGetClient": "Obtenir le client",
|
"actionGetClient": "Obtenir le client",
|
||||||
|
"actionCreateSiteResource": "Créer une ressource de site",
|
||||||
|
"actionDeleteSiteResource": "Supprimer une ressource de site",
|
||||||
|
"actionGetSiteResource": "Obtenir une ressource de site",
|
||||||
|
"actionListSiteResources": "Lister les ressources de site",
|
||||||
|
"actionUpdateSiteResource": "Mettre à jour une ressource de site",
|
||||||
|
"actionListInvitations": "Lister les invitations",
|
||||||
"noneSelected": "Aucune sélection",
|
"noneSelected": "Aucune sélection",
|
||||||
"orgNotFound2": "Aucune organisation trouvée.",
|
"orgNotFound2": "Aucune organisation trouvée.",
|
||||||
"searchProgress": "Rechercher...",
|
"searchProgress": "Rechercher...",
|
||||||
@@ -1341,8 +1353,148 @@
|
|||||||
"olmErrorFetchLatest": "Une erreur s'est produite lors de la récupération de la dernière version 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",
|
"remoteSubnets": "Sous-réseaux distants",
|
||||||
"enterCidrRange": "Entrez la plage CIDR",
|
"enterCidrRange": "Entrez la plage CIDR",
|
||||||
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
"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",
|
"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.",
|
"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é"
|
"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.",
|
||||||
|
"managedSelfHosted": {
|
||||||
|
"title": "Gestion autonome",
|
||||||
|
"description": "Serveur Pangolin auto-hébergé avec des cloches et des sifflets supplémentaires",
|
||||||
|
"introTitle": "Pangolin auto-hébergé géré",
|
||||||
|
"introDescription": "est une option de déploiement conçue pour les personnes qui veulent de la simplicité et de la fiabilité tout en gardant leurs données privées et auto-hébergées.",
|
||||||
|
"introDetail": "Avec cette option, vous exécutez toujours votre propre nœud Pangolin — vos tunnels, la terminaison SSL et le trafic restent sur votre serveur. La différence est que la gestion et la surveillance sont gérées via notre tableau de bord du cloud, qui déverrouille un certain nombre d'avantages :",
|
||||||
|
"benefitSimplerOperations": {
|
||||||
|
"title": "Opérations plus simples",
|
||||||
|
"description": "Pas besoin de faire tourner votre propre serveur de messagerie ou de configurer des alertes complexes. Vous obtiendrez des contrôles de santé et des alertes de temps d'arrêt par la suite."
|
||||||
|
},
|
||||||
|
"benefitAutomaticUpdates": {
|
||||||
|
"title": "Mises à jour automatiques",
|
||||||
|
"description": "Le tableau de bord du cloud évolue rapidement, de sorte que vous obtenez de nouvelles fonctionnalités et des corrections de bugs sans avoir à extraire manuellement de nouveaux conteneurs à chaque fois."
|
||||||
|
},
|
||||||
|
"benefitLessMaintenance": {
|
||||||
|
"title": "Moins de maintenance",
|
||||||
|
"description": "Aucune migration de base de données, sauvegarde ou infrastructure supplémentaire à gérer. Nous gérons cela dans le cloud."
|
||||||
|
},
|
||||||
|
"benefitCloudFailover": {
|
||||||
|
"title": "Basculement du Cloud",
|
||||||
|
"description": "Si votre nœud descend, vos tunnels peuvent temporairement échouer jusqu'à ce que vous le rapatriez en ligne."
|
||||||
|
},
|
||||||
|
"benefitHighAvailability": {
|
||||||
|
"title": "Haute disponibilité (PoPs)",
|
||||||
|
"description": "Vous pouvez également attacher plusieurs nœuds à votre compte pour une redondance et de meilleures performances."
|
||||||
|
},
|
||||||
|
"benefitFutureEnhancements": {
|
||||||
|
"title": "Améliorations futures",
|
||||||
|
"description": "Nous prévoyons d'ajouter plus d'outils d'analyse, d'alerte et de gestion pour rendre votre déploiement encore plus robuste."
|
||||||
|
},
|
||||||
|
"docsAlert": {
|
||||||
|
"text": "En savoir plus sur l'option Auto-Hébergement géré dans notre",
|
||||||
|
"documentation": "documentation"
|
||||||
|
},
|
||||||
|
"convertButton": "Convertir ce noeud en auto-hébergé géré"
|
||||||
|
},
|
||||||
|
"internationaldomaindetected": "Domaine international détecté",
|
||||||
|
"willbestoredas": "Sera stocké comme :"
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,9 @@
|
|||||||
"siteNewtTunnelDescription": "Modo più semplice per creare un entrypoint nella rete. Nessuna configurazione aggiuntiva.",
|
"siteNewtTunnelDescription": "Modo più semplice per creare un entrypoint nella rete. Nessuna configurazione aggiuntiva.",
|
||||||
"siteWg": "WireGuard Base",
|
"siteWg": "WireGuard Base",
|
||||||
"siteWgDescription": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.",
|
"siteWgDescription": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.",
|
||||||
|
"siteWgDescriptionSaas": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta. FUNZIONA SOLO SU NODI AUTO-OSPITATI",
|
||||||
"siteLocalDescription": "Solo risorse locali. Nessun tunneling.",
|
"siteLocalDescription": "Solo risorse locali. Nessun tunneling.",
|
||||||
|
"siteLocalDescriptionSaas": "Solo risorse locali. Nessun tunneling. FUNZIONA SOLO SU NODI AUTO-OSPITATI",
|
||||||
"siteSeeAll": "Vedi Tutti I Siti",
|
"siteSeeAll": "Vedi Tutti I Siti",
|
||||||
"siteTunnelDescription": "Determina come vuoi connetterti al tuo sito",
|
"siteTunnelDescription": "Determina come vuoi connetterti al tuo sito",
|
||||||
"siteNewtCredentials": "Credenziali Newt",
|
"siteNewtCredentials": "Credenziali Newt",
|
||||||
@@ -166,7 +168,7 @@
|
|||||||
"siteSelect": "Seleziona sito",
|
"siteSelect": "Seleziona sito",
|
||||||
"siteSearch": "Cerca sito",
|
"siteSearch": "Cerca sito",
|
||||||
"siteNotFound": "Nessun sito trovato.",
|
"siteNotFound": "Nessun sito trovato.",
|
||||||
"siteSelectionDescription": "Questo sito fornirà connettività alla risorsa.",
|
"siteSelectionDescription": "Questo sito fornirà connettività all'obiettivo.",
|
||||||
"resourceType": "Tipo Di Risorsa",
|
"resourceType": "Tipo Di Risorsa",
|
||||||
"resourceTypeDescription": "Determina come vuoi accedere alla tua risorsa",
|
"resourceTypeDescription": "Determina come vuoi accedere alla tua risorsa",
|
||||||
"resourceHTTPSSettings": "Impostazioni HTTPS",
|
"resourceHTTPSSettings": "Impostazioni HTTPS",
|
||||||
@@ -197,11 +199,13 @@
|
|||||||
"general": "Generale",
|
"general": "Generale",
|
||||||
"generalSettings": "Impostazioni Generali",
|
"generalSettings": "Impostazioni Generali",
|
||||||
"proxy": "Proxy",
|
"proxy": "Proxy",
|
||||||
|
"internal": "Interno",
|
||||||
"rules": "Regole",
|
"rules": "Regole",
|
||||||
"resourceSettingDescription": "Configura le impostazioni sulla tua risorsa",
|
"resourceSettingDescription": "Configura le impostazioni sulla tua risorsa",
|
||||||
"resourceSetting": "Impostazioni {resourceName}",
|
"resourceSetting": "Impostazioni {resourceName}",
|
||||||
"alwaysAllow": "Consenti Sempre",
|
"alwaysAllow": "Consenti Sempre",
|
||||||
"alwaysDeny": "Nega Sempre",
|
"alwaysDeny": "Nega Sempre",
|
||||||
|
"passToAuth": "Passa all'autenticazione",
|
||||||
"orgSettingsDescription": "Configura le impostazioni generali della tua organizzazione",
|
"orgSettingsDescription": "Configura le impostazioni generali della tua organizzazione",
|
||||||
"orgGeneralSettings": "Impostazioni Organizzazione",
|
"orgGeneralSettings": "Impostazioni Organizzazione",
|
||||||
"orgGeneralSettingsDescription": "Gestisci i dettagli dell'organizzazione e la configurazione",
|
"orgGeneralSettingsDescription": "Gestisci i dettagli dell'organizzazione e la configurazione",
|
||||||
@@ -490,7 +494,7 @@
|
|||||||
"targetTlsSniDescription": "Il Nome Server TLS da usare per SNI. Lascia vuoto per usare quello predefinito.",
|
"targetTlsSniDescription": "Il Nome Server TLS da usare per SNI. Lascia vuoto per usare quello predefinito.",
|
||||||
"targetTlsSubmit": "Salva Impostazioni",
|
"targetTlsSubmit": "Salva Impostazioni",
|
||||||
"targets": "Configurazione Target",
|
"targets": "Configurazione Target",
|
||||||
"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",
|
"targetStickySessions": "Abilita Sessioni Persistenti",
|
||||||
"targetStickySessionsDescription": "Mantieni le connessioni sullo stesso target backend per l'intera sessione.",
|
"targetStickySessionsDescription": "Mantieni le connessioni sullo stesso target backend per l'intera sessione.",
|
||||||
"methodSelect": "Seleziona metodo",
|
"methodSelect": "Seleziona metodo",
|
||||||
@@ -542,6 +546,7 @@
|
|||||||
"rulesActions": "Azioni",
|
"rulesActions": "Azioni",
|
||||||
"rulesActionAlwaysAllow": "Consenti Sempre: Ignora tutti i metodi di autenticazione",
|
"rulesActionAlwaysAllow": "Consenti Sempre: Ignora tutti i metodi di autenticazione",
|
||||||
"rulesActionAlwaysDeny": "Nega Sempre: Blocca tutte le richieste; nessuna autenticazione può essere tentata",
|
"rulesActionAlwaysDeny": "Nega Sempre: Blocca tutte le richieste; nessuna autenticazione può essere tentata",
|
||||||
|
"rulesActionPassToAuth": "Passa all'autenticazione: Consenti di tentare i metodi di autenticazione",
|
||||||
"rulesMatchCriteria": "Criteri di Corrispondenza",
|
"rulesMatchCriteria": "Criteri di Corrispondenza",
|
||||||
"rulesMatchCriteriaIpAddress": "Corrisponde a un indirizzo IP specifico",
|
"rulesMatchCriteriaIpAddress": "Corrisponde a un indirizzo IP specifico",
|
||||||
"rulesMatchCriteriaIpAddressRange": "Corrisponde a un intervallo di indirizzi IP in notazione CIDR",
|
"rulesMatchCriteriaIpAddressRange": "Corrisponde a un intervallo di indirizzi IP in notazione CIDR",
|
||||||
@@ -833,24 +838,24 @@
|
|||||||
"pincodeRequirementsLength": "Il PIN deve essere esattamente di 6 cifre",
|
"pincodeRequirementsLength": "Il PIN deve essere esattamente di 6 cifre",
|
||||||
"pincodeRequirementsChars": "Il PIN deve contenere solo numeri",
|
"pincodeRequirementsChars": "Il PIN deve contenere solo numeri",
|
||||||
"passwordRequirementsLength": "La password deve essere lunga almeno 1 carattere",
|
"passwordRequirementsLength": "La password deve essere lunga almeno 1 carattere",
|
||||||
"passwordRequirementsTitle": "Password requirements:",
|
"passwordRequirementsTitle": "Requisiti della password:",
|
||||||
"passwordRequirementLength": "At least 8 characters long",
|
"passwordRequirementLength": "Almeno 8 caratteri",
|
||||||
"passwordRequirementUppercase": "At least one uppercase letter",
|
"passwordRequirementUppercase": "Almeno una lettera maiuscola",
|
||||||
"passwordRequirementLowercase": "At least one lowercase letter",
|
"passwordRequirementLowercase": "Almeno una lettera minuscola",
|
||||||
"passwordRequirementNumber": "At least one number",
|
"passwordRequirementNumber": "Almeno un numero",
|
||||||
"passwordRequirementSpecial": "At least one special character",
|
"passwordRequirementSpecial": "Almeno un carattere speciale",
|
||||||
"passwordRequirementsMet": "✓ Password meets all requirements",
|
"passwordRequirementsMet": "✓ La password soddisfa tutti i requisiti",
|
||||||
"passwordStrength": "Password strength",
|
"passwordStrength": "Forza della password",
|
||||||
"passwordStrengthWeak": "Weak",
|
"passwordStrengthWeak": "Debole",
|
||||||
"passwordStrengthMedium": "Medium",
|
"passwordStrengthMedium": "Media",
|
||||||
"passwordStrengthStrong": "Strong",
|
"passwordStrengthStrong": "Forte",
|
||||||
"passwordRequirements": "Requirements:",
|
"passwordRequirements": "Requisiti:",
|
||||||
"passwordRequirementLengthText": "8+ characters",
|
"passwordRequirementLengthText": "8+ caratteri",
|
||||||
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
"passwordRequirementUppercaseText": "Lettera maiuscola (A-Z)",
|
||||||
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
"passwordRequirementLowercaseText": "Lettera minuscola (a-z)",
|
||||||
"passwordRequirementNumberText": "Number (0-9)",
|
"passwordRequirementNumberText": "Numero (0-9)",
|
||||||
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
"passwordRequirementSpecialText": "Carattere speciale (!@#$%...)",
|
||||||
"passwordsDoNotMatch": "Passwords do not match",
|
"passwordsDoNotMatch": "Le password non coincidono",
|
||||||
"otpEmailRequirementsLength": "L'OTP deve essere lungo almeno 1 carattere",
|
"otpEmailRequirementsLength": "L'OTP deve essere lungo almeno 1 carattere",
|
||||||
"otpEmailSent": "OTP Inviato",
|
"otpEmailSent": "OTP Inviato",
|
||||||
"otpEmailSentDescription": "Un OTP è stato inviato alla tua email",
|
"otpEmailSentDescription": "Un OTP è stato inviato alla tua email",
|
||||||
@@ -970,6 +975,7 @@
|
|||||||
"logoutError": "Errore durante il logout",
|
"logoutError": "Errore durante il logout",
|
||||||
"signingAs": "Accesso come",
|
"signingAs": "Accesso come",
|
||||||
"serverAdmin": "Amministratore Server",
|
"serverAdmin": "Amministratore Server",
|
||||||
|
"managedSelfhosted": "Gestito Auto-Ospitato",
|
||||||
"otpEnable": "Abilita Autenticazione a Due Fattori",
|
"otpEnable": "Abilita Autenticazione a Due Fattori",
|
||||||
"otpDisable": "Disabilita Autenticazione a Due Fattori",
|
"otpDisable": "Disabilita Autenticazione a Due Fattori",
|
||||||
"logout": "Disconnetti",
|
"logout": "Disconnetti",
|
||||||
@@ -985,9 +991,9 @@
|
|||||||
"actionDeleteSite": "Elimina Sito",
|
"actionDeleteSite": "Elimina Sito",
|
||||||
"actionGetSite": "Ottieni Sito",
|
"actionGetSite": "Ottieni Sito",
|
||||||
"actionListSites": "Elenca Siti",
|
"actionListSites": "Elenca Siti",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "Configura Token",
|
||||||
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
"setupTokenDescription": "Inserisci il token di configurazione dalla console del server.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "Il token di configurazione è richiesto",
|
||||||
"actionUpdateSite": "Aggiorna Sito",
|
"actionUpdateSite": "Aggiorna Sito",
|
||||||
"actionListSiteRoles": "Elenca Ruoli Sito Consentiti",
|
"actionListSiteRoles": "Elenca Ruoli Sito Consentiti",
|
||||||
"actionCreateResource": "Crea Risorsa",
|
"actionCreateResource": "Crea Risorsa",
|
||||||
@@ -1048,6 +1054,12 @@
|
|||||||
"actionUpdateClient": "Aggiorna Client",
|
"actionUpdateClient": "Aggiorna Client",
|
||||||
"actionListClients": "Elenco Clienti",
|
"actionListClients": "Elenco Clienti",
|
||||||
"actionGetClient": "Ottieni Client",
|
"actionGetClient": "Ottieni Client",
|
||||||
|
"actionCreateSiteResource": "Crea Risorsa del Sito",
|
||||||
|
"actionDeleteSiteResource": "Elimina Risorsa del Sito",
|
||||||
|
"actionGetSiteResource": "Ottieni Risorsa del Sito",
|
||||||
|
"actionListSiteResources": "Elenca Risorse del Sito",
|
||||||
|
"actionUpdateSiteResource": "Aggiorna Risorsa del Sito",
|
||||||
|
"actionListInvitations": "Elenco Inviti",
|
||||||
"noneSelected": "Nessuna selezione",
|
"noneSelected": "Nessuna selezione",
|
||||||
"orgNotFound2": "Nessuna organizzazione trovata.",
|
"orgNotFound2": "Nessuna organizzazione trovata.",
|
||||||
"searchProgress": "Ricerca...",
|
"searchProgress": "Ricerca...",
|
||||||
@@ -1341,8 +1353,148 @@
|
|||||||
"olmErrorFetchLatest": "Si è verificato un errore durante il recupero dell'ultima versione di Olm.",
|
"olmErrorFetchLatest": "Si è verificato un errore durante il recupero dell'ultima versione di Olm.",
|
||||||
"remoteSubnets": "Sottoreti Remote",
|
"remoteSubnets": "Sottoreti Remote",
|
||||||
"enterCidrRange": "Inserisci l'intervallo CIDR",
|
"enterCidrRange": "Inserisci l'intervallo CIDR",
|
||||||
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
"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",
|
"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.",
|
"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"
|
"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.",
|
||||||
|
"managedSelfHosted": {
|
||||||
|
"title": "Gestito Auto-Ospitato",
|
||||||
|
"description": "Server Pangolin self-hosted più affidabile e a bassa manutenzione con campanelli e fischietti extra",
|
||||||
|
"introTitle": "Managed Self-Hosted Pangolin",
|
||||||
|
"introDescription": "è un'opzione di distribuzione progettata per le persone che vogliono la semplicità e l'affidabilità extra mantenendo i loro dati privati e self-hosted.",
|
||||||
|
"introDetail": "Con questa opzione, esegui ancora il tuo nodo Pangolin — i tunnel, la terminazione SSL e il traffico rimangono tutti sul tuo server. La differenza è che la gestione e il monitoraggio sono gestiti attraverso il nostro cruscotto cloud, che sblocca una serie di vantaggi:",
|
||||||
|
"benefitSimplerOperations": {
|
||||||
|
"title": "Operazioni più semplici",
|
||||||
|
"description": "Non è necessario eseguire il proprio server di posta o impostare un avviso complesso. Otterrai controlli di salute e avvisi di inattività fuori dalla casella."
|
||||||
|
},
|
||||||
|
"benefitAutomaticUpdates": {
|
||||||
|
"title": "Aggiornamenti automatici",
|
||||||
|
"description": "Il cruscotto cloud si evolve rapidamente, in modo da ottenere nuove funzionalità e correzioni di bug senza dover tirare manualmente nuovi contenitori ogni volta."
|
||||||
|
},
|
||||||
|
"benefitLessMaintenance": {
|
||||||
|
"title": "Meno manutenzione",
|
||||||
|
"description": "Nessuna migrazione di database, backup o infrastruttura extra da gestire. Gestiamo questo problema nel cloud."
|
||||||
|
},
|
||||||
|
"benefitCloudFailover": {
|
||||||
|
"title": "failover del cloud",
|
||||||
|
"description": "Se il tuo nodo scende, i tuoi tunnel possono temporaneamente fallire nei nostri punti di presenza cloud fino a quando non lo riporti online."
|
||||||
|
},
|
||||||
|
"benefitHighAvailability": {
|
||||||
|
"title": "Alta disponibilità (PoPs)",
|
||||||
|
"description": "Puoi anche allegare più nodi al tuo account per ridondanza e prestazioni migliori."
|
||||||
|
},
|
||||||
|
"benefitFutureEnhancements": {
|
||||||
|
"title": "Miglioramenti futuri",
|
||||||
|
"description": "Stiamo pianificando di aggiungere più strumenti di analisi, allerta e gestione per rendere la tua distribuzione ancora più robusta."
|
||||||
|
},
|
||||||
|
"docsAlert": {
|
||||||
|
"text": "Scopri di più sull'opzione Managed Self-Hosted nella nostra",
|
||||||
|
"documentation": "documentazione"
|
||||||
|
},
|
||||||
|
"convertButton": "Converti questo nodo in auto-ospitato gestito"
|
||||||
|
},
|
||||||
|
"internationaldomaindetected": "Dominio Internazionale Rilevato",
|
||||||
|
"willbestoredas": "Verrà conservato come:"
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,9 @@
|
|||||||
"siteNewtTunnelDescription": "네트워크에 대한 진입점을 생성하는 가장 쉬운 방법입니다. 추가 설정이 필요 없습니다.",
|
"siteNewtTunnelDescription": "네트워크에 대한 진입점을 생성하는 가장 쉬운 방법입니다. 추가 설정이 필요 없습니다.",
|
||||||
"siteWg": "기본 WireGuard",
|
"siteWg": "기본 WireGuard",
|
||||||
"siteWgDescription": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다.",
|
"siteWgDescription": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다.",
|
||||||
|
"siteWgDescriptionSaas": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다. 자체 호스팅 노드에서만 작동합니다.",
|
||||||
"siteLocalDescription": "로컬 리소스만 사용 가능합니다. 터널링이 없습니다.",
|
"siteLocalDescription": "로컬 리소스만 사용 가능합니다. 터널링이 없습니다.",
|
||||||
|
"siteLocalDescriptionSaas": "로컬 리소스만. 터널링 없음. 자체 호스팅 노드에서만 작동합니다.",
|
||||||
"siteSeeAll": "모든 사이트 보기",
|
"siteSeeAll": "모든 사이트 보기",
|
||||||
"siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요",
|
"siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요",
|
||||||
"siteNewtCredentials": "Newt 자격 증명",
|
"siteNewtCredentials": "Newt 자격 증명",
|
||||||
@@ -166,7 +168,7 @@
|
|||||||
"siteSelect": "사이트 선택",
|
"siteSelect": "사이트 선택",
|
||||||
"siteSearch": "사이트 검색",
|
"siteSearch": "사이트 검색",
|
||||||
"siteNotFound": "사이트를 찾을 수 없습니다.",
|
"siteNotFound": "사이트를 찾을 수 없습니다.",
|
||||||
"siteSelectionDescription": "이 사이트는 리소스에 대한 연결을 제공합니다.",
|
"siteSelectionDescription": "이 사이트는 대상에 대한 연결을 제공합니다.",
|
||||||
"resourceType": "리소스 유형",
|
"resourceType": "리소스 유형",
|
||||||
"resourceTypeDescription": "리소스에 접근하는 방법을 결정하세요",
|
"resourceTypeDescription": "리소스에 접근하는 방법을 결정하세요",
|
||||||
"resourceHTTPSSettings": "HTTPS 설정",
|
"resourceHTTPSSettings": "HTTPS 설정",
|
||||||
@@ -197,11 +199,13 @@
|
|||||||
"general": "일반",
|
"general": "일반",
|
||||||
"generalSettings": "일반 설정",
|
"generalSettings": "일반 설정",
|
||||||
"proxy": "프록시",
|
"proxy": "프록시",
|
||||||
|
"internal": "내부",
|
||||||
"rules": "규칙",
|
"rules": "규칙",
|
||||||
"resourceSettingDescription": "리소스의 설정을 구성하세요.",
|
"resourceSettingDescription": "리소스의 설정을 구성하세요.",
|
||||||
"resourceSetting": "{resourceName} 설정",
|
"resourceSetting": "{resourceName} 설정",
|
||||||
"alwaysAllow": "항상 허용",
|
"alwaysAllow": "항상 허용",
|
||||||
"alwaysDeny": "항상 거부",
|
"alwaysDeny": "항상 거부",
|
||||||
|
"passToAuth": "인증으로 전달",
|
||||||
"orgSettingsDescription": "조직의 일반 설정을 구성하세요",
|
"orgSettingsDescription": "조직의 일반 설정을 구성하세요",
|
||||||
"orgGeneralSettings": "조직 설정",
|
"orgGeneralSettings": "조직 설정",
|
||||||
"orgGeneralSettingsDescription": "조직 세부정보 및 구성을 관리하세요.",
|
"orgGeneralSettingsDescription": "조직 세부정보 및 구성을 관리하세요.",
|
||||||
@@ -490,7 +494,7 @@
|
|||||||
"targetTlsSniDescription": "SNI에 사용할 TLS 서버 이름. 기본값을 사용하려면 비워 두십시오.",
|
"targetTlsSniDescription": "SNI에 사용할 TLS 서버 이름. 기본값을 사용하려면 비워 두십시오.",
|
||||||
"targetTlsSubmit": "설정 저장",
|
"targetTlsSubmit": "설정 저장",
|
||||||
"targets": "대상 구성",
|
"targets": "대상 구성",
|
||||||
"targetsDescription": "서비스로 트래픽을 라우팅할 대상을 설정하십시오",
|
"targetsDescription": "사용자 백엔드 서비스로 트래픽을 라우팅할 대상을 설정하십시오.",
|
||||||
"targetStickySessions": "스티키 세션 활성화",
|
"targetStickySessions": "스티키 세션 활성화",
|
||||||
"targetStickySessionsDescription": "세션 전체 동안 동일한 백엔드 대상을 유지합니다.",
|
"targetStickySessionsDescription": "세션 전체 동안 동일한 백엔드 대상을 유지합니다.",
|
||||||
"methodSelect": "선택 방법",
|
"methodSelect": "선택 방법",
|
||||||
@@ -542,6 +546,7 @@
|
|||||||
"rulesActions": "작업",
|
"rulesActions": "작업",
|
||||||
"rulesActionAlwaysAllow": "항상 허용: 모든 인증 방법 우회",
|
"rulesActionAlwaysAllow": "항상 허용: 모든 인증 방법 우회",
|
||||||
"rulesActionAlwaysDeny": "항상 거부: 모든 요청을 차단합니다. 인증을 시도할 수 없습니다.",
|
"rulesActionAlwaysDeny": "항상 거부: 모든 요청을 차단합니다. 인증을 시도할 수 없습니다.",
|
||||||
|
"rulesActionPassToAuth": "인증으로 전달: 인증 방법 시도를 허용합니다",
|
||||||
"rulesMatchCriteria": "일치 기준",
|
"rulesMatchCriteria": "일치 기준",
|
||||||
"rulesMatchCriteriaIpAddress": "특정 IP 주소와 일치",
|
"rulesMatchCriteriaIpAddress": "특정 IP 주소와 일치",
|
||||||
"rulesMatchCriteriaIpAddressRange": "CIDR 표기법으로 IP 주소 범위를 일치시킵니다",
|
"rulesMatchCriteriaIpAddressRange": "CIDR 표기법으로 IP 주소 범위를 일치시킵니다",
|
||||||
@@ -833,24 +838,24 @@
|
|||||||
"pincodeRequirementsLength": "PIN은 정확히 6자리여야 합니다",
|
"pincodeRequirementsLength": "PIN은 정확히 6자리여야 합니다",
|
||||||
"pincodeRequirementsChars": "PIN은 숫자만 포함해야 합니다.",
|
"pincodeRequirementsChars": "PIN은 숫자만 포함해야 합니다.",
|
||||||
"passwordRequirementsLength": "비밀번호는 최소 1자 이상이어야 합니다",
|
"passwordRequirementsLength": "비밀번호는 최소 1자 이상이어야 합니다",
|
||||||
"passwordRequirementsTitle": "Password requirements:",
|
"passwordRequirementsTitle": "비밀번호 요구사항:",
|
||||||
"passwordRequirementLength": "At least 8 characters long",
|
"passwordRequirementLength": "최소 8자 이상",
|
||||||
"passwordRequirementUppercase": "At least one uppercase letter",
|
"passwordRequirementUppercase": "최소 대문자 하나",
|
||||||
"passwordRequirementLowercase": "At least one lowercase letter",
|
"passwordRequirementLowercase": "최소 소문자 하나",
|
||||||
"passwordRequirementNumber": "At least one number",
|
"passwordRequirementNumber": "최소 숫자 하나",
|
||||||
"passwordRequirementSpecial": "At least one special character",
|
"passwordRequirementSpecial": "최소 특수 문자 하나",
|
||||||
"passwordRequirementsMet": "✓ Password meets all requirements",
|
"passwordRequirementsMet": "✓ 비밀번호가 모든 요구사항을 충족합니다.",
|
||||||
"passwordStrength": "Password strength",
|
"passwordStrength": "비밀번호 강도",
|
||||||
"passwordStrengthWeak": "Weak",
|
"passwordStrengthWeak": "약함",
|
||||||
"passwordStrengthMedium": "Medium",
|
"passwordStrengthMedium": "보통",
|
||||||
"passwordStrengthStrong": "Strong",
|
"passwordStrengthStrong": "강함",
|
||||||
"passwordRequirements": "Requirements:",
|
"passwordRequirements": "요구 사항:",
|
||||||
"passwordRequirementLengthText": "8+ characters",
|
"passwordRequirementLengthText": "8자 이상",
|
||||||
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
"passwordRequirementUppercaseText": "대문자 (A-Z)",
|
||||||
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
"passwordRequirementLowercaseText": "소문자 (a-z)",
|
||||||
"passwordRequirementNumberText": "Number (0-9)",
|
"passwordRequirementNumberText": "숫자 (0-9)",
|
||||||
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
"passwordRequirementSpecialText": "특수 문자 (!@#$%...)",
|
||||||
"passwordsDoNotMatch": "Passwords do not match",
|
"passwordsDoNotMatch": "비밀번호가 일치하지 않습니다.",
|
||||||
"otpEmailRequirementsLength": "OTP는 최소 1자 이상이어야 합니다",
|
"otpEmailRequirementsLength": "OTP는 최소 1자 이상이어야 합니다",
|
||||||
"otpEmailSent": "OTP 전송됨",
|
"otpEmailSent": "OTP 전송됨",
|
||||||
"otpEmailSentDescription": "OTP가 귀하의 이메일로 전송되었습니다.",
|
"otpEmailSentDescription": "OTP가 귀하의 이메일로 전송되었습니다.",
|
||||||
@@ -970,6 +975,7 @@
|
|||||||
"logoutError": "로그아웃 중 오류 발생",
|
"logoutError": "로그아웃 중 오류 발생",
|
||||||
"signingAs": "로그인한 사용자",
|
"signingAs": "로그인한 사용자",
|
||||||
"serverAdmin": "서버 관리자",
|
"serverAdmin": "서버 관리자",
|
||||||
|
"managedSelfhosted": "관리 자체 호스팅",
|
||||||
"otpEnable": "이중 인증 활성화",
|
"otpEnable": "이중 인증 활성화",
|
||||||
"otpDisable": "이중 인증 비활성화",
|
"otpDisable": "이중 인증 비활성화",
|
||||||
"logout": "로그 아웃",
|
"logout": "로그 아웃",
|
||||||
@@ -985,9 +991,9 @@
|
|||||||
"actionDeleteSite": "사이트 삭제",
|
"actionDeleteSite": "사이트 삭제",
|
||||||
"actionGetSite": "사이트 가져오기",
|
"actionGetSite": "사이트 가져오기",
|
||||||
"actionListSites": "사이트 목록",
|
"actionListSites": "사이트 목록",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "설정 토큰",
|
||||||
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
"setupTokenDescription": "서버 콘솔에서 설정 토큰 입력.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "설정 토큰이 필요합니다",
|
||||||
"actionUpdateSite": "사이트 업데이트",
|
"actionUpdateSite": "사이트 업데이트",
|
||||||
"actionListSiteRoles": "허용된 사이트 역할 목록",
|
"actionListSiteRoles": "허용된 사이트 역할 목록",
|
||||||
"actionCreateResource": "리소스 생성",
|
"actionCreateResource": "리소스 생성",
|
||||||
@@ -1043,11 +1049,17 @@
|
|||||||
"actionDeleteIdpOrg": "IDP 조직 정책 삭제",
|
"actionDeleteIdpOrg": "IDP 조직 정책 삭제",
|
||||||
"actionListIdpOrgs": "IDP 조직 목록",
|
"actionListIdpOrgs": "IDP 조직 목록",
|
||||||
"actionUpdateIdpOrg": "IDP 조직 업데이트",
|
"actionUpdateIdpOrg": "IDP 조직 업데이트",
|
||||||
"actionCreateClient": "Create Client",
|
"actionCreateClient": "클라이언트 생성",
|
||||||
"actionDeleteClient": "Delete Client",
|
"actionDeleteClient": "클라이언트 삭제",
|
||||||
"actionUpdateClient": "Update Client",
|
"actionUpdateClient": "클라이언트 업데이트",
|
||||||
"actionListClients": "List Clients",
|
"actionListClients": "클라이언트 목록",
|
||||||
"actionGetClient": "Get Client",
|
"actionGetClient": "클라이언트 가져오기",
|
||||||
|
"actionCreateSiteResource": "사이트 리소스 생성",
|
||||||
|
"actionDeleteSiteResource": "사이트 리소스 삭제",
|
||||||
|
"actionGetSiteResource": "사이트 리소스 가져오기",
|
||||||
|
"actionListSiteResources": "사이트 리소스 목록",
|
||||||
|
"actionUpdateSiteResource": "사이트 리소스 업데이트",
|
||||||
|
"actionListInvitations": "초대 목록",
|
||||||
"noneSelected": "선택된 항목 없음",
|
"noneSelected": "선택된 항목 없음",
|
||||||
"orgNotFound2": "조직이 없습니다.",
|
"orgNotFound2": "조직이 없습니다.",
|
||||||
"searchProgress": "검색...",
|
"searchProgress": "검색...",
|
||||||
@@ -1119,7 +1131,7 @@
|
|||||||
"sidebarAllUsers": "모든 사용자",
|
"sidebarAllUsers": "모든 사용자",
|
||||||
"sidebarIdentityProviders": "신원 공급자",
|
"sidebarIdentityProviders": "신원 공급자",
|
||||||
"sidebarLicense": "라이선스",
|
"sidebarLicense": "라이선스",
|
||||||
"sidebarClients": "Clients (Beta)",
|
"sidebarClients": "클라이언트 (Beta)",
|
||||||
"sidebarDomains": "도메인",
|
"sidebarDomains": "도메인",
|
||||||
"enableDockerSocket": "Docker 소켓 활성화",
|
"enableDockerSocket": "Docker 소켓 활성화",
|
||||||
"enableDockerSocketDescription": "컨테이너 정보를 채우기 위해 Docker 소켓 검색을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.",
|
"enableDockerSocketDescription": "컨테이너 정보를 채우기 위해 Docker 소켓 검색을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.",
|
||||||
@@ -1187,7 +1199,7 @@
|
|||||||
"selectDomainTypeCnameName": "단일 도메인 (CNAME)",
|
"selectDomainTypeCnameName": "단일 도메인 (CNAME)",
|
||||||
"selectDomainTypeCnameDescription": "단일 하위 도메인 또는 특정 도메인 항목에 사용됩니다.",
|
"selectDomainTypeCnameDescription": "단일 하위 도메인 또는 특정 도메인 항목에 사용됩니다.",
|
||||||
"selectDomainTypeWildcardName": "와일드카드 도메인",
|
"selectDomainTypeWildcardName": "와일드카드 도메인",
|
||||||
"selectDomainTypeWildcardDescription": "This domain and its subdomains.",
|
"selectDomainTypeWildcardDescription": "이 도메인 및 그 하위 도메인.",
|
||||||
"domainDelegation": "단일 도메인",
|
"domainDelegation": "단일 도메인",
|
||||||
"selectType": "유형 선택",
|
"selectType": "유형 선택",
|
||||||
"actions": "작업",
|
"actions": "작업",
|
||||||
@@ -1221,17 +1233,17 @@
|
|||||||
"sidebarExpand": "확장하기",
|
"sidebarExpand": "확장하기",
|
||||||
"newtUpdateAvailable": "업데이트 가능",
|
"newtUpdateAvailable": "업데이트 가능",
|
||||||
"newtUpdateAvailableInfo": "뉴트의 새 버전이 출시되었습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
|
"newtUpdateAvailableInfo": "뉴트의 새 버전이 출시되었습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
|
||||||
"domainPickerEnterDomain": "Domain",
|
"domainPickerEnterDomain": "도메인",
|
||||||
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, 또는 그냥 myapp",
|
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, 또는 그냥 myapp",
|
||||||
"domainPickerDescription": "Enter the full domain of the resource to see available options.",
|
"domainPickerDescription": "리소스의 전체 도메인을 입력하여 사용 가능한 옵션을 확인하십시오.",
|
||||||
"domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options",
|
"domainPickerDescriptionSaas": "전체 도메인, 서브도메인 또는 이름을 입력하여 사용 가능한 옵션을 확인하십시오.",
|
||||||
"domainPickerTabAll": "모두",
|
"domainPickerTabAll": "모두",
|
||||||
"domainPickerTabOrganization": "조직",
|
"domainPickerTabOrganization": "조직",
|
||||||
"domainPickerTabProvided": "제공 됨",
|
"domainPickerTabProvided": "제공 됨",
|
||||||
"domainPickerSortAsc": "A-Z",
|
"domainPickerSortAsc": "A-Z",
|
||||||
"domainPickerSortDesc": "Z-A",
|
"domainPickerSortDesc": "Z-A",
|
||||||
"domainPickerCheckingAvailability": "가용성을 확인 중...",
|
"domainPickerCheckingAvailability": "가용성을 확인 중...",
|
||||||
"domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.",
|
"domainPickerNoMatchingDomains": "일치하는 도메인을 찾을 수 없습니다. 다른 도메인을 시도하거나 조직의 도메인 설정을 확인하십시오.",
|
||||||
"domainPickerOrganizationDomains": "조직 도메인",
|
"domainPickerOrganizationDomains": "조직 도메인",
|
||||||
"domainPickerProvidedDomains": "제공된 도메인",
|
"domainPickerProvidedDomains": "제공된 도메인",
|
||||||
"domainPickerSubdomain": "서브도메인: {subdomain}",
|
"domainPickerSubdomain": "서브도메인: {subdomain}",
|
||||||
@@ -1257,7 +1269,7 @@
|
|||||||
"securityKeyRemoveSuccess": "보안 키가 성공적으로 제거되었습니다",
|
"securityKeyRemoveSuccess": "보안 키가 성공적으로 제거되었습니다",
|
||||||
"securityKeyRemoveError": "보안 키 제거 실패",
|
"securityKeyRemoveError": "보안 키 제거 실패",
|
||||||
"securityKeyLoadError": "보안 키를 불러오는 데 실패했습니다",
|
"securityKeyLoadError": "보안 키를 불러오는 데 실패했습니다",
|
||||||
"securityKeyLogin": "Continue with security key",
|
"securityKeyLogin": "보안 키로 계속하기",
|
||||||
"securityKeyAuthError": "보안 키를 사용한 인증 실패",
|
"securityKeyAuthError": "보안 키를 사용한 인증 실패",
|
||||||
"securityKeyRecommendation": "항상 계정에 액세스할 수 있도록 다른 장치에 백업 보안 키를 등록하세요.",
|
"securityKeyRecommendation": "항상 계정에 액세스할 수 있도록 다른 장치에 백업 보안 키를 등록하세요.",
|
||||||
"registering": "등록 중...",
|
"registering": "등록 중...",
|
||||||
@@ -1291,7 +1303,7 @@
|
|||||||
"createDomainName": "이름:",
|
"createDomainName": "이름:",
|
||||||
"createDomainValue": "값:",
|
"createDomainValue": "값:",
|
||||||
"createDomainCnameRecords": "CNAME 레코드",
|
"createDomainCnameRecords": "CNAME 레코드",
|
||||||
"createDomainARecords": "A Records",
|
"createDomainARecords": "A 레코드",
|
||||||
"createDomainRecordNumber": "레코드 {number}",
|
"createDomainRecordNumber": "레코드 {number}",
|
||||||
"createDomainTxtRecords": "TXT 레코드",
|
"createDomainTxtRecords": "TXT 레코드",
|
||||||
"createDomainSaveTheseRecords": "이 레코드 저장",
|
"createDomainSaveTheseRecords": "이 레코드 저장",
|
||||||
@@ -1301,48 +1313,188 @@
|
|||||||
"resourcePortRequired": "HTTP 리소스가 아닌 경우 포트 번호가 필요합니다",
|
"resourcePortRequired": "HTTP 리소스가 아닌 경우 포트 번호가 필요합니다",
|
||||||
"resourcePortNotAllowed": "HTTP 리소스에 대해 포트 번호를 설정하지 마세요",
|
"resourcePortNotAllowed": "HTTP 리소스에 대해 포트 번호를 설정하지 마세요",
|
||||||
"signUpTerms": {
|
"signUpTerms": {
|
||||||
"IAgreeToThe": "I agree to the",
|
"IAgreeToThe": "동의합니다",
|
||||||
"termsOfService": "terms of service",
|
"termsOfService": "서비스 약관",
|
||||||
"and": "and",
|
"and": "및",
|
||||||
"privacyPolicy": "privacy policy"
|
"privacyPolicy": "개인 정보 보호 정책"
|
||||||
},
|
},
|
||||||
"siteRequired": "Site is required.",
|
"siteRequired": "사이트가 필요합니다.",
|
||||||
"olmTunnel": "Olm Tunnel",
|
"olmTunnel": "Olm 터널",
|
||||||
"olmTunnelDescription": "Use Olm for client connectivity",
|
"olmTunnelDescription": "클라이언트 연결에 Olm 사용",
|
||||||
"errorCreatingClient": "Error creating client",
|
"errorCreatingClient": "클라이언트 생성 오류",
|
||||||
"clientDefaultsNotFound": "Client defaults not found",
|
"clientDefaultsNotFound": "클라이언트 기본값을 찾을 수 없습니다.",
|
||||||
"createClient": "Create Client",
|
"createClient": "클라이언트 생성",
|
||||||
"createClientDescription": "Create a new client for connecting to your sites",
|
"createClientDescription": "사이트에 연결하기 위한 새 클라이언트를 생성하십시오.",
|
||||||
"seeAllClients": "See All Clients",
|
"seeAllClients": "모든 클라이언트 보기",
|
||||||
"clientInformation": "Client Information",
|
"clientInformation": "클라이언트 정보",
|
||||||
"clientNamePlaceholder": "Client name",
|
"clientNamePlaceholder": "클라이언트 이름",
|
||||||
"address": "Address",
|
"address": "주소",
|
||||||
"subnetPlaceholder": "Subnet",
|
"subnetPlaceholder": "서브넷",
|
||||||
"addressDescription": "The address that this client will use for connectivity",
|
"addressDescription": "이 클라이언트가 연결에 사용할 주소",
|
||||||
"selectSites": "Select sites",
|
"selectSites": "사이트 선택",
|
||||||
"sitesDescription": "The client will have connectivity to the selected sites",
|
"sitesDescription": "클라이언트는 선택한 사이트에 연결됩니다.",
|
||||||
"clientInstallOlm": "Install Olm",
|
"clientInstallOlm": "Olm 설치",
|
||||||
"clientInstallOlmDescription": "Get Olm running on your system",
|
"clientInstallOlmDescription": "시스템에서 Olm을 실행하기",
|
||||||
"clientOlmCredentials": "Olm Credentials",
|
"clientOlmCredentials": "Olm 자격 증명",
|
||||||
"clientOlmCredentialsDescription": "This is how Olm will authenticate with the server",
|
"clientOlmCredentialsDescription": "Olm이 서버와 인증하는 방법입니다.",
|
||||||
"olmEndpoint": "Olm Endpoint",
|
"olmEndpoint": "Olm 엔드포인트",
|
||||||
"olmId": "Olm ID",
|
"olmId": "Olm ID",
|
||||||
"olmSecretKey": "Olm Secret Key",
|
"olmSecretKey": "Olm 비밀 키",
|
||||||
"clientCredentialsSave": "Save Your Credentials",
|
"clientCredentialsSave": "자격 증명 저장",
|
||||||
"clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
|
"clientCredentialsSaveDescription": "이것은 한 번만 볼 수 있습니다. 안전한 장소에 복사해 두세요.",
|
||||||
"generalSettingsDescription": "Configure the general settings for this client",
|
"generalSettingsDescription": "이 클라이언트에 대한 일반 설정을 구성하세요.",
|
||||||
"clientUpdated": "Client updated",
|
"clientUpdated": "클라이언트 업데이트됨",
|
||||||
"clientUpdatedDescription": "The client has been updated.",
|
"clientUpdatedDescription": "클라이언트가 업데이트되었습니다.",
|
||||||
"clientUpdateFailed": "Failed to update client",
|
"clientUpdateFailed": "클라이언트 업데이트 실패",
|
||||||
"clientUpdateError": "An error occurred while updating the client.",
|
"clientUpdateError": "클라이언트 업데이트 중 오류가 발생했습니다.",
|
||||||
"sitesFetchFailed": "Failed to fetch sites",
|
"sitesFetchFailed": "사이트 가져오기 실패",
|
||||||
"sitesFetchError": "An error occurred while fetching sites.",
|
"sitesFetchError": "사이트 가져오는 중 오류가 발생했습니다.",
|
||||||
"olmErrorFetchReleases": "An error occurred while fetching Olm releases.",
|
"olmErrorFetchReleases": "Olm 릴리즈 가져오는 중 오류가 발생했습니다.",
|
||||||
"olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.",
|
"olmErrorFetchLatest": "최신 Olm 릴리즈 가져오는 중 오류가 발생했습니다.",
|
||||||
"remoteSubnets": "Remote Subnets",
|
"remoteSubnets": "원격 서브넷",
|
||||||
"enterCidrRange": "Enter CIDR range",
|
"enterCidrRange": "CIDR 범위 입력",
|
||||||
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
"remoteSubnetsDescription": "이 사이트에서 원격으로 액세스할 수 있는 CIDR 범위를 추가하세요. 10.0.0.0/24와 같은 형식을 사용하세요. 이는 VPN 클라이언트 연결에만 적용됩니다.",
|
||||||
"resourceEnableProxy": "Enable Public Proxy",
|
"resourceEnableProxy": "공개 프록시 사용",
|
||||||
"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.",
|
"resourceEnableProxyDescription": "이 리소스에 대한 공개 프록시를 활성화하십시오. 이를 통해 네트워크 외부로부터 클라우드를 통해 열린 포트에서 리소스에 액세스할 수 있습니다. Traefik 구성이 필요합니다.",
|
||||||
"externalProxyEnabled": "External Proxy Enabled"
|
"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 생성 실패.",
|
||||||
|
"managedSelfHosted": {
|
||||||
|
"title": "관리 자체 호스팅",
|
||||||
|
"description": "더 신뢰할 수 있고 낮은 유지보수의 자체 호스팅 팡골린 서버, 추가 기능 포함",
|
||||||
|
"introTitle": "관리 자체 호스팅 팡골린",
|
||||||
|
"introDescription": "는 자신의 데이터를 프라이빗하고 자체 호스팅을 유지하면서 더 간단하고 추가적인 신뢰성을 원하는 사람들을 위한 배포 옵션입니다.",
|
||||||
|
"introDetail": "이 옵션을 사용하면 여전히 자신의 팡골린 노드를 운영하고 - 터널, SSL 종료 및 트래픽 모두 서버에 유지됩니다. 차이점은 관리 및 모니터링이 클라우드 대시보드를 통해 처리되어 여러 혜택을 제공합니다.",
|
||||||
|
"benefitSimplerOperations": {
|
||||||
|
"title": "더 간단한 운영",
|
||||||
|
"description": "자체 메일 서버를 운영하거나 복잡한 경고를 설정할 필요가 없습니다. 기본적으로 상태 점검 및 다운타임 경고를 받을 수 있습니다."
|
||||||
|
},
|
||||||
|
"benefitAutomaticUpdates": {
|
||||||
|
"title": "자동 업데이트",
|
||||||
|
"description": "클라우드 대시보드는 빠르게 발전하므로 새로운 기능과 버그 수정 사항을 수동으로 새로운 컨테이너를 가져오지 않고도 받을 수 있습니다."
|
||||||
|
},
|
||||||
|
"benefitLessMaintenance": {
|
||||||
|
"title": "유지보수 감소",
|
||||||
|
"description": "데이터베이스 마이그레이션, 백업 또는 추가 인프라를 관리할 필요가 없습니다. 저희가 클라우드에서 처리합니다."
|
||||||
|
},
|
||||||
|
"benefitCloudFailover": {
|
||||||
|
"title": "클라우드 장애 조치",
|
||||||
|
"description": "노드가 다운되면 터널이 클라우드의 프레즌스 포인트로 임시 전환되어 노드를 다시 온라인으로 가져올 때까지 유지됩니다."
|
||||||
|
},
|
||||||
|
"benefitHighAvailability": {
|
||||||
|
"title": "고가용성 (PoPs)",
|
||||||
|
"description": "계정에 여러 노드를 연결하여 이중성과 성능을 향상시킬 수 있습니다."
|
||||||
|
},
|
||||||
|
"benefitFutureEnhancements": {
|
||||||
|
"title": "향후 개선",
|
||||||
|
"description": "배포를 더욱 견고하게 만들기 위해 더 많은 분석, 경고, 및 관리 도구를 추가할 계획입니다."
|
||||||
|
},
|
||||||
|
"docsAlert": {
|
||||||
|
"text": "관리 자체 호스팅 옵션에 대해 더 알아보세요",
|
||||||
|
"documentation": "문서"
|
||||||
|
},
|
||||||
|
"convertButton": "이 노드를 관리 자체 호스팅으로 변환"
|
||||||
|
},
|
||||||
|
"internationaldomaindetected": "국제 도메인 감지됨",
|
||||||
|
"willbestoredas": "다음으로 저장됩니다:"
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,9 @@
|
|||||||
"siteNewtTunnelDescription": "Enkleste måte å opprette et inngangspunkt i nettverket ditt. Ingen ekstra oppsett.",
|
"siteNewtTunnelDescription": "Enkleste måte å opprette et inngangspunkt i nettverket ditt. Ingen ekstra oppsett.",
|
||||||
"siteWg": "Grunnleggende WireGuard",
|
"siteWg": "Grunnleggende WireGuard",
|
||||||
"siteWgDescription": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett kreves.",
|
"siteWgDescription": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett kreves.",
|
||||||
|
"siteWgDescriptionSaas": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett er nødvendig. FUNGERER KUN PÅ SELVHOSTEDE NODER",
|
||||||
"siteLocalDescription": "Kun lokale ressurser. Ingen tunnelering.",
|
"siteLocalDescription": "Kun lokale ressurser. Ingen tunnelering.",
|
||||||
|
"siteLocalDescriptionSaas": "Kun lokale ressurser. Ingen tunneling. FUNGERER KUN PÅ SELVHOSTEDE NODER",
|
||||||
"siteSeeAll": "Se alle områder",
|
"siteSeeAll": "Se alle områder",
|
||||||
"siteTunnelDescription": "Bestem hvordan du vil koble deg til ditt område",
|
"siteTunnelDescription": "Bestem hvordan du vil koble deg til ditt område",
|
||||||
"siteNewtCredentials": "Newt påloggingsinformasjon",
|
"siteNewtCredentials": "Newt påloggingsinformasjon",
|
||||||
@@ -166,7 +168,7 @@
|
|||||||
"siteSelect": "Velg område",
|
"siteSelect": "Velg område",
|
||||||
"siteSearch": "Søk i område",
|
"siteSearch": "Søk i område",
|
||||||
"siteNotFound": "Ingen område funnet.",
|
"siteNotFound": "Ingen område funnet.",
|
||||||
"siteSelectionDescription": "Dette området vil gi tilkobling til ressursen.",
|
"siteSelectionDescription": "Dette området vil gi tilkobling til mål.",
|
||||||
"resourceType": "Ressurstype",
|
"resourceType": "Ressurstype",
|
||||||
"resourceTypeDescription": "Bestem hvordan du vil få tilgang til ressursen din",
|
"resourceTypeDescription": "Bestem hvordan du vil få tilgang til ressursen din",
|
||||||
"resourceHTTPSSettings": "HTTPS-innstillinger",
|
"resourceHTTPSSettings": "HTTPS-innstillinger",
|
||||||
@@ -197,11 +199,13 @@
|
|||||||
"general": "Generelt",
|
"general": "Generelt",
|
||||||
"generalSettings": "Generelle innstillinger",
|
"generalSettings": "Generelle innstillinger",
|
||||||
"proxy": "Proxy",
|
"proxy": "Proxy",
|
||||||
|
"internal": "Intern",
|
||||||
"rules": "Regler",
|
"rules": "Regler",
|
||||||
"resourceSettingDescription": "Konfigurer innstillingene på ressursen din",
|
"resourceSettingDescription": "Konfigurer innstillingene på ressursen din",
|
||||||
"resourceSetting": "{resourceName} Innstillinger",
|
"resourceSetting": "{resourceName} Innstillinger",
|
||||||
"alwaysAllow": "Alltid tillat",
|
"alwaysAllow": "Alltid tillat",
|
||||||
"alwaysDeny": "Alltid avslå",
|
"alwaysDeny": "Alltid avslå",
|
||||||
|
"passToAuth": "Pass til Autentisering",
|
||||||
"orgSettingsDescription": "Konfigurer organisasjonens generelle innstillinger",
|
"orgSettingsDescription": "Konfigurer organisasjonens generelle innstillinger",
|
||||||
"orgGeneralSettings": "Organisasjonsinnstillinger",
|
"orgGeneralSettings": "Organisasjonsinnstillinger",
|
||||||
"orgGeneralSettingsDescription": "Administrer dine organisasjonsdetaljer og konfigurasjon",
|
"orgGeneralSettingsDescription": "Administrer dine organisasjonsdetaljer og konfigurasjon",
|
||||||
@@ -490,7 +494,7 @@
|
|||||||
"targetTlsSniDescription": "TLS-servernavnet som skal brukes for SNI. La stå tomt for å bruke standardverdien.",
|
"targetTlsSniDescription": "TLS-servernavnet som skal brukes for SNI. La stå tomt for å bruke standardverdien.",
|
||||||
"targetTlsSubmit": "Lagre innstillinger",
|
"targetTlsSubmit": "Lagre innstillinger",
|
||||||
"targets": "Målkonfigurasjon",
|
"targets": "Målkonfigurasjon",
|
||||||
"targetsDescription": "Sett opp mål for å rute trafikk til tjenestene dine",
|
"targetsDescription": "Sett opp mål for å rute trafikk til dine backend-tjenester",
|
||||||
"targetStickySessions": "Aktiver klebrige sesjoner",
|
"targetStickySessions": "Aktiver klebrige sesjoner",
|
||||||
"targetStickySessionsDescription": "Behold tilkoblinger på samme bakend-mål gjennom hele sesjonen.",
|
"targetStickySessionsDescription": "Behold tilkoblinger på samme bakend-mål gjennom hele sesjonen.",
|
||||||
"methodSelect": "Velg metode",
|
"methodSelect": "Velg metode",
|
||||||
@@ -542,6 +546,7 @@
|
|||||||
"rulesActions": "Handlinger",
|
"rulesActions": "Handlinger",
|
||||||
"rulesActionAlwaysAllow": "Alltid Tillat: Omgå alle autentiserings metoder",
|
"rulesActionAlwaysAllow": "Alltid Tillat: Omgå alle autentiserings metoder",
|
||||||
"rulesActionAlwaysDeny": "Alltid Nekt: Blokker alle forespørsler; ingen autentisering kan forsøkes",
|
"rulesActionAlwaysDeny": "Alltid Nekt: Blokker alle forespørsler; ingen autentisering kan forsøkes",
|
||||||
|
"rulesActionPassToAuth": "Pass til Autentisering: Tillat at autentiseringsmetoder forsøkes",
|
||||||
"rulesMatchCriteria": "Samsvarende kriterier",
|
"rulesMatchCriteria": "Samsvarende kriterier",
|
||||||
"rulesMatchCriteriaIpAddress": "Samsvar med en spesifikk IP-adresse",
|
"rulesMatchCriteriaIpAddress": "Samsvar med en spesifikk IP-adresse",
|
||||||
"rulesMatchCriteriaIpAddressRange": "Samsvar et IP-adresseområde i CIDR-notasjon",
|
"rulesMatchCriteriaIpAddressRange": "Samsvar et IP-adresseområde i CIDR-notasjon",
|
||||||
@@ -833,24 +838,24 @@
|
|||||||
"pincodeRequirementsLength": "PIN må være nøyaktig 6 siffer",
|
"pincodeRequirementsLength": "PIN må være nøyaktig 6 siffer",
|
||||||
"pincodeRequirementsChars": "PIN må kun inneholde tall",
|
"pincodeRequirementsChars": "PIN må kun inneholde tall",
|
||||||
"passwordRequirementsLength": "Passord må være minst 1 tegn langt",
|
"passwordRequirementsLength": "Passord må være minst 1 tegn langt",
|
||||||
"passwordRequirementsTitle": "Password requirements:",
|
"passwordRequirementsTitle": "Passordkrav:",
|
||||||
"passwordRequirementLength": "At least 8 characters long",
|
"passwordRequirementLength": "Minst 8 tegn lang",
|
||||||
"passwordRequirementUppercase": "At least one uppercase letter",
|
"passwordRequirementUppercase": "Minst én stor bokstav",
|
||||||
"passwordRequirementLowercase": "At least one lowercase letter",
|
"passwordRequirementLowercase": "Minst én liten bokstav",
|
||||||
"passwordRequirementNumber": "At least one number",
|
"passwordRequirementNumber": "Minst ét tall",
|
||||||
"passwordRequirementSpecial": "At least one special character",
|
"passwordRequirementSpecial": "Minst ett spesialtegn",
|
||||||
"passwordRequirementsMet": "✓ Password meets all requirements",
|
"passwordRequirementsMet": "✓ Passord oppfyller alle krav",
|
||||||
"passwordStrength": "Password strength",
|
"passwordStrength": "Passordstyrke",
|
||||||
"passwordStrengthWeak": "Weak",
|
"passwordStrengthWeak": "Svakt",
|
||||||
"passwordStrengthMedium": "Medium",
|
"passwordStrengthMedium": "Medium",
|
||||||
"passwordStrengthStrong": "Strong",
|
"passwordStrengthStrong": "Sterkt",
|
||||||
"passwordRequirements": "Requirements:",
|
"passwordRequirements": "Krav:",
|
||||||
"passwordRequirementLengthText": "8+ characters",
|
"passwordRequirementLengthText": "8+ tegn",
|
||||||
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
"passwordRequirementUppercaseText": "Stor bokstav (A-Z)",
|
||||||
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
"passwordRequirementLowercaseText": "Liten bokstav (a-z)",
|
||||||
"passwordRequirementNumberText": "Number (0-9)",
|
"passwordRequirementNumberText": "Tall (0-9)",
|
||||||
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
"passwordRequirementSpecialText": "Spesialtegn (!@#$%...)",
|
||||||
"passwordsDoNotMatch": "Passwords do not match",
|
"passwordsDoNotMatch": "Passordene stemmer ikke",
|
||||||
"otpEmailRequirementsLength": "OTP må være minst 1 tegn lang.",
|
"otpEmailRequirementsLength": "OTP må være minst 1 tegn lang.",
|
||||||
"otpEmailSent": "OTP sendt",
|
"otpEmailSent": "OTP sendt",
|
||||||
"otpEmailSentDescription": "En OTP er sendt til din e-post",
|
"otpEmailSentDescription": "En OTP er sendt til din e-post",
|
||||||
@@ -970,6 +975,7 @@
|
|||||||
"logoutError": "Feil ved utlogging",
|
"logoutError": "Feil ved utlogging",
|
||||||
"signingAs": "Logget inn som",
|
"signingAs": "Logget inn som",
|
||||||
"serverAdmin": "Serveradministrator",
|
"serverAdmin": "Serveradministrator",
|
||||||
|
"managedSelfhosted": "Administrert selv-hostet",
|
||||||
"otpEnable": "Aktiver tofaktor",
|
"otpEnable": "Aktiver tofaktor",
|
||||||
"otpDisable": "Deaktiver tofaktor",
|
"otpDisable": "Deaktiver tofaktor",
|
||||||
"logout": "Logg ut",
|
"logout": "Logg ut",
|
||||||
@@ -985,9 +991,9 @@
|
|||||||
"actionDeleteSite": "Slett område",
|
"actionDeleteSite": "Slett område",
|
||||||
"actionGetSite": "Hent område",
|
"actionGetSite": "Hent område",
|
||||||
"actionListSites": "List opp områder",
|
"actionListSites": "List opp områder",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "Oppsetttoken",
|
||||||
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
"setupTokenDescription": "Skriv inn oppsetttoken fra serverkonsollen.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "Oppsetttoken er nødvendig",
|
||||||
"actionUpdateSite": "Oppdater område",
|
"actionUpdateSite": "Oppdater område",
|
||||||
"actionListSiteRoles": "List opp tillatte områderoller",
|
"actionListSiteRoles": "List opp tillatte områderoller",
|
||||||
"actionCreateResource": "Opprett ressurs",
|
"actionCreateResource": "Opprett ressurs",
|
||||||
@@ -1048,6 +1054,12 @@
|
|||||||
"actionUpdateClient": "Oppdater klient",
|
"actionUpdateClient": "Oppdater klient",
|
||||||
"actionListClients": "List klienter",
|
"actionListClients": "List klienter",
|
||||||
"actionGetClient": "Hent klient",
|
"actionGetClient": "Hent klient",
|
||||||
|
"actionCreateSiteResource": "Opprett stedsressurs",
|
||||||
|
"actionDeleteSiteResource": "Slett Stedsressurs",
|
||||||
|
"actionGetSiteResource": "Hent Stedsressurs",
|
||||||
|
"actionListSiteResources": "List opp Stedsressurser",
|
||||||
|
"actionUpdateSiteResource": "Oppdater Stedsressurs",
|
||||||
|
"actionListInvitations": "Liste invitasjoner",
|
||||||
"noneSelected": "Ingen valgt",
|
"noneSelected": "Ingen valgt",
|
||||||
"orgNotFound2": "Ingen organisasjoner funnet.",
|
"orgNotFound2": "Ingen organisasjoner funnet.",
|
||||||
"searchProgress": "Søker...",
|
"searchProgress": "Søker...",
|
||||||
@@ -1344,5 +1356,145 @@
|
|||||||
"remoteSubnetsDescription": "Legg til CIDR-områder som kan få fjerntilgang til dette området. Bruk format som 10.0.0.0/24 eller 192.168.1.0/24.",
|
"remoteSubnetsDescription": "Legg til CIDR-områder som kan få fjerntilgang til dette området. Bruk format som 10.0.0.0/24 eller 192.168.1.0/24.",
|
||||||
"resourceEnableProxy": "Aktiver offentlig proxy",
|
"resourceEnableProxy": "Aktiver offentlig proxy",
|
||||||
"resourceEnableProxyDescription": "Aktiver offentlig proxying til denne ressursen. Dette gir tilgang til ressursen fra utsiden av nettverket gjennom skyen på en åpen port. Krever Traefik-konfigurasjon.",
|
"resourceEnableProxyDescription": "Aktiver offentlig proxying til denne ressursen. Dette gir tilgang til ressursen fra utsiden av nettverket gjennom skyen på en åpen port. Krever Traefik-konfigurasjon.",
|
||||||
"externalProxyEnabled": "Ekstern proxy aktivert"
|
"externalProxyEnabled": "Ekstern proxy aktivert",
|
||||||
|
"addNewTarget": "Legg til nytt mål",
|
||||||
|
"targetsList": "Liste over mål",
|
||||||
|
"targetErrorDuplicateTargetFound": "Duplikat av mål funnet",
|
||||||
|
"httpMethod": "HTTP-metode",
|
||||||
|
"selectHttpMethod": "Velg HTTP-metode",
|
||||||
|
"domainPickerSubdomainLabel": "Underdomene",
|
||||||
|
"domainPickerBaseDomainLabel": "Grunndomene",
|
||||||
|
"domainPickerSearchDomains": "Søk i domener...",
|
||||||
|
"domainPickerNoDomainsFound": "Ingen domener funnet",
|
||||||
|
"domainPickerLoadingDomains": "Laster inn domener...",
|
||||||
|
"domainPickerSelectBaseDomain": "Velg grunndomene...",
|
||||||
|
"domainPickerNotAvailableForCname": "Ikke tilgjengelig for CNAME-domener",
|
||||||
|
"domainPickerEnterSubdomainOrLeaveBlank": "Skriv inn underdomene eller la feltet stå tomt for å bruke grunndomene.",
|
||||||
|
"domainPickerEnterSubdomainToSearch": "Skriv inn et underdomene for å søke og velge blant tilgjengelige gratis domener.",
|
||||||
|
"domainPickerFreeDomains": "Gratis domener",
|
||||||
|
"domainPickerSearchForAvailableDomains": "Søk etter tilgjengelige domener",
|
||||||
|
"resourceDomain": "Domene",
|
||||||
|
"resourceEditDomain": "Rediger domene",
|
||||||
|
"siteName": "Områdenavn",
|
||||||
|
"proxyPort": "Port",
|
||||||
|
"resourcesTableProxyResources": "Proxy-ressurser",
|
||||||
|
"resourcesTableClientResources": "Klientressurser",
|
||||||
|
"resourcesTableNoProxyResourcesFound": "Ingen proxy-ressurser funnet.",
|
||||||
|
"resourcesTableNoInternalResourcesFound": "Ingen interne ressurser funnet.",
|
||||||
|
"resourcesTableDestination": "Destinasjon",
|
||||||
|
"resourcesTableTheseResourcesForUseWith": "Disse ressursene er til bruk med",
|
||||||
|
"resourcesTableClients": "Klienter",
|
||||||
|
"resourcesTableAndOnlyAccessibleInternally": "og er kun tilgjengelig internt når de er koblet til med en klient.",
|
||||||
|
"editInternalResourceDialogEditClientResource": "Rediger klientressurs",
|
||||||
|
"editInternalResourceDialogUpdateResourceProperties": "Oppdater ressursens egenskaper og målkonfigurasjon for {resourceName}.",
|
||||||
|
"editInternalResourceDialogResourceProperties": "Ressursegenskaper",
|
||||||
|
"editInternalResourceDialogName": "Navn",
|
||||||
|
"editInternalResourceDialogProtocol": "Protokoll",
|
||||||
|
"editInternalResourceDialogSitePort": "Områdeport",
|
||||||
|
"editInternalResourceDialogTargetConfiguration": "Målkonfigurasjon",
|
||||||
|
"editInternalResourceDialogDestinationIP": "Destinasjons-IP",
|
||||||
|
"editInternalResourceDialogDestinationPort": "Destinasjonsport",
|
||||||
|
"editInternalResourceDialogCancel": "Avbryt",
|
||||||
|
"editInternalResourceDialogSaveResource": "Lagre ressurs",
|
||||||
|
"editInternalResourceDialogSuccess": "Suksess",
|
||||||
|
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Intern ressurs oppdatert vellykket",
|
||||||
|
"editInternalResourceDialogError": "Feil",
|
||||||
|
"editInternalResourceDialogFailedToUpdateInternalResource": "Mislyktes å oppdatere intern ressurs",
|
||||||
|
"editInternalResourceDialogNameRequired": "Navn er påkrevd",
|
||||||
|
"editInternalResourceDialogNameMaxLength": "Navn kan ikke være lengre enn 255 tegn",
|
||||||
|
"editInternalResourceDialogProxyPortMin": "Proxy-port må være minst 1",
|
||||||
|
"editInternalResourceDialogProxyPortMax": "Proxy-port må være mindre enn 65536",
|
||||||
|
"editInternalResourceDialogInvalidIPAddressFormat": "Ugyldig IP-adresseformat",
|
||||||
|
"editInternalResourceDialogDestinationPortMin": "Destinasjonsport må være minst 1",
|
||||||
|
"editInternalResourceDialogDestinationPortMax": "Destinasjonsport må være mindre enn 65536",
|
||||||
|
"createInternalResourceDialogNoSitesAvailable": "Ingen tilgjengelige steder",
|
||||||
|
"createInternalResourceDialogNoSitesAvailableDescription": "Du må ha minst ett Newt-område med et konfigureret delnett for å lage interne ressurser.",
|
||||||
|
"createInternalResourceDialogClose": "Lukk",
|
||||||
|
"createInternalResourceDialogCreateClientResource": "Opprett klientressurs",
|
||||||
|
"createInternalResourceDialogCreateClientResourceDescription": "Lag en ny ressurs som blir tilgjengelig for klienter koblet til det valgte området.",
|
||||||
|
"createInternalResourceDialogResourceProperties": "Ressursegenskaper",
|
||||||
|
"createInternalResourceDialogName": "Navn",
|
||||||
|
"createInternalResourceDialogSite": "Område",
|
||||||
|
"createInternalResourceDialogSelectSite": "Velg område...",
|
||||||
|
"createInternalResourceDialogSearchSites": "Søk i områder...",
|
||||||
|
"createInternalResourceDialogNoSitesFound": "Ingen områder funnet.",
|
||||||
|
"createInternalResourceDialogProtocol": "Protokoll",
|
||||||
|
"createInternalResourceDialogTcp": "TCP",
|
||||||
|
"createInternalResourceDialogUdp": "UDP",
|
||||||
|
"createInternalResourceDialogSitePort": "Områdeport",
|
||||||
|
"createInternalResourceDialogSitePortDescription": "Bruk denne porten for å få tilgang til ressursen på området når du er tilkoblet med en klient.",
|
||||||
|
"createInternalResourceDialogTargetConfiguration": "Målkonfigurasjon",
|
||||||
|
"createInternalResourceDialogDestinationIP": "Destinasjons-IP",
|
||||||
|
"createInternalResourceDialogDestinationIPDescription": "IP-adressen til ressursen på områdets nettverk.",
|
||||||
|
"createInternalResourceDialogDestinationPort": "Destinasjonsport",
|
||||||
|
"createInternalResourceDialogDestinationPortDescription": "Porten på destinasjons-IP-en der ressursen kan nås.",
|
||||||
|
"createInternalResourceDialogCancel": "Avbryt",
|
||||||
|
"createInternalResourceDialogCreateResource": "Opprett ressurs",
|
||||||
|
"createInternalResourceDialogSuccess": "Suksess",
|
||||||
|
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Intern ressurs opprettet vellykket",
|
||||||
|
"createInternalResourceDialogError": "Feil",
|
||||||
|
"createInternalResourceDialogFailedToCreateInternalResource": "Kunne ikke opprette intern ressurs",
|
||||||
|
"createInternalResourceDialogNameRequired": "Navn er påkrevd",
|
||||||
|
"createInternalResourceDialogNameMaxLength": "Navn kan ikke være lengre enn 255 tegn",
|
||||||
|
"createInternalResourceDialogPleaseSelectSite": "Vennligst velg et område",
|
||||||
|
"createInternalResourceDialogProxyPortMin": "Proxy-port må være minst 1",
|
||||||
|
"createInternalResourceDialogProxyPortMax": "Proxy-port må være mindre enn 65536",
|
||||||
|
"createInternalResourceDialogInvalidIPAddressFormat": "Ugyldig IP-adresseformat",
|
||||||
|
"createInternalResourceDialogDestinationPortMin": "Destinasjonsport må være minst 1",
|
||||||
|
"createInternalResourceDialogDestinationPortMax": "Destinasjonsport må være mindre enn 65536",
|
||||||
|
"siteConfiguration": "Konfigurasjon",
|
||||||
|
"siteAcceptClientConnections": "Godta klientforbindelser",
|
||||||
|
"siteAcceptClientConnectionsDescription": "Tillat andre enheter å koble seg til gjennom denne Newt-instansen som en gateway ved hjelp av klienter.",
|
||||||
|
"siteAddress": "Områdeadresse",
|
||||||
|
"siteAddressDescription": "Angi IP-adressen til verten for klienter å koble seg til. Dette er den interne adressen til området i Pangolin-nettverket for klienter som adresserer. Må falle innenfor Org-underettet.",
|
||||||
|
"autoLoginExternalIdp": "Automatisk innlogging med ekstern IDP",
|
||||||
|
"autoLoginExternalIdpDescription": "Omdiriger brukeren umiddelbart til den eksterne IDP-en for autentisering.",
|
||||||
|
"selectIdp": "Velg IDP",
|
||||||
|
"selectIdpPlaceholder": "Velg en IDP...",
|
||||||
|
"selectIdpRequired": "Vennligst velg en IDP når automatisk innlogging er aktivert.",
|
||||||
|
"autoLoginTitle": "Omdirigering",
|
||||||
|
"autoLoginDescription": "Omdirigerer deg til den eksterne identitetsleverandøren for autentisering.",
|
||||||
|
"autoLoginProcessing": "Forbereder autentisering...",
|
||||||
|
"autoLoginRedirecting": "Omdirigerer til innlogging...",
|
||||||
|
"autoLoginError": "Feil ved automatisk innlogging",
|
||||||
|
"autoLoginErrorNoRedirectUrl": "Ingen omdirigerings-URL mottatt fra identitetsleverandøren.",
|
||||||
|
"autoLoginErrorGeneratingUrl": "Kunne ikke generere autentiserings-URL.",
|
||||||
|
"managedSelfHosted": {
|
||||||
|
"title": "Administrert selv-hostet",
|
||||||
|
"description": "Sikre og lavvedlikeholdsservere, selvbetjente Pangolin med ekstra klokker, og understell",
|
||||||
|
"introTitle": "Administrert Self-Hosted Pangolin",
|
||||||
|
"introDescription": "er et alternativ for bruk utviklet for personer som ønsker enkel og ekstra pålitelighet mens de fortsatt holder sine data privat og selvdrevne.",
|
||||||
|
"introDetail": "Med dette valget kjører du fortsatt din egen Pangolin-node - tunneler, SSL-terminering og trafikken ligger på serveren din. Forskjellen er at behandling og overvåking håndteres gjennom vårt skydashbord, som låser opp en rekke fordeler:",
|
||||||
|
"benefitSimplerOperations": {
|
||||||
|
"title": "Enklere operasjoner",
|
||||||
|
"description": "Ingen grunn til å kjøre din egen e-postserver eller sette opp kompleks varsling. Du vil få helsesjekk og nedetid varsler ut av boksen."
|
||||||
|
},
|
||||||
|
"benefitAutomaticUpdates": {
|
||||||
|
"title": "Automatiske oppdateringer",
|
||||||
|
"description": "Cloud dashbordet utvikler seg raskt, så du får nye funksjoner og feilrettinger uten at du trenger å trekke nye beholdere manuelt hver gang."
|
||||||
|
},
|
||||||
|
"benefitLessMaintenance": {
|
||||||
|
"title": "Mindre vedlikehold",
|
||||||
|
"description": "Ingen databasestyrer, sikkerhetskopier eller ekstra infrastruktur for å forvalte. Vi håndterer det i skyen."
|
||||||
|
},
|
||||||
|
"benefitCloudFailover": {
|
||||||
|
"title": "Sky feilslått",
|
||||||
|
"description": "Hvis EK-gruppen din går ned, kan tunnlene midlertidig mislykkes i å nå våre sky-punkter til du tar den tilbake på nett."
|
||||||
|
},
|
||||||
|
"benefitHighAvailability": {
|
||||||
|
"title": "Høy tilgjengelighet (PoPs)",
|
||||||
|
"description": "Du kan også legge ved flere noder til kontoen din for redundans og bedre ytelse."
|
||||||
|
},
|
||||||
|
"benefitFutureEnhancements": {
|
||||||
|
"title": "Fremtidige forbedringer",
|
||||||
|
"description": "Vi planlegger å legge inn mer analyser, varsle og styringsverktøy for å gjøre din distribusjon enda mer robust."
|
||||||
|
},
|
||||||
|
"docsAlert": {
|
||||||
|
"text": "Lær mer om Managed Self-Hosted alternativet i vår",
|
||||||
|
"documentation": "dokumentasjon"
|
||||||
|
},
|
||||||
|
"convertButton": "Konverter denne noden til manuelt bruk"
|
||||||
|
},
|
||||||
|
"internationaldomaindetected": "Internasjonalt domene oppdaget",
|
||||||
|
"willbestoredas": "Vil bli lagret som:"
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,9 @@
|
|||||||
"siteNewtTunnelDescription": "Gemakkelijkste manier om een ingangspunt in uw netwerk te maken. Geen extra opzet.",
|
"siteNewtTunnelDescription": "Gemakkelijkste manier om een ingangspunt in uw netwerk te maken. Geen extra opzet.",
|
||||||
"siteWg": "Basis WireGuard",
|
"siteWg": "Basis WireGuard",
|
||||||
"siteWgDescription": "Gebruik een WireGuard client om een tunnel te bouwen. Handmatige NAT installatie vereist.",
|
"siteWgDescription": "Gebruik een WireGuard client om een tunnel te bouwen. Handmatige NAT installatie vereist.",
|
||||||
|
"siteWgDescriptionSaas": "Gebruik elke WireGuard-client om een tunnel op te zetten. Handmatige NAT-instelling vereist. WERKT ALLEEN OP SELF HOSTED NODES",
|
||||||
"siteLocalDescription": "Alleen lokale bronnen. Geen tunneling.",
|
"siteLocalDescription": "Alleen lokale bronnen. Geen tunneling.",
|
||||||
|
"siteLocalDescriptionSaas": "Alleen lokale bronnen. Geen tunneling. WERKT ALLEEN OP SELF HOSTED NODES",
|
||||||
"siteSeeAll": "Alle werkruimtes bekijken",
|
"siteSeeAll": "Alle werkruimtes bekijken",
|
||||||
"siteTunnelDescription": "Bepaal hoe u verbinding wilt maken met uw site",
|
"siteTunnelDescription": "Bepaal hoe u verbinding wilt maken met uw site",
|
||||||
"siteNewtCredentials": "Nieuwste aanmeldgegevens",
|
"siteNewtCredentials": "Nieuwste aanmeldgegevens",
|
||||||
@@ -166,7 +168,7 @@
|
|||||||
"siteSelect": "Selecteer site",
|
"siteSelect": "Selecteer site",
|
||||||
"siteSearch": "Zoek site",
|
"siteSearch": "Zoek site",
|
||||||
"siteNotFound": "Geen site gevonden.",
|
"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",
|
"resourceType": "Type bron",
|
||||||
"resourceTypeDescription": "Bepaal hoe u toegang wilt krijgen tot uw bron",
|
"resourceTypeDescription": "Bepaal hoe u toegang wilt krijgen tot uw bron",
|
||||||
"resourceHTTPSSettings": "HTTPS instellingen",
|
"resourceHTTPSSettings": "HTTPS instellingen",
|
||||||
@@ -197,11 +199,13 @@
|
|||||||
"general": "Algemeen",
|
"general": "Algemeen",
|
||||||
"generalSettings": "Algemene instellingen",
|
"generalSettings": "Algemene instellingen",
|
||||||
"proxy": "Proxy",
|
"proxy": "Proxy",
|
||||||
|
"internal": "Intern",
|
||||||
"rules": "Regels",
|
"rules": "Regels",
|
||||||
"resourceSettingDescription": "Configureer de instellingen op uw bron",
|
"resourceSettingDescription": "Configureer de instellingen op uw bron",
|
||||||
"resourceSetting": "{resourceName} instellingen",
|
"resourceSetting": "{resourceName} instellingen",
|
||||||
"alwaysAllow": "Altijd toestaan",
|
"alwaysAllow": "Altijd toestaan",
|
||||||
"alwaysDeny": "Altijd weigeren",
|
"alwaysDeny": "Altijd weigeren",
|
||||||
|
"passToAuth": "Passeren naar Auth",
|
||||||
"orgSettingsDescription": "Configureer de algemene instellingen van je organisatie",
|
"orgSettingsDescription": "Configureer de algemene instellingen van je organisatie",
|
||||||
"orgGeneralSettings": "Organisatie Instellingen",
|
"orgGeneralSettings": "Organisatie Instellingen",
|
||||||
"orgGeneralSettingsDescription": "Beheer de details en configuratie van uw organisatie",
|
"orgGeneralSettingsDescription": "Beheer de details en configuratie van uw organisatie",
|
||||||
@@ -490,7 +494,7 @@
|
|||||||
"targetTlsSniDescription": "De TLS servernaam om te gebruiken voor SNI. Laat leeg om de standaard te gebruiken.",
|
"targetTlsSniDescription": "De TLS servernaam om te gebruiken voor SNI. Laat leeg om de standaard te gebruiken.",
|
||||||
"targetTlsSubmit": "Instellingen opslaan",
|
"targetTlsSubmit": "Instellingen opslaan",
|
||||||
"targets": "Doelstellingen configuratie",
|
"targets": "Doelstellingen configuratie",
|
||||||
"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",
|
"targetStickySessions": "Sticky sessies inschakelen",
|
||||||
"targetStickySessionsDescription": "Behoud verbindingen op hetzelfde backend doel voor hun hele sessie.",
|
"targetStickySessionsDescription": "Behoud verbindingen op hetzelfde backend doel voor hun hele sessie.",
|
||||||
"methodSelect": "Selecteer methode",
|
"methodSelect": "Selecteer methode",
|
||||||
@@ -542,6 +546,7 @@
|
|||||||
"rulesActions": "acties",
|
"rulesActions": "acties",
|
||||||
"rulesActionAlwaysAllow": "Altijd toegestaan: Omzeil alle authenticatiemethoden",
|
"rulesActionAlwaysAllow": "Altijd toegestaan: Omzeil alle authenticatiemethoden",
|
||||||
"rulesActionAlwaysDeny": "Altijd weigeren: Blokkeer alle aanvragen, er kan geen verificatie worden geprobeerd",
|
"rulesActionAlwaysDeny": "Altijd weigeren: Blokkeer alle aanvragen, er kan geen verificatie worden geprobeerd",
|
||||||
|
"rulesActionPassToAuth": "Doorgeven aan Auth: Toestaan dat authenticatiemethoden worden geprobeerd",
|
||||||
"rulesMatchCriteria": "Overeenkomende criteria",
|
"rulesMatchCriteria": "Overeenkomende criteria",
|
||||||
"rulesMatchCriteriaIpAddress": "Overeenkomen met een specifiek IP-adres",
|
"rulesMatchCriteriaIpAddress": "Overeenkomen met een specifiek IP-adres",
|
||||||
"rulesMatchCriteriaIpAddressRange": "Overeenkomen met een bereik van IP-adressen in de CIDR-notatie",
|
"rulesMatchCriteriaIpAddressRange": "Overeenkomen met een bereik van IP-adressen in de CIDR-notatie",
|
||||||
@@ -833,24 +838,24 @@
|
|||||||
"pincodeRequirementsLength": "Pincode moet precies 6 cijfers zijn",
|
"pincodeRequirementsLength": "Pincode moet precies 6 cijfers zijn",
|
||||||
"pincodeRequirementsChars": "Pincode mag alleen cijfers bevatten",
|
"pincodeRequirementsChars": "Pincode mag alleen cijfers bevatten",
|
||||||
"passwordRequirementsLength": "Wachtwoord moet ten minste 1 teken lang zijn",
|
"passwordRequirementsLength": "Wachtwoord moet ten minste 1 teken lang zijn",
|
||||||
"passwordRequirementsTitle": "Password requirements:",
|
"passwordRequirementsTitle": "Wachtwoordvereisten:",
|
||||||
"passwordRequirementLength": "At least 8 characters long",
|
"passwordRequirementLength": "Minstens 8 tekens lang",
|
||||||
"passwordRequirementUppercase": "At least one uppercase letter",
|
"passwordRequirementUppercase": "Minstens één hoofdletter",
|
||||||
"passwordRequirementLowercase": "At least one lowercase letter",
|
"passwordRequirementLowercase": "Minstens één kleine letter",
|
||||||
"passwordRequirementNumber": "At least one number",
|
"passwordRequirementNumber": "Minstens één cijfer",
|
||||||
"passwordRequirementSpecial": "At least one special character",
|
"passwordRequirementSpecial": "Minstens één speciaal teken",
|
||||||
"passwordRequirementsMet": "✓ Password meets all requirements",
|
"passwordRequirementsMet": "✓ Wachtwoord voldoet aan alle vereisten",
|
||||||
"passwordStrength": "Password strength",
|
"passwordStrength": "Wachtwoord sterkte",
|
||||||
"passwordStrengthWeak": "Weak",
|
"passwordStrengthWeak": "Zwak",
|
||||||
"passwordStrengthMedium": "Medium",
|
"passwordStrengthMedium": "Gemiddeld",
|
||||||
"passwordStrengthStrong": "Strong",
|
"passwordStrengthStrong": "Sterk",
|
||||||
"passwordRequirements": "Requirements:",
|
"passwordRequirements": "Vereisten:",
|
||||||
"passwordRequirementLengthText": "8+ characters",
|
"passwordRequirementLengthText": "8+ tekens",
|
||||||
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
"passwordRequirementUppercaseText": "Hoofdletter (A-Z)",
|
||||||
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
"passwordRequirementLowercaseText": "Kleine letter (a-z)",
|
||||||
"passwordRequirementNumberText": "Number (0-9)",
|
"passwordRequirementNumberText": "Cijfer (0-9)",
|
||||||
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
"passwordRequirementSpecialText": "Speciaal teken (!@#$%...)",
|
||||||
"passwordsDoNotMatch": "Passwords do not match",
|
"passwordsDoNotMatch": "Wachtwoorden komen niet overeen",
|
||||||
"otpEmailRequirementsLength": "OTP moet minstens 1 teken lang zijn",
|
"otpEmailRequirementsLength": "OTP moet minstens 1 teken lang zijn",
|
||||||
"otpEmailSent": "OTP verzonden",
|
"otpEmailSent": "OTP verzonden",
|
||||||
"otpEmailSentDescription": "Een OTP is naar uw e-mail verzonden",
|
"otpEmailSentDescription": "Een OTP is naar uw e-mail verzonden",
|
||||||
@@ -970,6 +975,7 @@
|
|||||||
"logoutError": "Fout bij uitloggen",
|
"logoutError": "Fout bij uitloggen",
|
||||||
"signingAs": "Ingelogd als",
|
"signingAs": "Ingelogd als",
|
||||||
"serverAdmin": "Server Beheerder",
|
"serverAdmin": "Server Beheerder",
|
||||||
|
"managedSelfhosted": "Beheerde Self-Hosted",
|
||||||
"otpEnable": "Twee-factor inschakelen",
|
"otpEnable": "Twee-factor inschakelen",
|
||||||
"otpDisable": "Tweestapsverificatie uitschakelen",
|
"otpDisable": "Tweestapsverificatie uitschakelen",
|
||||||
"logout": "Log uit",
|
"logout": "Log uit",
|
||||||
@@ -986,8 +992,8 @@
|
|||||||
"actionGetSite": "Site ophalen",
|
"actionGetSite": "Site ophalen",
|
||||||
"actionListSites": "Sites weergeven",
|
"actionListSites": "Sites weergeven",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "Setup Token",
|
||||||
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
"setupTokenDescription": "Voer het setup-token in vanaf de serverconsole.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "Setup-token is vereist",
|
||||||
"actionUpdateSite": "Site bijwerken",
|
"actionUpdateSite": "Site bijwerken",
|
||||||
"actionListSiteRoles": "Toon toegestane sitenollen",
|
"actionListSiteRoles": "Toon toegestane sitenollen",
|
||||||
"actionCreateResource": "Bron maken",
|
"actionCreateResource": "Bron maken",
|
||||||
@@ -1048,6 +1054,12 @@
|
|||||||
"actionUpdateClient": "Klant bijwerken",
|
"actionUpdateClient": "Klant bijwerken",
|
||||||
"actionListClients": "Lijst klanten",
|
"actionListClients": "Lijst klanten",
|
||||||
"actionGetClient": "Client ophalen",
|
"actionGetClient": "Client ophalen",
|
||||||
|
"actionCreateSiteResource": "Sitebron maken",
|
||||||
|
"actionDeleteSiteResource": "Document verwijderen van site",
|
||||||
|
"actionGetSiteResource": "Bron van site ophalen",
|
||||||
|
"actionListSiteResources": "Bronnen van site weergeven",
|
||||||
|
"actionUpdateSiteResource": "Document bijwerken van site",
|
||||||
|
"actionListInvitations": "Toon uitnodigingen",
|
||||||
"noneSelected": "Niet geselecteerd",
|
"noneSelected": "Niet geselecteerd",
|
||||||
"orgNotFound2": "Geen organisaties gevonden.",
|
"orgNotFound2": "Geen organisaties gevonden.",
|
||||||
"searchProgress": "Zoeken...",
|
"searchProgress": "Zoeken...",
|
||||||
@@ -1341,8 +1353,148 @@
|
|||||||
"olmErrorFetchLatest": "Er is een fout opgetreden bij het ophalen van de nieuwste Olm release.",
|
"olmErrorFetchLatest": "Er is een fout opgetreden bij het ophalen van de nieuwste Olm release.",
|
||||||
"remoteSubnets": "Externe Subnets",
|
"remoteSubnets": "Externe Subnets",
|
||||||
"enterCidrRange": "Voer CIDR-bereik in",
|
"enterCidrRange": "Voer CIDR-bereik in",
|
||||||
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
"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",
|
"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.",
|
"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"
|
"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.",
|
||||||
|
"managedSelfHosted": {
|
||||||
|
"title": "Beheerde Self-Hosted",
|
||||||
|
"description": "betrouwbaardere en slecht onderhouden Pangolin server met extra klokken en klokkenluiders",
|
||||||
|
"introTitle": "Beheerde zelfgehoste pangolin",
|
||||||
|
"introDescription": "is een implementatieoptie ontworpen voor mensen die eenvoud en extra betrouwbaarheid willen, terwijl hun gegevens privé en zelf georganiseerd blijven.",
|
||||||
|
"introDetail": "Met deze optie beheert u nog steeds uw eigen Pangolin node - uw tunnels, SSL-verbinding en verkeer alles op uw server. Het verschil is dat beheer en monitoring worden behandeld via onze cloud dashboard, wat een aantal voordelen oplevert:",
|
||||||
|
"benefitSimplerOperations": {
|
||||||
|
"title": "Simpler operaties",
|
||||||
|
"description": "Je hoeft geen eigen mailserver te draaien of complexe waarschuwingen in te stellen. Je krijgt gezondheidscontroles en downtime meldingen uit de box."
|
||||||
|
},
|
||||||
|
"benefitAutomaticUpdates": {
|
||||||
|
"title": "Automatische updates",
|
||||||
|
"description": "Het cloud dashboard evolueert snel, zodat u nieuwe functies en bug fixes krijgt zonder elke keer handmatig nieuwe containers te moeten trekken."
|
||||||
|
},
|
||||||
|
"benefitLessMaintenance": {
|
||||||
|
"title": "Minder onderhoud",
|
||||||
|
"description": "Geen database migratie, back-ups of extra infrastructuur om te beheren. Dat behandelen we in de cloud."
|
||||||
|
},
|
||||||
|
"benefitCloudFailover": {
|
||||||
|
"title": "Cloud fout",
|
||||||
|
"description": "Als uw node omlaag gaat, kunnen uw tunnels tijdelijk niet meer naar onze aanwezigheidspunten gaan totdat u hem weer online brengt."
|
||||||
|
},
|
||||||
|
"benefitHighAvailability": {
|
||||||
|
"title": "Hoge beschikbaarheid (PoPs)",
|
||||||
|
"description": "U kunt ook meerdere nodes koppelen aan uw account voor ontslag en betere prestaties."
|
||||||
|
},
|
||||||
|
"benefitFutureEnhancements": {
|
||||||
|
"title": "Toekomstige verbeteringen",
|
||||||
|
"description": "We zijn van plan om meer analytica, waarschuwing en beheerhulpmiddelen toe te voegen om uw implementatie nog steviger te maken."
|
||||||
|
},
|
||||||
|
"docsAlert": {
|
||||||
|
"text": "Meer informatie over de optie voor zelf-verzorging in onze",
|
||||||
|
"documentation": "documentatie"
|
||||||
|
},
|
||||||
|
"convertButton": "Converteer deze node naar Beheerde Zelf-Hosted"
|
||||||
|
},
|
||||||
|
"internationaldomaindetected": "Internationaal Domein Gedetecteerd",
|
||||||
|
"willbestoredas": "Zal worden opgeslagen als:"
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,9 @@
|
|||||||
"siteNewtTunnelDescription": "Łatwiejszy sposób na stworzenie punktu wejścia w sieci. Nie ma dodatkowej konfiguracji.",
|
"siteNewtTunnelDescription": "Łatwiejszy sposób na stworzenie punktu wejścia w sieci. Nie ma dodatkowej konfiguracji.",
|
||||||
"siteWg": "Podstawowy WireGuard",
|
"siteWg": "Podstawowy WireGuard",
|
||||||
"siteWgDescription": "Użyj dowolnego klienta WireGuard do utworzenia tunelu. Wymagana jest ręczna konfiguracja NAT.",
|
"siteWgDescription": "Użyj dowolnego klienta WireGuard do utworzenia tunelu. Wymagana jest ręczna konfiguracja NAT.",
|
||||||
|
"siteWgDescriptionSaas": "Użyj dowolnego klienta WireGuard do utworzenia tunelu. Wymagana ręczna konfiguracja NAT. DZIAŁA TYLKO NA SAMODZIELNIE HOSTOWANYCH WĘZŁACH",
|
||||||
"siteLocalDescription": "Tylko lokalne zasoby. Brak tunelu.",
|
"siteLocalDescription": "Tylko lokalne zasoby. Brak tunelu.",
|
||||||
|
"siteLocalDescriptionSaas": "Tylko zasoby lokalne. Brak tunelowania. DZIAŁA TYLKO NA SAMODZIELNIE HOSTOWANYCH WĘZŁACH",
|
||||||
"siteSeeAll": "Zobacz wszystkie witryny",
|
"siteSeeAll": "Zobacz wszystkie witryny",
|
||||||
"siteTunnelDescription": "Określ jak chcesz połączyć się ze swoją stroną",
|
"siteTunnelDescription": "Określ jak chcesz połączyć się ze swoją stroną",
|
||||||
"siteNewtCredentials": "Aktualne dane logowania",
|
"siteNewtCredentials": "Aktualne dane logowania",
|
||||||
@@ -166,7 +168,7 @@
|
|||||||
"siteSelect": "Wybierz witrynę",
|
"siteSelect": "Wybierz witrynę",
|
||||||
"siteSearch": "Szukaj witryny",
|
"siteSearch": "Szukaj witryny",
|
||||||
"siteNotFound": "Nie znaleziono witryny.",
|
"siteNotFound": "Nie znaleziono witryny.",
|
||||||
"siteSelectionDescription": "Ta strona zapewni połączenie z zasobem.",
|
"siteSelectionDescription": "Ta strona zapewni połączenie z celem.",
|
||||||
"resourceType": "Typ zasobu",
|
"resourceType": "Typ zasobu",
|
||||||
"resourceTypeDescription": "Określ jak chcesz uzyskać dostęp do swojego zasobu",
|
"resourceTypeDescription": "Określ jak chcesz uzyskać dostęp do swojego zasobu",
|
||||||
"resourceHTTPSSettings": "Ustawienia HTTPS",
|
"resourceHTTPSSettings": "Ustawienia HTTPS",
|
||||||
@@ -197,11 +199,13 @@
|
|||||||
"general": "Ogólny",
|
"general": "Ogólny",
|
||||||
"generalSettings": "Ustawienia ogólne",
|
"generalSettings": "Ustawienia ogólne",
|
||||||
"proxy": "Serwer pośredniczący",
|
"proxy": "Serwer pośredniczący",
|
||||||
|
"internal": "Wewętrzny",
|
||||||
"rules": "Regulamin",
|
"rules": "Regulamin",
|
||||||
"resourceSettingDescription": "Skonfiguruj ustawienia zasobu",
|
"resourceSettingDescription": "Skonfiguruj ustawienia zasobu",
|
||||||
"resourceSetting": "Ustawienia {resourceName}",
|
"resourceSetting": "Ustawienia {resourceName}",
|
||||||
"alwaysAllow": "Zawsze zezwalaj",
|
"alwaysAllow": "Zawsze zezwalaj",
|
||||||
"alwaysDeny": "Zawsze odmawiaj",
|
"alwaysDeny": "Zawsze odmawiaj",
|
||||||
|
"passToAuth": "Przekaż do Autoryzacji",
|
||||||
"orgSettingsDescription": "Skonfiguruj ustawienia ogólne swojej organizacji",
|
"orgSettingsDescription": "Skonfiguruj ustawienia ogólne swojej organizacji",
|
||||||
"orgGeneralSettings": "Ustawienia organizacji",
|
"orgGeneralSettings": "Ustawienia organizacji",
|
||||||
"orgGeneralSettingsDescription": "Zarządzaj szczegółami swojej organizacji i konfiguracją",
|
"orgGeneralSettingsDescription": "Zarządzaj szczegółami swojej organizacji i konfiguracją",
|
||||||
@@ -490,7 +494,7 @@
|
|||||||
"targetTlsSniDescription": "Nazwa serwera TLS do użycia dla SNI. Pozostaw puste, aby użyć domyślnej.",
|
"targetTlsSniDescription": "Nazwa serwera TLS do użycia dla SNI. Pozostaw puste, aby użyć domyślnej.",
|
||||||
"targetTlsSubmit": "Zapisz ustawienia",
|
"targetTlsSubmit": "Zapisz ustawienia",
|
||||||
"targets": "Konfiguracja celów",
|
"targets": "Konfiguracja celów",
|
||||||
"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",
|
"targetStickySessions": "Włącz sesje trwałe",
|
||||||
"targetStickySessionsDescription": "Utrzymuj połączenia na tym samym celu backendowym przez całą sesję.",
|
"targetStickySessionsDescription": "Utrzymuj połączenia na tym samym celu backendowym przez całą sesję.",
|
||||||
"methodSelect": "Wybierz metodę",
|
"methodSelect": "Wybierz metodę",
|
||||||
@@ -542,6 +546,7 @@
|
|||||||
"rulesActions": "Akcje",
|
"rulesActions": "Akcje",
|
||||||
"rulesActionAlwaysAllow": "Zawsze zezwalaj: Pomiń wszystkie metody uwierzytelniania",
|
"rulesActionAlwaysAllow": "Zawsze zezwalaj: Pomiń wszystkie metody uwierzytelniania",
|
||||||
"rulesActionAlwaysDeny": "Zawsze odmawiaj: Blokuj wszystkie żądania; nie można próbować uwierzytelniania",
|
"rulesActionAlwaysDeny": "Zawsze odmawiaj: Blokuj wszystkie żądania; nie można próbować uwierzytelniania",
|
||||||
|
"rulesActionPassToAuth": "Przekaż do Autoryzacji: Zezwól na próby metod uwierzytelniania",
|
||||||
"rulesMatchCriteria": "Kryteria dopasowania",
|
"rulesMatchCriteria": "Kryteria dopasowania",
|
||||||
"rulesMatchCriteriaIpAddress": "Dopasuj konkretny adres IP",
|
"rulesMatchCriteriaIpAddress": "Dopasuj konkretny adres IP",
|
||||||
"rulesMatchCriteriaIpAddressRange": "Dopasuj zakres adresów IP w notacji CIDR",
|
"rulesMatchCriteriaIpAddressRange": "Dopasuj zakres adresów IP w notacji CIDR",
|
||||||
@@ -833,24 +838,24 @@
|
|||||||
"pincodeRequirementsLength": "PIN musi składać się dokładnie z 6 cyfr",
|
"pincodeRequirementsLength": "PIN musi składać się dokładnie z 6 cyfr",
|
||||||
"pincodeRequirementsChars": "PIN może zawierać tylko cyfry",
|
"pincodeRequirementsChars": "PIN może zawierać tylko cyfry",
|
||||||
"passwordRequirementsLength": "Hasło musi mieć co najmniej 1 znak",
|
"passwordRequirementsLength": "Hasło musi mieć co najmniej 1 znak",
|
||||||
"passwordRequirementsTitle": "Password requirements:",
|
"passwordRequirementsTitle": "Wymagania dotyczące hasła:",
|
||||||
"passwordRequirementLength": "At least 8 characters long",
|
"passwordRequirementLength": "Przynajmniej 8 znaków długości",
|
||||||
"passwordRequirementUppercase": "At least one uppercase letter",
|
"passwordRequirementUppercase": "Przynajmniej jedna wielka litera",
|
||||||
"passwordRequirementLowercase": "At least one lowercase letter",
|
"passwordRequirementLowercase": "Przynajmniej jedna mała litera",
|
||||||
"passwordRequirementNumber": "At least one number",
|
"passwordRequirementNumber": "Przynajmniej jedna cyfra",
|
||||||
"passwordRequirementSpecial": "At least one special character",
|
"passwordRequirementSpecial": "Przynajmniej jeden znak specjalny",
|
||||||
"passwordRequirementsMet": "✓ Password meets all requirements",
|
"passwordRequirementsMet": "✓ Hasło spełnia wszystkie wymagania",
|
||||||
"passwordStrength": "Password strength",
|
"passwordStrength": "Siła hasła",
|
||||||
"passwordStrengthWeak": "Weak",
|
"passwordStrengthWeak": "Słabe",
|
||||||
"passwordStrengthMedium": "Medium",
|
"passwordStrengthMedium": "Średnie",
|
||||||
"passwordStrengthStrong": "Strong",
|
"passwordStrengthStrong": "Silne",
|
||||||
"passwordRequirements": "Requirements:",
|
"passwordRequirements": "Wymagania:",
|
||||||
"passwordRequirementLengthText": "8+ characters",
|
"passwordRequirementLengthText": "8+ znaków",
|
||||||
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
"passwordRequirementUppercaseText": "Wielka litera (A-Z)",
|
||||||
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
"passwordRequirementLowercaseText": "Mała litera (a-z)",
|
||||||
"passwordRequirementNumberText": "Number (0-9)",
|
"passwordRequirementNumberText": "Cyfra (0-9)",
|
||||||
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
"passwordRequirementSpecialText": "Znak specjalny (!@#$%...)",
|
||||||
"passwordsDoNotMatch": "Passwords do not match",
|
"passwordsDoNotMatch": "Hasła nie są zgodne",
|
||||||
"otpEmailRequirementsLength": "Kod jednorazowy musi mieć co najmniej 1 znak",
|
"otpEmailRequirementsLength": "Kod jednorazowy musi mieć co najmniej 1 znak",
|
||||||
"otpEmailSent": "Kod jednorazowy wysłany",
|
"otpEmailSent": "Kod jednorazowy wysłany",
|
||||||
"otpEmailSentDescription": "Kod jednorazowy został wysłany na Twój e-mail",
|
"otpEmailSentDescription": "Kod jednorazowy został wysłany na Twój e-mail",
|
||||||
@@ -970,6 +975,7 @@
|
|||||||
"logoutError": "Błąd podczas wylogowywania",
|
"logoutError": "Błąd podczas wylogowywania",
|
||||||
"signingAs": "Zalogowany jako",
|
"signingAs": "Zalogowany jako",
|
||||||
"serverAdmin": "Administrator serwera",
|
"serverAdmin": "Administrator serwera",
|
||||||
|
"managedSelfhosted": "Zarządzane Samodzielnie-Hostingowane",
|
||||||
"otpEnable": "Włącz uwierzytelnianie dwuskładnikowe",
|
"otpEnable": "Włącz uwierzytelnianie dwuskładnikowe",
|
||||||
"otpDisable": "Wyłącz uwierzytelnianie dwuskładnikowe",
|
"otpDisable": "Wyłącz uwierzytelnianie dwuskładnikowe",
|
||||||
"logout": "Wyloguj się",
|
"logout": "Wyloguj się",
|
||||||
@@ -985,9 +991,9 @@
|
|||||||
"actionDeleteSite": "Usuń witrynę",
|
"actionDeleteSite": "Usuń witrynę",
|
||||||
"actionGetSite": "Pobierz witrynę",
|
"actionGetSite": "Pobierz witrynę",
|
||||||
"actionListSites": "Lista witryn",
|
"actionListSites": "Lista witryn",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "Skonfiguruj token",
|
||||||
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
"setupTokenDescription": "Wprowadź token konfiguracji z konsoli serwera.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "Wymagany jest token konfiguracji",
|
||||||
"actionUpdateSite": "Aktualizuj witrynę",
|
"actionUpdateSite": "Aktualizuj witrynę",
|
||||||
"actionListSiteRoles": "Lista dozwolonych ról witryny",
|
"actionListSiteRoles": "Lista dozwolonych ról witryny",
|
||||||
"actionCreateResource": "Utwórz zasób",
|
"actionCreateResource": "Utwórz zasób",
|
||||||
@@ -1048,6 +1054,12 @@
|
|||||||
"actionUpdateClient": "Aktualizuj klienta",
|
"actionUpdateClient": "Aktualizuj klienta",
|
||||||
"actionListClients": "Lista klientów",
|
"actionListClients": "Lista klientów",
|
||||||
"actionGetClient": "Pobierz klienta",
|
"actionGetClient": "Pobierz klienta",
|
||||||
|
"actionCreateSiteResource": "Utwórz zasób witryny",
|
||||||
|
"actionDeleteSiteResource": "Usuń zasób strony",
|
||||||
|
"actionGetSiteResource": "Pobierz zasób strony",
|
||||||
|
"actionListSiteResources": "Lista zasobów strony",
|
||||||
|
"actionUpdateSiteResource": "Aktualizuj zasób strony",
|
||||||
|
"actionListInvitations": "Lista zaproszeń",
|
||||||
"noneSelected": "Nie wybrano",
|
"noneSelected": "Nie wybrano",
|
||||||
"orgNotFound2": "Nie znaleziono organizacji.",
|
"orgNotFound2": "Nie znaleziono organizacji.",
|
||||||
"searchProgress": "Szukaj...",
|
"searchProgress": "Szukaj...",
|
||||||
@@ -1341,8 +1353,148 @@
|
|||||||
"olmErrorFetchLatest": "Wystąpił błąd podczas pobierania najnowszego wydania Olm.",
|
"olmErrorFetchLatest": "Wystąpił błąd podczas pobierania najnowszego wydania Olm.",
|
||||||
"remoteSubnets": "Zdalne Podsieci",
|
"remoteSubnets": "Zdalne Podsieci",
|
||||||
"enterCidrRange": "Wprowadź zakres CIDR",
|
"enterCidrRange": "Wprowadź zakres CIDR",
|
||||||
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
"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",
|
"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.",
|
"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"
|
"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.",
|
||||||
|
"managedSelfHosted": {
|
||||||
|
"title": "Zarządzane Samodzielnie-Hostingowane",
|
||||||
|
"description": "Większa niezawodność i niska konserwacja serwera Pangolin z dodatkowymi dzwonkami i sygnałami",
|
||||||
|
"introTitle": "Zarządzany samowystarczalny Pangolin",
|
||||||
|
"introDescription": "jest opcją wdrażania zaprojektowaną dla osób, które chcą prostoty i dodatkowej niezawodności, przy jednoczesnym utrzymaniu swoich danych prywatnych i samodzielnych.",
|
||||||
|
"introDetail": "Z tą opcją nadal obsługujesz swój własny węzeł Pangolin — tunele, zakończenie SSL i ruch na Twoim serwerze. Różnica polega na tym, że zarządzanie i monitorowanie odbywa się za pomocą naszej tablicy rozdzielczej, która odblokowuje szereg korzyści:",
|
||||||
|
"benefitSimplerOperations": {
|
||||||
|
"title": "Uproszczone operacje",
|
||||||
|
"description": "Nie ma potrzeby uruchamiania własnego serwera pocztowego lub ustawiania skomplikowanych powiadomień. Będziesz mieć kontrolę zdrowia i powiadomienia o przestoju."
|
||||||
|
},
|
||||||
|
"benefitAutomaticUpdates": {
|
||||||
|
"title": "Automatyczne aktualizacje",
|
||||||
|
"description": "Panel chmury rozwija się szybko, więc otrzymujesz nowe funkcje i poprawki błędów bez konieczności ręcznego ciągnięcia nowych kontenerów za każdym razem."
|
||||||
|
},
|
||||||
|
"benefitLessMaintenance": {
|
||||||
|
"title": "Mniej konserwacji",
|
||||||
|
"description": "Brak migracji bazy danych, kopii zapasowych lub dodatkowej infrastruktury do zarządzania. Obsługujemy to w chmurze."
|
||||||
|
},
|
||||||
|
"benefitCloudFailover": {
|
||||||
|
"title": "Przegrywanie w chmurze",
|
||||||
|
"description": "Jeśli Twój węzeł zostanie wyłączony, tunele mogą tymczasowo zawieść do naszych punktów w chmurze, dopóki nie przyniesiesz go z powrotem do trybu online."
|
||||||
|
},
|
||||||
|
"benefitHighAvailability": {
|
||||||
|
"title": "Wysoka dostępność (PoPs)",
|
||||||
|
"description": "Możesz również dołączyć wiele węzłów do swojego konta w celu nadmiarowości i lepszej wydajności."
|
||||||
|
},
|
||||||
|
"benefitFutureEnhancements": {
|
||||||
|
"title": "Przyszłe ulepszenia",
|
||||||
|
"description": "Planujemy dodać więcej narzędzi analitycznych, ostrzegawczych i zarządzania, aby zwiększyć odporność wdrożenia."
|
||||||
|
},
|
||||||
|
"docsAlert": {
|
||||||
|
"text": "Dowiedz się więcej o opcji zarządzania samodzielnym hostingiem w naszym",
|
||||||
|
"documentation": "dokumentacja"
|
||||||
|
},
|
||||||
|
"convertButton": "Konwertuj ten węzeł do zarządzanego samodzielnie"
|
||||||
|
},
|
||||||
|
"internationaldomaindetected": "Wykryto międzynarodową domenę",
|
||||||
|
"willbestoredas": "Będą przechowywane jako:"
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,9 @@
|
|||||||
"siteNewtTunnelDescription": "A maneira mais fácil de criar um ponto de entrada na sua rede. Nenhuma configuração extra.",
|
"siteNewtTunnelDescription": "A maneira mais fácil de criar um ponto de entrada na sua rede. Nenhuma configuração extra.",
|
||||||
"siteWg": "WireGuard Básico",
|
"siteWg": "WireGuard Básico",
|
||||||
"siteWgDescription": "Use qualquer cliente do WireGuard para estabelecer um túnel. Configuração manual NAT é necessária.",
|
"siteWgDescription": "Use qualquer cliente do WireGuard para estabelecer um túnel. Configuração manual NAT é necessária.",
|
||||||
|
"siteWgDescriptionSaas": "Use qualquer cliente WireGuard para estabelecer um túnel. Configuração manual NAT necessária. SOMENTE FUNCIONA EM NODES AUTO-HOSPEDADOS",
|
||||||
"siteLocalDescription": "Recursos locais apenas. Sem túneis.",
|
"siteLocalDescription": "Recursos locais apenas. Sem túneis.",
|
||||||
|
"siteLocalDescriptionSaas": "Apenas recursos locais. Sem tunelamento. SOMENTE FUNCIONA EM NODES AUTO-HOSPEDADOS",
|
||||||
"siteSeeAll": "Ver todos os sites",
|
"siteSeeAll": "Ver todos os sites",
|
||||||
"siteTunnelDescription": "Determine como você deseja se conectar ao seu site",
|
"siteTunnelDescription": "Determine como você deseja se conectar ao seu site",
|
||||||
"siteNewtCredentials": "Credenciais Novas",
|
"siteNewtCredentials": "Credenciais Novas",
|
||||||
@@ -166,7 +168,7 @@
|
|||||||
"siteSelect": "Selecionar site",
|
"siteSelect": "Selecionar site",
|
||||||
"siteSearch": "Procurar no site",
|
"siteSearch": "Procurar no site",
|
||||||
"siteNotFound": "Nenhum site encontrado.",
|
"siteNotFound": "Nenhum site encontrado.",
|
||||||
"siteSelectionDescription": "Este site fornecerá conectividade ao recurso.",
|
"siteSelectionDescription": "Este site fornecerá conectividade ao destino.",
|
||||||
"resourceType": "Tipo de Recurso",
|
"resourceType": "Tipo de Recurso",
|
||||||
"resourceTypeDescription": "Determine como você deseja acessar seu recurso",
|
"resourceTypeDescription": "Determine como você deseja acessar seu recurso",
|
||||||
"resourceHTTPSSettings": "Configurações de HTTPS",
|
"resourceHTTPSSettings": "Configurações de HTTPS",
|
||||||
@@ -197,11 +199,13 @@
|
|||||||
"general": "Gerais",
|
"general": "Gerais",
|
||||||
"generalSettings": "Configurações Gerais",
|
"generalSettings": "Configurações Gerais",
|
||||||
"proxy": "Proxy",
|
"proxy": "Proxy",
|
||||||
|
"internal": "Interno",
|
||||||
"rules": "Regras",
|
"rules": "Regras",
|
||||||
"resourceSettingDescription": "Configure as configurações do seu recurso",
|
"resourceSettingDescription": "Configure as configurações do seu recurso",
|
||||||
"resourceSetting": "Configurações do {resourceName}",
|
"resourceSetting": "Configurações do {resourceName}",
|
||||||
"alwaysAllow": "Sempre permitir",
|
"alwaysAllow": "Sempre permitir",
|
||||||
"alwaysDeny": "Sempre negar",
|
"alwaysDeny": "Sempre negar",
|
||||||
|
"passToAuth": "Passar para Autenticação",
|
||||||
"orgSettingsDescription": "Configurar as configurações gerais da sua organização",
|
"orgSettingsDescription": "Configurar as configurações gerais da sua organização",
|
||||||
"orgGeneralSettings": "Configurações da organização",
|
"orgGeneralSettings": "Configurações da organização",
|
||||||
"orgGeneralSettingsDescription": "Gerencie os detalhes e a configuração da sua organização",
|
"orgGeneralSettingsDescription": "Gerencie os detalhes e a configuração da sua organização",
|
||||||
@@ -490,7 +494,7 @@
|
|||||||
"targetTlsSniDescription": "O Nome do Servidor TLS para usar para SNI. Deixe vazio para usar o padrão.",
|
"targetTlsSniDescription": "O Nome do Servidor TLS para usar para SNI. Deixe vazio para usar o padrão.",
|
||||||
"targetTlsSubmit": "Salvar Configurações",
|
"targetTlsSubmit": "Salvar Configurações",
|
||||||
"targets": "Configuração de Alvos",
|
"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",
|
"targetStickySessions": "Ativar Sessões Persistentes",
|
||||||
"targetStickySessionsDescription": "Manter conexões no mesmo alvo backend durante toda a sessão.",
|
"targetStickySessionsDescription": "Manter conexões no mesmo alvo backend durante toda a sessão.",
|
||||||
"methodSelect": "Selecionar método",
|
"methodSelect": "Selecionar método",
|
||||||
@@ -542,6 +546,7 @@
|
|||||||
"rulesActions": "Ações",
|
"rulesActions": "Ações",
|
||||||
"rulesActionAlwaysAllow": "Sempre Permitir: Ignorar todos os métodos de autenticação",
|
"rulesActionAlwaysAllow": "Sempre Permitir: Ignorar todos os métodos de autenticação",
|
||||||
"rulesActionAlwaysDeny": "Sempre Negar: Bloquear todas as requisições; nenhuma autenticação pode ser tentada",
|
"rulesActionAlwaysDeny": "Sempre Negar: Bloquear todas as requisições; nenhuma autenticação pode ser tentada",
|
||||||
|
"rulesActionPassToAuth": "Passar para Autenticação: Permitir que métodos de autenticação sejam tentados",
|
||||||
"rulesMatchCriteria": "Critérios de Correspondência",
|
"rulesMatchCriteria": "Critérios de Correspondência",
|
||||||
"rulesMatchCriteriaIpAddress": "Corresponder a um endereço IP específico",
|
"rulesMatchCriteriaIpAddress": "Corresponder a um endereço IP específico",
|
||||||
"rulesMatchCriteriaIpAddressRange": "Corresponder a uma faixa de endereços IP em notação CIDR",
|
"rulesMatchCriteriaIpAddressRange": "Corresponder a uma faixa de endereços IP em notação CIDR",
|
||||||
@@ -833,24 +838,24 @@
|
|||||||
"pincodeRequirementsLength": "O PIN deve ter exatamente 6 dígitos",
|
"pincodeRequirementsLength": "O PIN deve ter exatamente 6 dígitos",
|
||||||
"pincodeRequirementsChars": "O PIN deve conter apenas números",
|
"pincodeRequirementsChars": "O PIN deve conter apenas números",
|
||||||
"passwordRequirementsLength": "A palavra-passe deve ter pelo menos 1 caractere",
|
"passwordRequirementsLength": "A palavra-passe deve ter pelo menos 1 caractere",
|
||||||
"passwordRequirementsTitle": "Password requirements:",
|
"passwordRequirementsTitle": "Requisitos de senha:",
|
||||||
"passwordRequirementLength": "At least 8 characters long",
|
"passwordRequirementLength": "Pelo menos 8 caracteres de comprimento",
|
||||||
"passwordRequirementUppercase": "At least one uppercase letter",
|
"passwordRequirementUppercase": "Pelo menos uma letra maiúscula",
|
||||||
"passwordRequirementLowercase": "At least one lowercase letter",
|
"passwordRequirementLowercase": "Pelo menos uma letra minúscula",
|
||||||
"passwordRequirementNumber": "At least one number",
|
"passwordRequirementNumber": "Pelo menos um número",
|
||||||
"passwordRequirementSpecial": "At least one special character",
|
"passwordRequirementSpecial": "Pelo menos um caractere especial",
|
||||||
"passwordRequirementsMet": "✓ Password meets all requirements",
|
"passwordRequirementsMet": "✓ Senha atende a todos os requisitos",
|
||||||
"passwordStrength": "Password strength",
|
"passwordStrength": "Força da senha",
|
||||||
"passwordStrengthWeak": "Weak",
|
"passwordStrengthWeak": "Fraca",
|
||||||
"passwordStrengthMedium": "Medium",
|
"passwordStrengthMedium": "Média",
|
||||||
"passwordStrengthStrong": "Strong",
|
"passwordStrengthStrong": "Forte",
|
||||||
"passwordRequirements": "Requirements:",
|
"passwordRequirements": "Requisitos:",
|
||||||
"passwordRequirementLengthText": "8+ characters",
|
"passwordRequirementLengthText": "8+ caracteres",
|
||||||
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
"passwordRequirementUppercaseText": "Letra maiúscula (A-Z)",
|
||||||
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
"passwordRequirementLowercaseText": "Letra minúscula (a-z)",
|
||||||
"passwordRequirementNumberText": "Number (0-9)",
|
"passwordRequirementNumberText": "Número (0-9)",
|
||||||
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
"passwordRequirementSpecialText": "Caractere especial (!@#$%...)",
|
||||||
"passwordsDoNotMatch": "Passwords do not match",
|
"passwordsDoNotMatch": "As palavras-passe não correspondem",
|
||||||
"otpEmailRequirementsLength": "O OTP deve ter pelo menos 1 caractere",
|
"otpEmailRequirementsLength": "O OTP deve ter pelo menos 1 caractere",
|
||||||
"otpEmailSent": "OTP Enviado",
|
"otpEmailSent": "OTP Enviado",
|
||||||
"otpEmailSentDescription": "Um OTP foi enviado para o seu email",
|
"otpEmailSentDescription": "Um OTP foi enviado para o seu email",
|
||||||
@@ -970,6 +975,7 @@
|
|||||||
"logoutError": "Erro ao terminar sessão",
|
"logoutError": "Erro ao terminar sessão",
|
||||||
"signingAs": "Sessão iniciada como",
|
"signingAs": "Sessão iniciada como",
|
||||||
"serverAdmin": "Administrador do Servidor",
|
"serverAdmin": "Administrador do Servidor",
|
||||||
|
"managedSelfhosted": "Gerenciado Auto-Hospedado",
|
||||||
"otpEnable": "Ativar Autenticação de Dois Fatores",
|
"otpEnable": "Ativar Autenticação de Dois Fatores",
|
||||||
"otpDisable": "Desativar Autenticação de Dois Fatores",
|
"otpDisable": "Desativar Autenticação de Dois Fatores",
|
||||||
"logout": "Terminar Sessão",
|
"logout": "Terminar Sessão",
|
||||||
@@ -985,9 +991,9 @@
|
|||||||
"actionDeleteSite": "Eliminar Site",
|
"actionDeleteSite": "Eliminar Site",
|
||||||
"actionGetSite": "Obter Site",
|
"actionGetSite": "Obter Site",
|
||||||
"actionListSites": "Listar Sites",
|
"actionListSites": "Listar Sites",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "Configuração do Token",
|
||||||
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
"setupTokenDescription": "Digite o token de configuração do console do servidor.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "Token de configuração é necessário",
|
||||||
"actionUpdateSite": "Atualizar Site",
|
"actionUpdateSite": "Atualizar Site",
|
||||||
"actionListSiteRoles": "Listar Funções Permitidas do Site",
|
"actionListSiteRoles": "Listar Funções Permitidas do Site",
|
||||||
"actionCreateResource": "Criar Recurso",
|
"actionCreateResource": "Criar Recurso",
|
||||||
@@ -1048,6 +1054,12 @@
|
|||||||
"actionUpdateClient": "Atualizar Cliente",
|
"actionUpdateClient": "Atualizar Cliente",
|
||||||
"actionListClients": "Listar Clientes",
|
"actionListClients": "Listar Clientes",
|
||||||
"actionGetClient": "Obter Cliente",
|
"actionGetClient": "Obter Cliente",
|
||||||
|
"actionCreateSiteResource": "Criar Recurso do Site",
|
||||||
|
"actionDeleteSiteResource": "Eliminar Recurso do Site",
|
||||||
|
"actionGetSiteResource": "Obter Recurso do Site",
|
||||||
|
"actionListSiteResources": "Listar Recursos do Site",
|
||||||
|
"actionUpdateSiteResource": "Atualizar Recurso do Site",
|
||||||
|
"actionListInvitations": "Listar Convites",
|
||||||
"noneSelected": "Nenhum selecionado",
|
"noneSelected": "Nenhum selecionado",
|
||||||
"orgNotFound2": "Nenhuma organização encontrada.",
|
"orgNotFound2": "Nenhuma organização encontrada.",
|
||||||
"searchProgress": "Pesquisar...",
|
"searchProgress": "Pesquisar...",
|
||||||
@@ -1341,8 +1353,148 @@
|
|||||||
"olmErrorFetchLatest": "Ocorreu um erro ao buscar o lançamento mais recente do Olm.",
|
"olmErrorFetchLatest": "Ocorreu um erro ao buscar o lançamento mais recente do Olm.",
|
||||||
"remoteSubnets": "Sub-redes Remotas",
|
"remoteSubnets": "Sub-redes Remotas",
|
||||||
"enterCidrRange": "Insira o intervalo CIDR",
|
"enterCidrRange": "Insira o intervalo CIDR",
|
||||||
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
"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",
|
"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.",
|
"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"
|
"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.",
|
||||||
|
"managedSelfHosted": {
|
||||||
|
"title": "Gerenciado Auto-Hospedado",
|
||||||
|
"description": "Servidor Pangolin auto-hospedado mais confiável e com baixa manutenção com sinos extras e assobiamentos",
|
||||||
|
"introTitle": "Pangolin Auto-Hospedado Gerenciado",
|
||||||
|
"introDescription": "é uma opção de implantação projetada para pessoas que querem simplicidade e confiança adicional, mantendo os seus dados privados e auto-hospedados.",
|
||||||
|
"introDetail": "Com esta opção, você ainda roda seu próprio nó Pangolin — seus túneis, terminação SSL e tráfego todos permanecem no seu servidor. A diferença é que a gestão e a monitorização são geridos através do nosso painel de nuvem, que desbloqueia vários benefícios:",
|
||||||
|
"benefitSimplerOperations": {
|
||||||
|
"title": "Operações simples",
|
||||||
|
"description": "Não é necessário executar o seu próprio servidor de e-mail ou configurar um alerta complexo. Você receberá fora de caixa verificações de saúde e alertas de tempo de inatividade."
|
||||||
|
},
|
||||||
|
"benefitAutomaticUpdates": {
|
||||||
|
"title": "Atualizações automáticas",
|
||||||
|
"description": "O painel em nuvem evolui rapidamente, para que você obtenha novos recursos e correções de bugs sem ter de puxar manualmente novos contêineres toda vez."
|
||||||
|
},
|
||||||
|
"benefitLessMaintenance": {
|
||||||
|
"title": "Menos manutenção",
|
||||||
|
"description": "Sem migrações, backups ou infraestrutura extra para gerenciar. Lidamos com isso na nuvem."
|
||||||
|
},
|
||||||
|
"benefitCloudFailover": {
|
||||||
|
"title": "Falha na nuvem",
|
||||||
|
"description": "Se o seu nó descer, seus túneis podem falhar temporariamente nos nossos pontos de presença na nuvem até que você o traga online."
|
||||||
|
},
|
||||||
|
"benefitHighAvailability": {
|
||||||
|
"title": "Alta disponibilidade (Ppos)",
|
||||||
|
"description": "Você também pode anexar vários nós à sua conta para um melhor desempenho."
|
||||||
|
},
|
||||||
|
"benefitFutureEnhancements": {
|
||||||
|
"title": "Aprimoramentos futuros",
|
||||||
|
"description": "Estamos planejando adicionar mais análises, alertas e ferramentas de gerenciamento para tornar sua implantação ainda mais robusta."
|
||||||
|
},
|
||||||
|
"docsAlert": {
|
||||||
|
"text": "Saiba mais sobre a opção Hospedagem Auto-Gerenciada no nosso",
|
||||||
|
"documentation": "documentação"
|
||||||
|
},
|
||||||
|
"convertButton": "Converter este nó para Auto-Hospedado Gerenciado"
|
||||||
|
},
|
||||||
|
"internationaldomaindetected": "Domínio Internacional Detectado",
|
||||||
|
"willbestoredas": "Será armazenado como:"
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,9 @@
|
|||||||
"siteNewtTunnelDescription": "Простейший способ создать точку входа в вашу сеть. Дополнительная настройка не требуется.",
|
"siteNewtTunnelDescription": "Простейший способ создать точку входа в вашу сеть. Дополнительная настройка не требуется.",
|
||||||
"siteWg": "Базовый WireGuard",
|
"siteWg": "Базовый WireGuard",
|
||||||
"siteWgDescription": "Используйте любой клиент WireGuard для открытия туннеля. Требуется ручная настройка NAT.",
|
"siteWgDescription": "Используйте любой клиент WireGuard для открытия туннеля. Требуется ручная настройка NAT.",
|
||||||
|
"siteWgDescriptionSaas": "Используйте любой клиент WireGuard для создания туннеля. Требуется ручная настройка NAT. РАБОТАЕТ ТОЛЬКО НА САМОСТОЯТЕЛЬНО РАЗМЕЩЕННЫХ УЗЛАХ",
|
||||||
"siteLocalDescription": "Только локальные ресурсы. Без туннелирования.",
|
"siteLocalDescription": "Только локальные ресурсы. Без туннелирования.",
|
||||||
|
"siteLocalDescriptionSaas": "Только локальные ресурсы. Без туннелирования. РАБОТАЕТ ТОЛЬКО НА САМОСТОЯТЕЛЬНО РАЗМЕЩЕННЫХ УЗЛАХ",
|
||||||
"siteSeeAll": "Просмотреть все сайты",
|
"siteSeeAll": "Просмотреть все сайты",
|
||||||
"siteTunnelDescription": "Выберите способ подключения к вашему сайту",
|
"siteTunnelDescription": "Выберите способ подключения к вашему сайту",
|
||||||
"siteNewtCredentials": "Учётные данные Newt",
|
"siteNewtCredentials": "Учётные данные Newt",
|
||||||
@@ -166,7 +168,7 @@
|
|||||||
"siteSelect": "Выберите сайт",
|
"siteSelect": "Выберите сайт",
|
||||||
"siteSearch": "Поиск сайта",
|
"siteSearch": "Поиск сайта",
|
||||||
"siteNotFound": "Сайт не найден.",
|
"siteNotFound": "Сайт не найден.",
|
||||||
"siteSelectionDescription": "Этот сайт обеспечит подключение к ресурсу.",
|
"siteSelectionDescription": "Этот сайт предоставит подключение к цели.",
|
||||||
"resourceType": "Тип ресурса",
|
"resourceType": "Тип ресурса",
|
||||||
"resourceTypeDescription": "Определите, как вы хотите получать доступ к вашему ресурсу",
|
"resourceTypeDescription": "Определите, как вы хотите получать доступ к вашему ресурсу",
|
||||||
"resourceHTTPSSettings": "Настройки HTTPS",
|
"resourceHTTPSSettings": "Настройки HTTPS",
|
||||||
@@ -197,11 +199,13 @@
|
|||||||
"general": "Общие",
|
"general": "Общие",
|
||||||
"generalSettings": "Общие настройки",
|
"generalSettings": "Общие настройки",
|
||||||
"proxy": "Прокси",
|
"proxy": "Прокси",
|
||||||
|
"internal": "Внутренний",
|
||||||
"rules": "Правила",
|
"rules": "Правила",
|
||||||
"resourceSettingDescription": "Настройте параметры вашего ресурса",
|
"resourceSettingDescription": "Настройте параметры вашего ресурса",
|
||||||
"resourceSetting": "Настройки {resourceName}",
|
"resourceSetting": "Настройки {resourceName}",
|
||||||
"alwaysAllow": "Всегда разрешать",
|
"alwaysAllow": "Всегда разрешать",
|
||||||
"alwaysDeny": "Всегда запрещать",
|
"alwaysDeny": "Всегда запрещать",
|
||||||
|
"passToAuth": "Переход к аутентификации",
|
||||||
"orgSettingsDescription": "Настройте общие параметры вашей организации",
|
"orgSettingsDescription": "Настройте общие параметры вашей организации",
|
||||||
"orgGeneralSettings": "Настройки организации",
|
"orgGeneralSettings": "Настройки организации",
|
||||||
"orgGeneralSettingsDescription": "Управляйте данными и конфигурацией вашей организации",
|
"orgGeneralSettingsDescription": "Управляйте данными и конфигурацией вашей организации",
|
||||||
@@ -490,7 +494,7 @@
|
|||||||
"targetTlsSniDescription": "Имя TLS сервера для использования в SNI. Оставьте пустым для использования по умолчанию.",
|
"targetTlsSniDescription": "Имя TLS сервера для использования в SNI. Оставьте пустым для использования по умолчанию.",
|
||||||
"targetTlsSubmit": "Сохранить настройки",
|
"targetTlsSubmit": "Сохранить настройки",
|
||||||
"targets": "Конфигурация целей",
|
"targets": "Конфигурация целей",
|
||||||
"targetsDescription": "Настройте цели для маршрутизации трафика к вашим сервисам",
|
"targetsDescription": "Настройте цели для маршрутизации трафика к вашим бэкэнд сервисам",
|
||||||
"targetStickySessions": "Включить фиксированные сессии",
|
"targetStickySessions": "Включить фиксированные сессии",
|
||||||
"targetStickySessionsDescription": "Сохранять соединения на одной и той же целевой точке в течение всей сессии.",
|
"targetStickySessionsDescription": "Сохранять соединения на одной и той же целевой точке в течение всей сессии.",
|
||||||
"methodSelect": "Выберите метод",
|
"methodSelect": "Выберите метод",
|
||||||
@@ -542,6 +546,7 @@
|
|||||||
"rulesActions": "Действия",
|
"rulesActions": "Действия",
|
||||||
"rulesActionAlwaysAllow": "Всегда разрешать: Обойти все методы аутентификации",
|
"rulesActionAlwaysAllow": "Всегда разрешать: Обойти все методы аутентификации",
|
||||||
"rulesActionAlwaysDeny": "Всегда запрещать: Блокировать все запросы; аутентификация не может быть выполнена",
|
"rulesActionAlwaysDeny": "Всегда запрещать: Блокировать все запросы; аутентификация не может быть выполнена",
|
||||||
|
"rulesActionPassToAuth": "Переход к аутентификации: Разрешить попытки методов аутентификации",
|
||||||
"rulesMatchCriteria": "Критерии совпадения",
|
"rulesMatchCriteria": "Критерии совпадения",
|
||||||
"rulesMatchCriteriaIpAddress": "Совпадение с конкретным IP адресом",
|
"rulesMatchCriteriaIpAddress": "Совпадение с конкретным IP адресом",
|
||||||
"rulesMatchCriteriaIpAddressRange": "Совпадение с диапазоном IP адресов в нотации CIDR",
|
"rulesMatchCriteriaIpAddressRange": "Совпадение с диапазоном IP адресов в нотации CIDR",
|
||||||
@@ -833,24 +838,24 @@
|
|||||||
"pincodeRequirementsLength": "PIN должен состоять ровно из 6 цифр",
|
"pincodeRequirementsLength": "PIN должен состоять ровно из 6 цифр",
|
||||||
"pincodeRequirementsChars": "PIN должен содержать только цифры",
|
"pincodeRequirementsChars": "PIN должен содержать только цифры",
|
||||||
"passwordRequirementsLength": "Пароль должен быть не менее 1 символа",
|
"passwordRequirementsLength": "Пароль должен быть не менее 1 символа",
|
||||||
"passwordRequirementsTitle": "Password requirements:",
|
"passwordRequirementsTitle": "Требования к паролю:",
|
||||||
"passwordRequirementLength": "At least 8 characters long",
|
"passwordRequirementLength": "Не менее 8 символов",
|
||||||
"passwordRequirementUppercase": "At least one uppercase letter",
|
"passwordRequirementUppercase": "По крайней мере, одна заглавная буква",
|
||||||
"passwordRequirementLowercase": "At least one lowercase letter",
|
"passwordRequirementLowercase": "По крайней мере, одна строчная буква",
|
||||||
"passwordRequirementNumber": "At least one number",
|
"passwordRequirementNumber": "По крайней мере, одна цифра",
|
||||||
"passwordRequirementSpecial": "At least one special character",
|
"passwordRequirementSpecial": "По крайней мере, один специальный символ",
|
||||||
"passwordRequirementsMet": "✓ Password meets all requirements",
|
"passwordRequirementsMet": "✓ Пароль соответствует всем требованиям",
|
||||||
"passwordStrength": "Password strength",
|
"passwordStrength": "Сила пароля",
|
||||||
"passwordStrengthWeak": "Weak",
|
"passwordStrengthWeak": "Слабый",
|
||||||
"passwordStrengthMedium": "Medium",
|
"passwordStrengthMedium": "Средний",
|
||||||
"passwordStrengthStrong": "Strong",
|
"passwordStrengthStrong": "Сильный",
|
||||||
"passwordRequirements": "Requirements:",
|
"passwordRequirements": "Требования:",
|
||||||
"passwordRequirementLengthText": "8+ characters",
|
"passwordRequirementLengthText": "8+ символов",
|
||||||
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
"passwordRequirementUppercaseText": "Заглавная буква (A-Z)",
|
||||||
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
"passwordRequirementLowercaseText": "Строчная буква (a-z)",
|
||||||
"passwordRequirementNumberText": "Number (0-9)",
|
"passwordRequirementNumberText": "Цифра (0-9)",
|
||||||
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
"passwordRequirementSpecialText": "Специальный символ (!@#$%...)",
|
||||||
"passwordsDoNotMatch": "Passwords do not match",
|
"passwordsDoNotMatch": "Пароли не совпадают",
|
||||||
"otpEmailRequirementsLength": "OTP должен быть не менее 1 символа",
|
"otpEmailRequirementsLength": "OTP должен быть не менее 1 символа",
|
||||||
"otpEmailSent": "OTP отправлен",
|
"otpEmailSent": "OTP отправлен",
|
||||||
"otpEmailSentDescription": "OTP был отправлен на ваш email",
|
"otpEmailSentDescription": "OTP был отправлен на ваш email",
|
||||||
@@ -970,6 +975,7 @@
|
|||||||
"logoutError": "Ошибка при выходе",
|
"logoutError": "Ошибка при выходе",
|
||||||
"signingAs": "Вы вошли как",
|
"signingAs": "Вы вошли как",
|
||||||
"serverAdmin": "Администратор сервера",
|
"serverAdmin": "Администратор сервера",
|
||||||
|
"managedSelfhosted": "Управляемый с самовывоза",
|
||||||
"otpEnable": "Включить Двухфакторную Аутентификацию",
|
"otpEnable": "Включить Двухфакторную Аутентификацию",
|
||||||
"otpDisable": "Отключить двухфакторную аутентификацию",
|
"otpDisable": "Отключить двухфакторную аутентификацию",
|
||||||
"logout": "Выйти",
|
"logout": "Выйти",
|
||||||
@@ -985,9 +991,9 @@
|
|||||||
"actionDeleteSite": "Удалить сайт",
|
"actionDeleteSite": "Удалить сайт",
|
||||||
"actionGetSite": "Получить сайт",
|
"actionGetSite": "Получить сайт",
|
||||||
"actionListSites": "Список сайтов",
|
"actionListSites": "Список сайтов",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "Код настройки",
|
||||||
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
"setupTokenDescription": "Введите токен настройки из консоли сервера.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "Токен настройки обязателен",
|
||||||
"actionUpdateSite": "Обновить сайт",
|
"actionUpdateSite": "Обновить сайт",
|
||||||
"actionListSiteRoles": "Список разрешенных ролей сайта",
|
"actionListSiteRoles": "Список разрешенных ролей сайта",
|
||||||
"actionCreateResource": "Создать ресурс",
|
"actionCreateResource": "Создать ресурс",
|
||||||
@@ -1001,8 +1007,8 @@
|
|||||||
"actionListAllowedResourceRoles": "Список разрешенных ролей сайта",
|
"actionListAllowedResourceRoles": "Список разрешенных ролей сайта",
|
||||||
"actionSetResourcePassword": "Задать пароль ресурса",
|
"actionSetResourcePassword": "Задать пароль ресурса",
|
||||||
"actionSetResourcePincode": "Установить ПИН-код ресурса",
|
"actionSetResourcePincode": "Установить ПИН-код ресурса",
|
||||||
"actionSetResourceEmailWhitelist": "Set Resource Email Whitelist",
|
"actionSetResourceEmailWhitelist": "Настроить белый список ресурсов email",
|
||||||
"actionGetResourceEmailWhitelist": "Get Resource Email Whitelist",
|
"actionGetResourceEmailWhitelist": "Получить белый список ресурсов email",
|
||||||
"actionCreateTarget": "Создать цель",
|
"actionCreateTarget": "Создать цель",
|
||||||
"actionDeleteTarget": "Удалить цель",
|
"actionDeleteTarget": "Удалить цель",
|
||||||
"actionGetTarget": "Получить цель",
|
"actionGetTarget": "Получить цель",
|
||||||
@@ -1048,6 +1054,12 @@
|
|||||||
"actionUpdateClient": "Обновить Клиента",
|
"actionUpdateClient": "Обновить Клиента",
|
||||||
"actionListClients": "Список Клиентов",
|
"actionListClients": "Список Клиентов",
|
||||||
"actionGetClient": "Получить Клиента",
|
"actionGetClient": "Получить Клиента",
|
||||||
|
"actionCreateSiteResource": "Создать ресурс сайта",
|
||||||
|
"actionDeleteSiteResource": "Удалить ресурс сайта ",
|
||||||
|
"actionGetSiteResource": "Получить ресурс сайта",
|
||||||
|
"actionListSiteResources": "Список ресурсов сайта",
|
||||||
|
"actionUpdateSiteResource": "Обновить ресурс сайта",
|
||||||
|
"actionListInvitations": "Список приглашений",
|
||||||
"noneSelected": "Ничего не выбрано",
|
"noneSelected": "Ничего не выбрано",
|
||||||
"orgNotFound2": "Организации не найдены.",
|
"orgNotFound2": "Организации не найдены.",
|
||||||
"searchProgress": "Поиск...",
|
"searchProgress": "Поиск...",
|
||||||
@@ -1186,114 +1198,114 @@
|
|||||||
"selectDomainTypeNsDescription": "Этот домен и все его субдомены. Используйте это, когда вы хотите управлять всей доменной зоной.",
|
"selectDomainTypeNsDescription": "Этот домен и все его субдомены. Используйте это, когда вы хотите управлять всей доменной зоной.",
|
||||||
"selectDomainTypeCnameName": "Одиночный домен (CNAME)",
|
"selectDomainTypeCnameName": "Одиночный домен (CNAME)",
|
||||||
"selectDomainTypeCnameDescription": "Только этот конкретный домен. Используйте это для отдельных субдоменов или отдельных записей домена.",
|
"selectDomainTypeCnameDescription": "Только этот конкретный домен. Используйте это для отдельных субдоменов или отдельных записей домена.",
|
||||||
"selectDomainTypeWildcardName": "Wildcard Domain",
|
"selectDomainTypeWildcardName": "Подставной домен",
|
||||||
"selectDomainTypeWildcardDescription": "Этот домен и его субдомены.",
|
"selectDomainTypeWildcardDescription": "Этот домен и его субдомены.",
|
||||||
"domainDelegation": "Единый домен",
|
"domainDelegation": "Единый домен",
|
||||||
"selectType": "Выберите тип",
|
"selectType": "Выберите тип",
|
||||||
"actions": "Actions",
|
"actions": "Действия",
|
||||||
"refresh": "Refresh",
|
"refresh": "Обновить",
|
||||||
"refreshError": "Failed to refresh data",
|
"refreshError": "Не удалось обновить данные",
|
||||||
"verified": "Verified",
|
"verified": "Подтверждено",
|
||||||
"pending": "Pending",
|
"pending": "В ожидании",
|
||||||
"sidebarBilling": "Billing",
|
"sidebarBilling": "Выставление счетов",
|
||||||
"billing": "Billing",
|
"billing": "Выставление счетов",
|
||||||
"orgBillingDescription": "Manage your billing information and subscriptions",
|
"orgBillingDescription": "Управляйте информацией о выставлении счетов и подписками",
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
"pangolinHosted": "Pangolin Hosted",
|
"pangolinHosted": "Pangolin Hosted",
|
||||||
"fossorial": "Fossorial",
|
"fossorial": "Fossorial",
|
||||||
"completeAccountSetup": "Complete Account Setup",
|
"completeAccountSetup": "Завершите настройку аккаунта",
|
||||||
"completeAccountSetupDescription": "Set your password to get started",
|
"completeAccountSetupDescription": "Установите ваш пароль, чтобы начать",
|
||||||
"accountSetupSent": "We'll send an account setup code to this email address.",
|
"accountSetupSent": "Мы отправим код для настройки аккаунта на этот email адрес.",
|
||||||
"accountSetupCode": "Setup Code",
|
"accountSetupCode": "Код настройки",
|
||||||
"accountSetupCodeDescription": "Check your email for the setup code.",
|
"accountSetupCodeDescription": "Проверьте вашу почту для получения кода настройки.",
|
||||||
"passwordCreate": "Create Password",
|
"passwordCreate": "Создать пароль",
|
||||||
"passwordCreateConfirm": "Confirm Password",
|
"passwordCreateConfirm": "Подтвердите пароль",
|
||||||
"accountSetupSubmit": "Send Setup Code",
|
"accountSetupSubmit": "Отправить код настройки",
|
||||||
"completeSetup": "Complete Setup",
|
"completeSetup": "Завершить настройку",
|
||||||
"accountSetupSuccess": "Account setup completed! Welcome to Pangolin!",
|
"accountSetupSuccess": "Настройка аккаунта завершена! Добро пожаловать в Pangolin!",
|
||||||
"documentation": "Documentation",
|
"documentation": "Документация",
|
||||||
"saveAllSettings": "Save All Settings",
|
"saveAllSettings": "Сохранить все настройки",
|
||||||
"settingsUpdated": "Settings updated",
|
"settingsUpdated": "Настройки обновлены",
|
||||||
"settingsUpdatedDescription": "All settings have been updated successfully",
|
"settingsUpdatedDescription": "Все настройки успешно обновлены",
|
||||||
"settingsErrorUpdate": "Failed to update settings",
|
"settingsErrorUpdate": "Не удалось обновить настройки",
|
||||||
"settingsErrorUpdateDescription": "An error occurred while updating settings",
|
"settingsErrorUpdateDescription": "Произошла ошибка при обновлении настроек",
|
||||||
"sidebarCollapse": "Collapse",
|
"sidebarCollapse": "Свернуть",
|
||||||
"sidebarExpand": "Expand",
|
"sidebarExpand": "Развернуть",
|
||||||
"newtUpdateAvailable": "Update Available",
|
"newtUpdateAvailable": "Доступно обновление",
|
||||||
"newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.",
|
"newtUpdateAvailableInfo": "Доступна новая версия Newt. Пожалуйста, обновитесь до последней версии для лучшего опыта.",
|
||||||
"domainPickerEnterDomain": "Domain",
|
"domainPickerEnterDomain": "Домен",
|
||||||
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp",
|
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, или просто myapp",
|
||||||
"domainPickerDescription": "Enter the full domain of the resource to see available options.",
|
"domainPickerDescription": "Введите полный домен ресурса, чтобы увидеть доступные опции.",
|
||||||
"domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options",
|
"domainPickerDescriptionSaas": "Введите полный домен, поддомен или просто имя, чтобы увидеть доступные опции",
|
||||||
"domainPickerTabAll": "All",
|
"domainPickerTabAll": "Все",
|
||||||
"domainPickerTabOrganization": "Organization",
|
"domainPickerTabOrganization": "Организация",
|
||||||
"domainPickerTabProvided": "Provided",
|
"domainPickerTabProvided": "Предоставлено",
|
||||||
"domainPickerSortAsc": "A-Z",
|
"domainPickerSortAsc": "А-Я",
|
||||||
"domainPickerSortDesc": "Z-A",
|
"domainPickerSortDesc": "Я-А",
|
||||||
"domainPickerCheckingAvailability": "Checking availability...",
|
"domainPickerCheckingAvailability": "Проверка доступности...",
|
||||||
"domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.",
|
"domainPickerNoMatchingDomains": "Не найдены сопоставимые домены. Попробуйте другой домен или проверьте настройки доменов вашей организации.",
|
||||||
"domainPickerOrganizationDomains": "Organization Domains",
|
"domainPickerOrganizationDomains": "Домены организации",
|
||||||
"domainPickerProvidedDomains": "Provided Domains",
|
"domainPickerProvidedDomains": "Предоставленные домены",
|
||||||
"domainPickerSubdomain": "Subdomain: {subdomain}",
|
"domainPickerSubdomain": "Поддомен: {subdomain}",
|
||||||
"domainPickerNamespace": "Namespace: {namespace}",
|
"domainPickerNamespace": "Пространство имен: {namespace}",
|
||||||
"domainPickerShowMore": "Show More",
|
"domainPickerShowMore": "Показать еще",
|
||||||
"domainNotFound": "Domain Not Found",
|
"domainNotFound": "Домен не найден",
|
||||||
"domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.",
|
"domainNotFoundDescription": "Этот ресурс отключен, так как домен больше не существует в нашей системе. Пожалуйста, установите новый домен для этого ресурса.",
|
||||||
"failed": "Failed",
|
"failed": "Ошибка",
|
||||||
"createNewOrgDescription": "Create a new organization",
|
"createNewOrgDescription": "Создать новую организацию",
|
||||||
"organization": "Organization",
|
"organization": "Организация",
|
||||||
"port": "Port",
|
"port": "Порт",
|
||||||
"securityKeyManage": "Manage Security Keys",
|
"securityKeyManage": "Управление ключами безопасности",
|
||||||
"securityKeyDescription": "Add or remove security keys for passwordless authentication",
|
"securityKeyDescription": "Добавить или удалить ключи безопасности для аутентификации без пароля",
|
||||||
"securityKeyRegister": "Register New Security Key",
|
"securityKeyRegister": "Зарегистрировать новый ключ безопасности",
|
||||||
"securityKeyList": "Your Security Keys",
|
"securityKeyList": "Ваши ключи безопасности",
|
||||||
"securityKeyNone": "No security keys registered yet",
|
"securityKeyNone": "Ключи безопасности еще не зарегистрированы",
|
||||||
"securityKeyNameRequired": "Name is required",
|
"securityKeyNameRequired": "Имя обязательно",
|
||||||
"securityKeyRemove": "Remove",
|
"securityKeyRemove": "Удалить",
|
||||||
"securityKeyLastUsed": "Last used: {date}",
|
"securityKeyLastUsed": "Последнее использование: {date}",
|
||||||
"securityKeyNameLabel": "Security Key Name",
|
"securityKeyNameLabel": "Имя ключа безопасности",
|
||||||
"securityKeyRegisterSuccess": "Security key registered successfully",
|
"securityKeyRegisterSuccess": "Ключ безопасности успешно зарегистрирован",
|
||||||
"securityKeyRegisterError": "Failed to register security key",
|
"securityKeyRegisterError": "Не удалось зарегистрировать ключ безопасности",
|
||||||
"securityKeyRemoveSuccess": "Security key removed successfully",
|
"securityKeyRemoveSuccess": "Ключ безопасности успешно удален",
|
||||||
"securityKeyRemoveError": "Failed to remove security key",
|
"securityKeyRemoveError": "Не удалось удалить ключ безопасности",
|
||||||
"securityKeyLoadError": "Failed to load security keys",
|
"securityKeyLoadError": "Не удалось загрузить ключи безопасности",
|
||||||
"securityKeyLogin": "Continue with security key",
|
"securityKeyLogin": "Продолжить с ключом безопасности",
|
||||||
"securityKeyAuthError": "Failed to authenticate with security key",
|
"securityKeyAuthError": "Не удалось аутентифицироваться с ключом безопасности",
|
||||||
"securityKeyRecommendation": "Register a backup security key on another device to ensure you always have access to your account.",
|
"securityKeyRecommendation": "Зарегистрируйте резервный ключ безопасности на другом устройстве, чтобы всегда иметь доступ к вашему аккаунту.",
|
||||||
"registering": "Registering...",
|
"registering": "Регистрация...",
|
||||||
"securityKeyPrompt": "Please verify your identity using your security key. Make sure your security key is connected and ready.",
|
"securityKeyPrompt": "Пожалуйста, подтвердите свою личность с использованием вашего ключа безопасности. Убедитесь, что ваш ключ безопасности подключен и готов.",
|
||||||
"securityKeyBrowserNotSupported": "Your browser doesn't support security keys. Please use a modern browser like Chrome, Firefox, or Safari.",
|
"securityKeyBrowserNotSupported": "Ваш браузер не поддерживает ключи безопасности. Пожалуйста, используйте современный браузер, такой как Chrome, Firefox или Safari.",
|
||||||
"securityKeyPermissionDenied": "Please allow access to your security key to continue signing in.",
|
"securityKeyPermissionDenied": "Пожалуйста, разрешите доступ к вашему ключу безопасности, чтобы продолжить вход.",
|
||||||
"securityKeyRemovedTooQuickly": "Please keep your security key connected until the sign-in process completes.",
|
"securityKeyRemovedTooQuickly": "Пожалуйста, держите ваш ключ безопасности подключенным, пока процесс входа не завершится.",
|
||||||
"securityKeyNotSupported": "Your security key may not be compatible. Please try a different security key.",
|
"securityKeyNotSupported": "Ваш ключ безопасности может быть несовместим. Попробуйте другой ключ безопасности.",
|
||||||
"securityKeyUnknownError": "There was a problem using your security key. Please try again.",
|
"securityKeyUnknownError": "Произошла проблема при использовании вашего ключа безопасности. Пожалуйста, попробуйте еще раз.",
|
||||||
"twoFactorRequired": "Two-factor authentication is required to register a security key.",
|
"twoFactorRequired": "Для регистрации ключа безопасности требуется двухфакторная аутентификация.",
|
||||||
"twoFactor": "Two-Factor Authentication",
|
"twoFactor": "Двухфакторная аутентификация",
|
||||||
"adminEnabled2FaOnYourAccount": "Your administrator has enabled two-factor authentication for {email}. Please complete the setup process to continue.",
|
"adminEnabled2FaOnYourAccount": "Ваш администратор включил двухфакторную аутентификацию для {email}. Пожалуйста, завершите процесс настройки, чтобы продолжить.",
|
||||||
"continueToApplication": "Continue to Application",
|
"continueToApplication": "Перейти к приложению",
|
||||||
"securityKeyAdd": "Add Security Key",
|
"securityKeyAdd": "Добавить ключ безопасности",
|
||||||
"securityKeyRegisterTitle": "Register New Security Key",
|
"securityKeyRegisterTitle": "Регистрация нового ключа безопасности",
|
||||||
"securityKeyRegisterDescription": "Connect your security key and enter a name to identify it",
|
"securityKeyRegisterDescription": "Подключите свой ключ безопасности и введите имя для его идентификации",
|
||||||
"securityKeyTwoFactorRequired": "Two-Factor Authentication Required",
|
"securityKeyTwoFactorRequired": "Требуется двухфакторная аутентификация",
|
||||||
"securityKeyTwoFactorDescription": "Please enter your two-factor authentication code to register the security key",
|
"securityKeyTwoFactorDescription": "Пожалуйста, введите ваш код двухфакторной аутентификации для регистрации ключа безопасности",
|
||||||
"securityKeyTwoFactorRemoveDescription": "Please enter your two-factor authentication code to remove the security key",
|
"securityKeyTwoFactorRemoveDescription": "Пожалуйста, введите ваш код двухфакторной аутентификации для удаления ключа безопасности",
|
||||||
"securityKeyTwoFactorCode": "Two-Factor Code",
|
"securityKeyTwoFactorCode": "Код двухфакторной аутентификации",
|
||||||
"securityKeyRemoveTitle": "Remove Security Key",
|
"securityKeyRemoveTitle": "Удалить ключ безопасности",
|
||||||
"securityKeyRemoveDescription": "Enter your password to remove the security key \"{name}\"",
|
"securityKeyRemoveDescription": "Введите ваш пароль для удаления ключа безопасности \"{name}\"",
|
||||||
"securityKeyNoKeysRegistered": "No security keys registered",
|
"securityKeyNoKeysRegistered": "Ключи безопасности не зарегистрированы",
|
||||||
"securityKeyNoKeysDescription": "Add a security key to enhance your account security",
|
"securityKeyNoKeysDescription": "Добавьте ключ безопасности, чтобы повысить безопасность вашего аккаунта",
|
||||||
"createDomainRequired": "Domain is required",
|
"createDomainRequired": "Домен обязателен",
|
||||||
"createDomainAddDnsRecords": "Add DNS Records",
|
"createDomainAddDnsRecords": "Добавить DNS записи",
|
||||||
"createDomainAddDnsRecordsDescription": "Add the following DNS records to your domain provider to complete the setup.",
|
"createDomainAddDnsRecordsDescription": "Добавьте следующие DNS записи у вашего провайдера доменных имен для завершения настройки.",
|
||||||
"createDomainNsRecords": "NS Records",
|
"createDomainNsRecords": "NS Записи",
|
||||||
"createDomainRecord": "Record",
|
"createDomainRecord": "Запись",
|
||||||
"createDomainType": "Type:",
|
"createDomainType": "Тип:",
|
||||||
"createDomainName": "Name:",
|
"createDomainName": "Имя:",
|
||||||
"createDomainValue": "Value:",
|
"createDomainValue": "Значение:",
|
||||||
"createDomainCnameRecords": "CNAME Records",
|
"createDomainCnameRecords": "CNAME Записи",
|
||||||
"createDomainARecords": "A Records",
|
"createDomainARecords": "A Записи",
|
||||||
"createDomainRecordNumber": "Record {number}",
|
"createDomainRecordNumber": "Запись {number}",
|
||||||
"createDomainTxtRecords": "TXT Records",
|
"createDomainTxtRecords": "TXT Записи",
|
||||||
"createDomainSaveTheseRecords": "Сохранить эти записи",
|
"createDomainSaveTheseRecords": "Сохранить эти записи",
|
||||||
"createDomainSaveTheseRecordsDescription": "Обязательно сохраните эти DNS записи, так как вы их больше не увидите.",
|
"createDomainSaveTheseRecordsDescription": "Обязательно сохраните эти DNS записи, так как вы их больше не увидите.",
|
||||||
"createDomainDnsPropagation": "Распространение DNS",
|
"createDomainDnsPropagation": "Распространение DNS",
|
||||||
@@ -1307,42 +1319,182 @@
|
|||||||
"privacyPolicy": "политика конфиденциальности"
|
"privacyPolicy": "политика конфиденциальности"
|
||||||
},
|
},
|
||||||
"siteRequired": "Необходимо указать сайт.",
|
"siteRequired": "Необходимо указать сайт.",
|
||||||
"olmTunnel": "Olm Tunnel",
|
"olmTunnel": "Olm Туннель",
|
||||||
"olmTunnelDescription": "Use Olm for client connectivity",
|
"olmTunnelDescription": "Используйте Olm для подключений клиентов",
|
||||||
"errorCreatingClient": "Error creating client",
|
"errorCreatingClient": "Ошибка при создании клиента",
|
||||||
"clientDefaultsNotFound": "Client defaults not found",
|
"clientDefaultsNotFound": "Настройки клиента по умолчанию не найдены",
|
||||||
"createClient": "Create Client",
|
"createClient": "Создать клиента",
|
||||||
"createClientDescription": "Create a new client for connecting to your sites",
|
"createClientDescription": "Создайте нового клиента для подключения к вашим сайтам",
|
||||||
"seeAllClients": "See All Clients",
|
"seeAllClients": "Просмотреть всех клиентов",
|
||||||
"clientInformation": "Client Information",
|
"clientInformation": "Информация о клиенте",
|
||||||
"clientNamePlaceholder": "Client name",
|
"clientNamePlaceholder": "Имя клиента",
|
||||||
"address": "Address",
|
"address": "Адрес",
|
||||||
"subnetPlaceholder": "Subnet",
|
"subnetPlaceholder": "Подсеть",
|
||||||
"addressDescription": "The address that this client will use for connectivity",
|
"addressDescription": "Адрес, который этот клиент будет использовать для подключения",
|
||||||
"selectSites": "Select sites",
|
"selectSites": "Выберите сайты",
|
||||||
"sitesDescription": "The client will have connectivity to the selected sites",
|
"sitesDescription": "Клиент будет иметь подключение к выбранным сайтам",
|
||||||
"clientInstallOlm": "Install Olm",
|
"clientInstallOlm": "Установить Olm",
|
||||||
"clientInstallOlmDescription": "Get Olm running on your system",
|
"clientInstallOlmDescription": "Запустите Olm на вашей системе",
|
||||||
"clientOlmCredentials": "Olm Credentials",
|
"clientOlmCredentials": "Учётные данные Olm",
|
||||||
"clientOlmCredentialsDescription": "This is how Olm will authenticate with the server",
|
"clientOlmCredentialsDescription": "Так Olm будет аутентифицироваться через сервер",
|
||||||
"olmEndpoint": "Olm Endpoint",
|
"olmEndpoint": "Конечная точка Olm",
|
||||||
"olmId": "Olm ID",
|
"olmId": "Olm ID",
|
||||||
"olmSecretKey": "Olm Secret Key",
|
"olmSecretKey": "Секретный ключ Olm",
|
||||||
"clientCredentialsSave": "Save Your Credentials",
|
"clientCredentialsSave": "Сохраните ваши учётные данные",
|
||||||
"clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
|
"clientCredentialsSaveDescription": "Вы сможете увидеть их только один раз. Обязательно скопируйте в безопасное место.",
|
||||||
"generalSettingsDescription": "Configure the general settings for this client",
|
"generalSettingsDescription": "Настройте общие параметры для этого клиента",
|
||||||
"clientUpdated": "Client updated",
|
"clientUpdated": "Клиент обновлен",
|
||||||
"clientUpdatedDescription": "The client has been updated.",
|
"clientUpdatedDescription": "Клиент был обновлён.",
|
||||||
"clientUpdateFailed": "Failed to update client",
|
"clientUpdateFailed": "Не удалось обновить клиента",
|
||||||
"clientUpdateError": "An error occurred while updating the client.",
|
"clientUpdateError": "Произошла ошибка при обновлении клиента.",
|
||||||
"sitesFetchFailed": "Failed to fetch sites",
|
"sitesFetchFailed": "Не удалось получить сайты",
|
||||||
"sitesFetchError": "An error occurred while fetching sites.",
|
"sitesFetchError": "Произошла ошибка при получении сайтов.",
|
||||||
"olmErrorFetchReleases": "An error occurred while fetching Olm releases.",
|
"olmErrorFetchReleases": "Произошла ошибка при получении релизов Olm.",
|
||||||
"olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.",
|
"olmErrorFetchLatest": "Произошла ошибка при получении последнего релиза Olm.",
|
||||||
"remoteSubnets": "Remote Subnets",
|
"remoteSubnets": "Удалённые подсети",
|
||||||
"enterCidrRange": "Enter CIDR range",
|
"enterCidrRange": "Введите диапазон CIDR",
|
||||||
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
"remoteSubnetsDescription": "Добавьте диапазоны адресов CIDR, которые можно получить из этого сайта удаленно, используя клиентов. Используйте формат 10.0.0.0/24. Это относится ТОЛЬКО к подключению через VPN клиентов.",
|
||||||
"resourceEnableProxy": "Enable Public Proxy",
|
"resourceEnableProxy": "Включить публичный прокси",
|
||||||
"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.",
|
"resourceEnableProxyDescription": "Включите публичное проксирование для этого ресурса. Это позволяет получить доступ к ресурсу извне сети через облако через открытый порт. Требуется конфигурация Traefik.",
|
||||||
"externalProxyEnabled": "External Proxy Enabled"
|
"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 для адресации клиентов. Должен находиться в пределах подсети организационного уровня.",
|
||||||
|
"autoLoginExternalIdp": "Автоматический вход с внешним провайдером",
|
||||||
|
"autoLoginExternalIdpDescription": "Немедленно перенаправьте пользователя к внешнему провайдеру для аутентификации.",
|
||||||
|
"selectIdp": "Выберите провайдера",
|
||||||
|
"selectIdpPlaceholder": "Выберите провайдера...",
|
||||||
|
"selectIdpRequired": "Пожалуйста, выберите провайдера, когда автоматический вход включен.",
|
||||||
|
"autoLoginTitle": "Перенаправление",
|
||||||
|
"autoLoginDescription": "Перенаправление вас к внешнему провайдеру для аутентификации.",
|
||||||
|
"autoLoginProcessing": "Подготовка аутентификации...",
|
||||||
|
"autoLoginRedirecting": "Перенаправление к входу...",
|
||||||
|
"autoLoginError": "Ошибка автоматического входа",
|
||||||
|
"autoLoginErrorNoRedirectUrl": "URL-адрес перенаправления не получен от провайдера удостоверения.",
|
||||||
|
"autoLoginErrorGeneratingUrl": "Не удалось сгенерировать URL-адрес аутентификации.",
|
||||||
|
"managedSelfHosted": {
|
||||||
|
"title": "Управляемый с самовывоза",
|
||||||
|
"description": "Более надежный и низко обслуживаемый сервер Pangolin с дополнительными колокольнями и свистками",
|
||||||
|
"introTitle": "Управляемый Само-Хост Панголина",
|
||||||
|
"introDescription": "- это вариант развертывания, предназначенный для людей, которые хотят простоты и надёжности, сохраняя при этом свои данные конфиденциальными и самостоятельными.",
|
||||||
|
"introDetail": "С помощью этой опции вы по-прежнему используете узел Pangolin — туннели, SSL, и весь остающийся на вашем сервере. Разница заключается в том, что управление и мониторинг осуществляются через нашу панель инструментов из облака, которая открывает ряд преимуществ:",
|
||||||
|
"benefitSimplerOperations": {
|
||||||
|
"title": "Более простые операции",
|
||||||
|
"description": "Не нужно запускать свой собственный почтовый сервер или настроить комплексное оповещение. Вы будете получать проверки состояния здоровья и оповещения о неисправностях из коробки."
|
||||||
|
},
|
||||||
|
"benefitAutomaticUpdates": {
|
||||||
|
"title": "Автоматическое обновление",
|
||||||
|
"description": "Панель управления в облаке развивается быстро, так что вы получаете новые функции и исправления ошибок, без необходимости каждый раз получать новые контейнеры."
|
||||||
|
},
|
||||||
|
"benefitLessMaintenance": {
|
||||||
|
"title": "Меньше обслуживания",
|
||||||
|
"description": "Нет миграции баз данных, резервных копий или дополнительной инфраструктуры для управления. Мы обрабатываем это в облаке."
|
||||||
|
},
|
||||||
|
"benefitCloudFailover": {
|
||||||
|
"title": "Облачное срабатывание",
|
||||||
|
"description": "Если ваш узел исчезнет, ваши туннели могут временно прерваться до наших облачных точек присутствия, пока вы не вернете его в сети."
|
||||||
|
},
|
||||||
|
"benefitHighAvailability": {
|
||||||
|
"title": "Высокая доступность (PoP)",
|
||||||
|
"description": "Вы также можете прикрепить несколько узлов к вашему аккаунту для избыточности и лучшей производительности."
|
||||||
|
},
|
||||||
|
"benefitFutureEnhancements": {
|
||||||
|
"title": "Будущие улучшения",
|
||||||
|
"description": "Мы планируем добавить дополнительные инструменты аналитики, оповещения и управления, чтобы сделать установку еще более надежной."
|
||||||
|
},
|
||||||
|
"docsAlert": {
|
||||||
|
"text": "Узнайте больше о опции Managed Self-Hosted в нашей",
|
||||||
|
"documentation": "документация"
|
||||||
|
},
|
||||||
|
"convertButton": "Конвертировать этот узел в управляемый себе-хост"
|
||||||
|
},
|
||||||
|
"internationaldomaindetected": "Обнаружен международный домен",
|
||||||
|
"willbestoredas": "Будет храниться как:"
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,9 @@
|
|||||||
"siteNewtTunnelDescription": "Ağınıza giriş noktası oluşturmanın en kolay yolu. Ekstra kurulum gerekmez.",
|
"siteNewtTunnelDescription": "Ağınıza giriş noktası oluşturmanın en kolay yolu. Ekstra kurulum gerekmez.",
|
||||||
"siteWg": "Temel WireGuard",
|
"siteWg": "Temel WireGuard",
|
||||||
"siteWgDescription": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir.",
|
"siteWgDescription": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir.",
|
||||||
|
"siteWgDescriptionSaas": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir. YALNIZCA SELF HOSTED DÜĞÜMLERDE ÇALIŞIR",
|
||||||
"siteLocalDescription": "Yalnızca yerel kaynaklar. Tünelleme yok.",
|
"siteLocalDescription": "Yalnızca yerel kaynaklar. Tünelleme yok.",
|
||||||
|
"siteLocalDescriptionSaas": "Yalnızca yerel kaynaklar. Tünel yok. YALNIZCA SELF HOSTED DÜĞÜMLERDE ÇALIŞIR",
|
||||||
"siteSeeAll": "Tüm Siteleri Gör",
|
"siteSeeAll": "Tüm Siteleri Gör",
|
||||||
"siteTunnelDescription": "Sitenize nasıl bağlanmak istediğinizi belirleyin",
|
"siteTunnelDescription": "Sitenize nasıl bağlanmak istediğinizi belirleyin",
|
||||||
"siteNewtCredentials": "Newt Kimlik Bilgileri",
|
"siteNewtCredentials": "Newt Kimlik Bilgileri",
|
||||||
@@ -166,7 +168,7 @@
|
|||||||
"siteSelect": "Site seç",
|
"siteSelect": "Site seç",
|
||||||
"siteSearch": "Site ara",
|
"siteSearch": "Site ara",
|
||||||
"siteNotFound": "Herhangi bir site bulunamadı.",
|
"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ü",
|
"resourceType": "Kaynak Türü",
|
||||||
"resourceTypeDescription": "Kaynağınıza nasıl erişmek istediğinizi belirleyin",
|
"resourceTypeDescription": "Kaynağınıza nasıl erişmek istediğinizi belirleyin",
|
||||||
"resourceHTTPSSettings": "HTTPS Ayarları",
|
"resourceHTTPSSettings": "HTTPS Ayarları",
|
||||||
@@ -197,11 +199,13 @@
|
|||||||
"general": "Genel",
|
"general": "Genel",
|
||||||
"generalSettings": "Genel Ayarlar",
|
"generalSettings": "Genel Ayarlar",
|
||||||
"proxy": "Vekil Sunucu",
|
"proxy": "Vekil Sunucu",
|
||||||
|
"internal": "Dahili",
|
||||||
"rules": "Kurallar",
|
"rules": "Kurallar",
|
||||||
"resourceSettingDescription": "Kaynağınızdaki ayarları yapılandırın",
|
"resourceSettingDescription": "Kaynağınızdaki ayarları yapılandırın",
|
||||||
"resourceSetting": "{resourceName} Ayarları",
|
"resourceSetting": "{resourceName} Ayarları",
|
||||||
"alwaysAllow": "Her Zaman İzin Ver",
|
"alwaysAllow": "Her Zaman İzin Ver",
|
||||||
"alwaysDeny": "Her Zaman Reddet",
|
"alwaysDeny": "Her Zaman Reddet",
|
||||||
|
"passToAuth": "Kimlik Doğrulamasına Geç",
|
||||||
"orgSettingsDescription": "Organizasyonunuzun genel ayarlarını yapılandırın",
|
"orgSettingsDescription": "Organizasyonunuzun genel ayarlarını yapılandırın",
|
||||||
"orgGeneralSettings": "Organizasyon Ayarları",
|
"orgGeneralSettings": "Organizasyon Ayarları",
|
||||||
"orgGeneralSettingsDescription": "Organizasyon detaylarınızı ve yapılandırmanızı yönetin",
|
"orgGeneralSettingsDescription": "Organizasyon detaylarınızı ve yapılandırmanızı yönetin",
|
||||||
@@ -490,7 +494,7 @@
|
|||||||
"targetTlsSniDescription": "SNI için kullanılacak TLS Sunucu Adı'",
|
"targetTlsSniDescription": "SNI için kullanılacak TLS Sunucu Adı'",
|
||||||
"targetTlsSubmit": "Ayarları Kaydet",
|
"targetTlsSubmit": "Ayarları Kaydet",
|
||||||
"targets": "Hedefler Konfigürasyonu",
|
"targets": "Hedefler Konfigürasyonu",
|
||||||
"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",
|
"targetStickySessions": "Yapışkan Oturumları Etkinleştir",
|
||||||
"targetStickySessionsDescription": "Bağlantıları oturum süresince aynı arka uç hedef üzerinde tutun.",
|
"targetStickySessionsDescription": "Bağlantıları oturum süresince aynı arka uç hedef üzerinde tutun.",
|
||||||
"methodSelect": "Yöntemi Seç",
|
"methodSelect": "Yöntemi Seç",
|
||||||
@@ -542,6 +546,7 @@
|
|||||||
"rulesActions": "Aksiyonlar",
|
"rulesActions": "Aksiyonlar",
|
||||||
"rulesActionAlwaysAllow": "Her Zaman İzin Ver: Tüm kimlik doğrulama yöntemlerini atlayın",
|
"rulesActionAlwaysAllow": "Her Zaman İzin Ver: Tüm kimlik doğrulama yöntemlerini atlayın",
|
||||||
"rulesActionAlwaysDeny": "Her Zaman Reddedin: Tüm istekleri engelleyin; kimlik doğrulaması yapılamaz",
|
"rulesActionAlwaysDeny": "Her Zaman Reddedin: Tüm istekleri engelleyin; kimlik doğrulaması yapılamaz",
|
||||||
|
"rulesActionPassToAuth": "Kimlik Doğrulamasına Geç: Kimlik doğrulama yöntemlerinin denenmesine izin ver",
|
||||||
"rulesMatchCriteria": "Eşleşme Kriterleri",
|
"rulesMatchCriteria": "Eşleşme Kriterleri",
|
||||||
"rulesMatchCriteriaIpAddress": "Belirli bir IP adresi ile eşleşme",
|
"rulesMatchCriteriaIpAddress": "Belirli bir IP adresi ile eşleşme",
|
||||||
"rulesMatchCriteriaIpAddressRange": "CIDR gösteriminde bir IP adresi aralığı ile eşleşme",
|
"rulesMatchCriteriaIpAddressRange": "CIDR gösteriminde bir IP adresi aralığı ile eşleşme",
|
||||||
@@ -833,24 +838,24 @@
|
|||||||
"pincodeRequirementsLength": "PIN kesinlikle 6 haneli olmalıdır",
|
"pincodeRequirementsLength": "PIN kesinlikle 6 haneli olmalıdır",
|
||||||
"pincodeRequirementsChars": "PIN sadece numaralardan oluşmalıdır",
|
"pincodeRequirementsChars": "PIN sadece numaralardan oluşmalıdır",
|
||||||
"passwordRequirementsLength": "Şifre en az 1 karakter uzunluğunda olmalıdır",
|
"passwordRequirementsLength": "Şifre en az 1 karakter uzunluğunda olmalıdır",
|
||||||
"passwordRequirementsTitle": "Password requirements:",
|
"passwordRequirementsTitle": "Şifre gereksinimleri:",
|
||||||
"passwordRequirementLength": "At least 8 characters long",
|
"passwordRequirementLength": "En az 8 karakter uzunluğunda",
|
||||||
"passwordRequirementUppercase": "At least one uppercase letter",
|
"passwordRequirementUppercase": "En az bir büyük harf",
|
||||||
"passwordRequirementLowercase": "At least one lowercase letter",
|
"passwordRequirementLowercase": "En az bir küçük harf",
|
||||||
"passwordRequirementNumber": "At least one number",
|
"passwordRequirementNumber": "En az bir sayı",
|
||||||
"passwordRequirementSpecial": "At least one special character",
|
"passwordRequirementSpecial": "En az bir özel karakter",
|
||||||
"passwordRequirementsMet": "✓ Password meets all requirements",
|
"passwordRequirementsMet": "✓ Şifre tüm gereksinimleri karşılıyor",
|
||||||
"passwordStrength": "Password strength",
|
"passwordStrength": "Şifre gücü",
|
||||||
"passwordStrengthWeak": "Weak",
|
"passwordStrengthWeak": "Zayıf",
|
||||||
"passwordStrengthMedium": "Medium",
|
"passwordStrengthMedium": "Orta",
|
||||||
"passwordStrengthStrong": "Strong",
|
"passwordStrengthStrong": "Güçlü",
|
||||||
"passwordRequirements": "Requirements:",
|
"passwordRequirements": "Gereksinimler:",
|
||||||
"passwordRequirementLengthText": "8+ characters",
|
"passwordRequirementLengthText": "8+ karakter",
|
||||||
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
"passwordRequirementUppercaseText": "Büyük harf (A-Z)",
|
||||||
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
"passwordRequirementLowercaseText": "Küçük harf (a-z)",
|
||||||
"passwordRequirementNumberText": "Number (0-9)",
|
"passwordRequirementNumberText": "Sayı (0-9)",
|
||||||
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
"passwordRequirementSpecialText": "Özel karakter (!@#$%...)",
|
||||||
"passwordsDoNotMatch": "Passwords do not match",
|
"passwordsDoNotMatch": "Parolalar eşleşmiyor",
|
||||||
"otpEmailRequirementsLength": "OTP en az 1 karakter uzunluğunda olmalıdır",
|
"otpEmailRequirementsLength": "OTP en az 1 karakter uzunluğunda olmalıdır",
|
||||||
"otpEmailSent": "OTP Gönderildi",
|
"otpEmailSent": "OTP Gönderildi",
|
||||||
"otpEmailSentDescription": "E-posta adresinize bir OTP gönderildi",
|
"otpEmailSentDescription": "E-posta adresinize bir OTP gönderildi",
|
||||||
@@ -970,6 +975,7 @@
|
|||||||
"logoutError": "Çıkış yaparken hata",
|
"logoutError": "Çıkış yaparken hata",
|
||||||
"signingAs": "Olarak giriş yapıldı",
|
"signingAs": "Olarak giriş yapıldı",
|
||||||
"serverAdmin": "Sunucu Yöneticisi",
|
"serverAdmin": "Sunucu Yöneticisi",
|
||||||
|
"managedSelfhosted": "Yönetilen Self-Hosted",
|
||||||
"otpEnable": "İki faktörlü özelliğini etkinleştir",
|
"otpEnable": "İki faktörlü özelliğini etkinleştir",
|
||||||
"otpDisable": "İki faktörlü özelliğini devre dışı bırak",
|
"otpDisable": "İki faktörlü özelliğini devre dışı bırak",
|
||||||
"logout": "Çıkış Yap",
|
"logout": "Çıkış Yap",
|
||||||
@@ -985,9 +991,9 @@
|
|||||||
"actionDeleteSite": "Siteyi Sil",
|
"actionDeleteSite": "Siteyi Sil",
|
||||||
"actionGetSite": "Siteyi Al",
|
"actionGetSite": "Siteyi Al",
|
||||||
"actionListSites": "Siteleri Listele",
|
"actionListSites": "Siteleri Listele",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "Kurulum Simgesi",
|
||||||
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
"setupTokenDescription": "Sunucu konsolundan kurulum simgesini girin.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "Kurulum simgesi gerekli",
|
||||||
"actionUpdateSite": "Siteyi Güncelle",
|
"actionUpdateSite": "Siteyi Güncelle",
|
||||||
"actionListSiteRoles": "İzin Verilen Site Rolleri Listele",
|
"actionListSiteRoles": "İzin Verilen Site Rolleri Listele",
|
||||||
"actionCreateResource": "Kaynak Oluştur",
|
"actionCreateResource": "Kaynak Oluştur",
|
||||||
@@ -1048,6 +1054,12 @@
|
|||||||
"actionUpdateClient": "Müşteri Güncelle",
|
"actionUpdateClient": "Müşteri Güncelle",
|
||||||
"actionListClients": "Müşterileri Listele",
|
"actionListClients": "Müşterileri Listele",
|
||||||
"actionGetClient": "Müşteriyi Al",
|
"actionGetClient": "Müşteriyi Al",
|
||||||
|
"actionCreateSiteResource": "Site Kaynağı Oluştur",
|
||||||
|
"actionDeleteSiteResource": "Site Kaynağını Sil",
|
||||||
|
"actionGetSiteResource": "Site Kaynağını Al",
|
||||||
|
"actionListSiteResources": "Site Kaynaklarını Listele",
|
||||||
|
"actionUpdateSiteResource": "Site Kaynağını Güncelle",
|
||||||
|
"actionListInvitations": "Davetiyeleri Listele",
|
||||||
"noneSelected": "Hiçbiri seçili değil",
|
"noneSelected": "Hiçbiri seçili değil",
|
||||||
"orgNotFound2": "Hiçbir organizasyon bulunamadı.",
|
"orgNotFound2": "Hiçbir organizasyon bulunamadı.",
|
||||||
"searchProgress": "Ara...",
|
"searchProgress": "Ara...",
|
||||||
@@ -1341,8 +1353,148 @@
|
|||||||
"olmErrorFetchLatest": "En son Olm yayını 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",
|
"remoteSubnets": "Uzak Alt Ağlar",
|
||||||
"enterCidrRange": "CIDR aralığını girin",
|
"enterCidrRange": "CIDR aralığını girin",
|
||||||
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
"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",
|
"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.",
|
"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"
|
"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ı.",
|
||||||
|
"managedSelfHosted": {
|
||||||
|
"title": "Yönetilen Self-Hosted",
|
||||||
|
"description": "Daha güvenilir ve düşük bakım gerektiren, ekstra özelliklere sahip kendi kendine barındırabileceğiniz Pangolin sunucusu",
|
||||||
|
"introTitle": "Yönetilen Kendi Kendine Barındırılan Pangolin",
|
||||||
|
"introDescription": "Bu, basitlik ve ekstra güvenilirlik arayan, ancak verilerini gizli tutmak ve kendi sunucularında barındırmak isteyen kişiler için tasarlanmış bir dağıtım seçeneğidir.",
|
||||||
|
"introDetail": "Bu seçenekle, kendi Pangolin düğümünüzü çalıştırmaya devam edersiniz — tünelleriniz, SSL bitişiniz ve trafiğiniz tamamen sunucunuzda kalır. Fark, yönetim ve izlemeyi bulut panomuz üzerinden gerçekleştiririz, bu da bir dizi avantaj sağlar:",
|
||||||
|
"benefitSimplerOperations": {
|
||||||
|
"title": "Daha basit işlemler",
|
||||||
|
"description": "Kendi e-posta sunucunuzu çalıştırmanıza veya karmaşık uyarılar kurmanıza gerek yok. Sağlık kontrolleri ve kesinti uyarılarını kutudan çıktığı gibi alırsınız."
|
||||||
|
},
|
||||||
|
"benefitAutomaticUpdates": {
|
||||||
|
"title": "Otomatik güncellemeler",
|
||||||
|
"description": "Bulut panosu hızla gelişir, böylece her seferinde yeni konteynerler manuel olarak çekmeden yeni özellikler ve hata düzeltmeleri alırsınız."
|
||||||
|
},
|
||||||
|
"benefitLessMaintenance": {
|
||||||
|
"title": "Daha az bakım",
|
||||||
|
"description": "Veritabanı geçişleri, yedeklemeler veya ekstra altyapı yönetimi yok. Biz bunu bulutta hallederiz."
|
||||||
|
},
|
||||||
|
"benefitCloudFailover": {
|
||||||
|
"title": "Bulut yedekleme",
|
||||||
|
"description": "Düğümünüz kapandığında, tünelleriniz geçici olarak bulut bağlantı noktalarımıza geçebilir, böylece tekrar çevrimiçi hale getirene kadar tünelleriniz kesintiye uğramaz."
|
||||||
|
},
|
||||||
|
"benefitHighAvailability": {
|
||||||
|
"title": "Yüksek kullanılabilirlik (Bağlantı Noktaları)",
|
||||||
|
"description": "Yedeklilik ve daha iyi performans için hesabınıza birden fazla düğüm bağlayabilirsiniz."
|
||||||
|
},
|
||||||
|
"benefitFutureEnhancements": {
|
||||||
|
"title": "Gelecek iyileştirmeler",
|
||||||
|
"description": "Dağıtımınızı daha sağlam hale getirmek amacıyla daha fazla analiz, uyarı ve yönetim aracı eklemeyi planlıyoruz."
|
||||||
|
},
|
||||||
|
"docsAlert": {
|
||||||
|
"text": "Yönetilen Kendi Kendine Barındırılan seçeneği hakkında daha fazla bilgi edinin",
|
||||||
|
"documentation": "dokümantasyon"
|
||||||
|
},
|
||||||
|
"convertButton": "Bu Düğümü Yönetilen Kendi Kendine Barındırma Dönüştürün"
|
||||||
|
},
|
||||||
|
"internationaldomaindetected": "Uluslararası Alan Adı Tespit Edildi",
|
||||||
|
"willbestoredas": "Şu şekilde depolanacak:"
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,9 @@
|
|||||||
"siteNewtTunnelDescription": "最简单的方式来连接到您的网络。不需要任何额外设置。",
|
"siteNewtTunnelDescription": "最简单的方式来连接到您的网络。不需要任何额外设置。",
|
||||||
"siteWg": "基本 WireGuard",
|
"siteWg": "基本 WireGuard",
|
||||||
"siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。",
|
"siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。",
|
||||||
|
"siteWgDescriptionSaas": "使用任何WireGuard客户端建立隧道。需要手动配置NAT。仅适用于自托管节点。",
|
||||||
"siteLocalDescription": "仅限本地资源。不需要隧道。",
|
"siteLocalDescription": "仅限本地资源。不需要隧道。",
|
||||||
|
"siteLocalDescriptionSaas": "仅本地资源。无需隧道。仅适用于自托管节点。",
|
||||||
"siteSeeAll": "查看所有站点",
|
"siteSeeAll": "查看所有站点",
|
||||||
"siteTunnelDescription": "确定如何连接到您的网站",
|
"siteTunnelDescription": "确定如何连接到您的网站",
|
||||||
"siteNewtCredentials": "Newt 凭据",
|
"siteNewtCredentials": "Newt 凭据",
|
||||||
@@ -166,7 +168,7 @@
|
|||||||
"siteSelect": "选择站点",
|
"siteSelect": "选择站点",
|
||||||
"siteSearch": "搜索站点",
|
"siteSearch": "搜索站点",
|
||||||
"siteNotFound": "未找到站点。",
|
"siteNotFound": "未找到站点。",
|
||||||
"siteSelectionDescription": "此站点将为资源提供连接。",
|
"siteSelectionDescription": "此站点将为目标提供连接。",
|
||||||
"resourceType": "资源类型",
|
"resourceType": "资源类型",
|
||||||
"resourceTypeDescription": "确定如何访问您的资源",
|
"resourceTypeDescription": "确定如何访问您的资源",
|
||||||
"resourceHTTPSSettings": "HTTPS 设置",
|
"resourceHTTPSSettings": "HTTPS 设置",
|
||||||
@@ -197,11 +199,13 @@
|
|||||||
"general": "概览",
|
"general": "概览",
|
||||||
"generalSettings": "常规设置",
|
"generalSettings": "常规设置",
|
||||||
"proxy": "代理服务器",
|
"proxy": "代理服务器",
|
||||||
|
"internal": "内部设置",
|
||||||
"rules": "规则",
|
"rules": "规则",
|
||||||
"resourceSettingDescription": "配置您资源上的设置",
|
"resourceSettingDescription": "配置您资源上的设置",
|
||||||
"resourceSetting": "{resourceName} 设置",
|
"resourceSetting": "{resourceName} 设置",
|
||||||
"alwaysAllow": "一律允许",
|
"alwaysAllow": "一律允许",
|
||||||
"alwaysDeny": "一律拒绝",
|
"alwaysDeny": "一律拒绝",
|
||||||
|
"passToAuth": "传递至认证",
|
||||||
"orgSettingsDescription": "配置您组织的一般设置",
|
"orgSettingsDescription": "配置您组织的一般设置",
|
||||||
"orgGeneralSettings": "组织设置",
|
"orgGeneralSettings": "组织设置",
|
||||||
"orgGeneralSettingsDescription": "管理您的机构详细信息和配置",
|
"orgGeneralSettingsDescription": "管理您的机构详细信息和配置",
|
||||||
@@ -490,7 +494,7 @@
|
|||||||
"targetTlsSniDescription": "SNI使用的 TLS 服务器名称。留空使用默认值。",
|
"targetTlsSniDescription": "SNI使用的 TLS 服务器名称。留空使用默认值。",
|
||||||
"targetTlsSubmit": "保存设置",
|
"targetTlsSubmit": "保存设置",
|
||||||
"targets": "目标配置",
|
"targets": "目标配置",
|
||||||
"targetsDescription": "设置目标来路由流量到您的服务",
|
"targetsDescription": "设置目标来路由流量到您的后端服务",
|
||||||
"targetStickySessions": "启用置顶会话",
|
"targetStickySessions": "启用置顶会话",
|
||||||
"targetStickySessionsDescription": "将连接保持在同一个后端目标的整个会话中。",
|
"targetStickySessionsDescription": "将连接保持在同一个后端目标的整个会话中。",
|
||||||
"methodSelect": "选择方法",
|
"methodSelect": "选择方法",
|
||||||
@@ -542,6 +546,7 @@
|
|||||||
"rulesActions": "行动",
|
"rulesActions": "行动",
|
||||||
"rulesActionAlwaysAllow": "总是允许:绕过所有身份验证方法",
|
"rulesActionAlwaysAllow": "总是允许:绕过所有身份验证方法",
|
||||||
"rulesActionAlwaysDeny": "总是拒绝:阻止所有请求;无法尝试验证",
|
"rulesActionAlwaysDeny": "总是拒绝:阻止所有请求;无法尝试验证",
|
||||||
|
"rulesActionPassToAuth": "传递至认证:允许尝试身份验证方法",
|
||||||
"rulesMatchCriteria": "匹配条件",
|
"rulesMatchCriteria": "匹配条件",
|
||||||
"rulesMatchCriteriaIpAddress": "匹配一个指定的 IP 地址",
|
"rulesMatchCriteriaIpAddress": "匹配一个指定的 IP 地址",
|
||||||
"rulesMatchCriteriaIpAddressRange": "在 CIDR 符号中匹配一系列IP地址",
|
"rulesMatchCriteriaIpAddressRange": "在 CIDR 符号中匹配一系列IP地址",
|
||||||
@@ -833,24 +838,24 @@
|
|||||||
"pincodeRequirementsLength": "PIN码必须是6位数字",
|
"pincodeRequirementsLength": "PIN码必须是6位数字",
|
||||||
"pincodeRequirementsChars": "PIN 必须只包含数字",
|
"pincodeRequirementsChars": "PIN 必须只包含数字",
|
||||||
"passwordRequirementsLength": "密码必须至少 1 个字符长",
|
"passwordRequirementsLength": "密码必须至少 1 个字符长",
|
||||||
"passwordRequirementsTitle": "Password requirements:",
|
"passwordRequirementsTitle": "密码要求:",
|
||||||
"passwordRequirementLength": "At least 8 characters long",
|
"passwordRequirementLength": "至少8个字符长",
|
||||||
"passwordRequirementUppercase": "At least one uppercase letter",
|
"passwordRequirementUppercase": "至少一个大写字母",
|
||||||
"passwordRequirementLowercase": "At least one lowercase letter",
|
"passwordRequirementLowercase": "至少一个小写字母",
|
||||||
"passwordRequirementNumber": "At least one number",
|
"passwordRequirementNumber": "至少一个数字",
|
||||||
"passwordRequirementSpecial": "At least one special character",
|
"passwordRequirementSpecial": "至少一个特殊字符",
|
||||||
"passwordRequirementsMet": "✓ Password meets all requirements",
|
"passwordRequirementsMet": "✓ 密码满足所有要求",
|
||||||
"passwordStrength": "Password strength",
|
"passwordStrength": "密码强度",
|
||||||
"passwordStrengthWeak": "Weak",
|
"passwordStrengthWeak": "弱",
|
||||||
"passwordStrengthMedium": "Medium",
|
"passwordStrengthMedium": "中",
|
||||||
"passwordStrengthStrong": "Strong",
|
"passwordStrengthStrong": "强",
|
||||||
"passwordRequirements": "Requirements:",
|
"passwordRequirements": "要求:",
|
||||||
"passwordRequirementLengthText": "8+ characters",
|
"passwordRequirementLengthText": "8+ 个字符",
|
||||||
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
"passwordRequirementUppercaseText": "大写字母 (A-Z)",
|
||||||
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
"passwordRequirementLowercaseText": "小写字母 (a-z)",
|
||||||
"passwordRequirementNumberText": "Number (0-9)",
|
"passwordRequirementNumberText": "数字 (0-9)",
|
||||||
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
"passwordRequirementSpecialText": "特殊字符 (!@#$%...)",
|
||||||
"passwordsDoNotMatch": "Passwords do not match",
|
"passwordsDoNotMatch": "密码不匹配",
|
||||||
"otpEmailRequirementsLength": "OTP 必须至少 1 个字符长",
|
"otpEmailRequirementsLength": "OTP 必须至少 1 个字符长",
|
||||||
"otpEmailSent": "OTP 已发送",
|
"otpEmailSent": "OTP 已发送",
|
||||||
"otpEmailSentDescription": "OTP 已经发送到您的电子邮件",
|
"otpEmailSentDescription": "OTP 已经发送到您的电子邮件",
|
||||||
@@ -970,6 +975,7 @@
|
|||||||
"logoutError": "注销错误",
|
"logoutError": "注销错误",
|
||||||
"signingAs": "登录为",
|
"signingAs": "登录为",
|
||||||
"serverAdmin": "服务器管理员",
|
"serverAdmin": "服务器管理员",
|
||||||
|
"managedSelfhosted": "托管自托管",
|
||||||
"otpEnable": "启用双因子认证",
|
"otpEnable": "启用双因子认证",
|
||||||
"otpDisable": "禁用双因子认证",
|
"otpDisable": "禁用双因子认证",
|
||||||
"logout": "登出",
|
"logout": "登出",
|
||||||
@@ -985,9 +991,9 @@
|
|||||||
"actionDeleteSite": "删除站点",
|
"actionDeleteSite": "删除站点",
|
||||||
"actionGetSite": "获取站点",
|
"actionGetSite": "获取站点",
|
||||||
"actionListSites": "站点列表",
|
"actionListSites": "站点列表",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "设置令牌",
|
||||||
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
"setupTokenDescription": "从服务器控制台输入设置令牌。",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "需要设置令牌",
|
||||||
"actionUpdateSite": "更新站点",
|
"actionUpdateSite": "更新站点",
|
||||||
"actionListSiteRoles": "允许站点角色列表",
|
"actionListSiteRoles": "允许站点角色列表",
|
||||||
"actionCreateResource": "创建资源",
|
"actionCreateResource": "创建资源",
|
||||||
@@ -1048,6 +1054,12 @@
|
|||||||
"actionUpdateClient": "更新客户端",
|
"actionUpdateClient": "更新客户端",
|
||||||
"actionListClients": "列出客户端",
|
"actionListClients": "列出客户端",
|
||||||
"actionGetClient": "获取客户端",
|
"actionGetClient": "获取客户端",
|
||||||
|
"actionCreateSiteResource": "创建站点资源",
|
||||||
|
"actionDeleteSiteResource": "删除站点资源",
|
||||||
|
"actionGetSiteResource": "获取站点资源",
|
||||||
|
"actionListSiteResources": "列出站点资源",
|
||||||
|
"actionUpdateSiteResource": "更新站点资源",
|
||||||
|
"actionListInvitations": "邀请列表",
|
||||||
"noneSelected": "未选择",
|
"noneSelected": "未选择",
|
||||||
"orgNotFound2": "未找到组织。",
|
"orgNotFound2": "未找到组织。",
|
||||||
"searchProgress": "搜索中...",
|
"searchProgress": "搜索中...",
|
||||||
@@ -1341,8 +1353,148 @@
|
|||||||
"olmErrorFetchLatest": "获取最新 Olm 发布版本时出错。",
|
"olmErrorFetchLatest": "获取最新 Olm 发布版本时出错。",
|
||||||
"remoteSubnets": "远程子网",
|
"remoteSubnets": "远程子网",
|
||||||
"enterCidrRange": "输入 CIDR 范围",
|
"enterCidrRange": "输入 CIDR 范围",
|
||||||
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
"remoteSubnetsDescription": "添加可以通过客户端远程访问该站点的CIDR范围。使用类似10.0.0.0/24的格式。这仅适用于VPN客户端连接。",
|
||||||
"resourceEnableProxy": "启用公共代理",
|
"resourceEnableProxy": "启用公共代理",
|
||||||
"resourceEnableProxyDescription": "启用到此资源的公共代理。这允许外部网络通过开放端口访问资源。需要 Traefik 配置。",
|
"resourceEnableProxyDescription": "启用到此资源的公共代理。这允许外部网络通过开放端口访问资源。需要 Traefik 配置。",
|
||||||
"externalProxyEnabled": "外部代理已启用"
|
"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失败。",
|
||||||
|
"managedSelfHosted": {
|
||||||
|
"title": "托管自托管",
|
||||||
|
"description": "更可靠和低维护自我托管的 Pangolin 服务器,带有额外的铃声和告密器",
|
||||||
|
"introTitle": "托管自托管的潘戈林公司",
|
||||||
|
"introDescription": "这是一种部署选择,为那些希望简洁和额外可靠的人设计,同时仍然保持他们的数据的私密性和自我托管性。",
|
||||||
|
"introDetail": "通过此选项,您仍然运行您自己的 Pangolin 节点 — — 您的隧道、SSL 终止,并且流量在您的服务器上保持所有状态。 不同之处在于,管理和监测是通过我们的云层仪表板进行的,该仪表板开启了一些好处:",
|
||||||
|
"benefitSimplerOperations": {
|
||||||
|
"title": "简单的操作",
|
||||||
|
"description": "无需运行您自己的邮件服务器或设置复杂的警报。您将从方框中获得健康检查和下限提醒。"
|
||||||
|
},
|
||||||
|
"benefitAutomaticUpdates": {
|
||||||
|
"title": "自动更新",
|
||||||
|
"description": "云仪表盘快速演化,所以您可以获得新的功能和错误修复,而不必每次手动拉取新的容器。"
|
||||||
|
},
|
||||||
|
"benefitLessMaintenance": {
|
||||||
|
"title": "减少维护时间",
|
||||||
|
"description": "没有要管理的数据库迁移、备份或额外的基础设施。我们在云端处理这个问题。"
|
||||||
|
},
|
||||||
|
"benefitCloudFailover": {
|
||||||
|
"title": "云失败",
|
||||||
|
"description": "如果您的节点被关闭,您的隧道可能暂时无法连接到我们的云端,直到您将其重新连接上线。"
|
||||||
|
},
|
||||||
|
"benefitHighAvailability": {
|
||||||
|
"title": "高可用率(PoPs)",
|
||||||
|
"description": "您还可以将多个节点添加到您的帐户中以获取冗余和更好的性能。"
|
||||||
|
},
|
||||||
|
"benefitFutureEnhancements": {
|
||||||
|
"title": "将来的改进",
|
||||||
|
"description": "我们正在计划添加更多的分析、警报和管理工具,使你的部署更加有力。"
|
||||||
|
},
|
||||||
|
"docsAlert": {
|
||||||
|
"text": "在我们中更多地了解管理下的自托管选项",
|
||||||
|
"documentation": "文档"
|
||||||
|
},
|
||||||
|
"convertButton": "将此节点转换为管理自托管的"
|
||||||
|
},
|
||||||
|
"internationaldomaindetected": "检测到国际域",
|
||||||
|
"willbestoredas": "储存为:"
|
||||||
}
|
}
|
||||||
662
package-lock.json
generated
662
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@@ -21,8 +21,7 @@
|
|||||||
"db:clear-migrations": "rm -rf server/migrations",
|
"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:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
|
||||||
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
|
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
|
||||||
"start: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": "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'",
|
|
||||||
"email": "email dev --dir server/emails/templates --port 3005",
|
"email": "email dev --dir server/emails/templates --port 3005",
|
||||||
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs"
|
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs"
|
||||||
},
|
},
|
||||||
@@ -33,23 +32,23 @@
|
|||||||
"@oslojs/crypto": "1.0.1",
|
"@oslojs/crypto": "1.0.1",
|
||||||
"@oslojs/encoding": "1.1.0",
|
"@oslojs/encoding": "1.1.0",
|
||||||
"@radix-ui/react-avatar": "1.1.10",
|
"@radix-ui/react-avatar": "1.1.10",
|
||||||
"@radix-ui/react-checkbox": "1.3.2",
|
"@radix-ui/react-checkbox": "1.3.3",
|
||||||
"@radix-ui/react-collapsible": "1.1.11",
|
"@radix-ui/react-collapsible": "1.1.12",
|
||||||
"@radix-ui/react-dialog": "1.1.14",
|
"@radix-ui/react-dialog": "1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "2.1.15",
|
"@radix-ui/react-dropdown-menu": "2.1.16",
|
||||||
"@radix-ui/react-icons": "1.3.2",
|
"@radix-ui/react-icons": "1.3.2",
|
||||||
"@radix-ui/react-label": "2.1.7",
|
"@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-progress": "^1.1.7",
|
||||||
"@radix-ui/react-radio-group": "1.3.7",
|
"@radix-ui/react-radio-group": "1.3.8",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-select": "2.2.5",
|
"@radix-ui/react-select": "2.2.6",
|
||||||
"@radix-ui/react-separator": "1.1.7",
|
"@radix-ui/react-separator": "1.1.7",
|
||||||
"@radix-ui/react-slot": "1.2.3",
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
"@radix-ui/react-switch": "1.2.5",
|
"@radix-ui/react-switch": "1.2.6",
|
||||||
"@radix-ui/react-tabs": "1.1.12",
|
"@radix-ui/react-tabs": "1.1.13",
|
||||||
"@radix-ui/react-toast": "1.2.14",
|
"@radix-ui/react-toast": "1.2.15",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@react-email/components": "0.5.0",
|
"@react-email/components": "0.5.0",
|
||||||
"@react-email/render": "^1.2.0",
|
"@react-email/render": "^1.2.0",
|
||||||
"@react-email/tailwind": "1.2.2",
|
"@react-email/tailwind": "1.2.2",
|
||||||
@@ -73,7 +72,7 @@
|
|||||||
"eslint": "9.33.0",
|
"eslint": "9.33.0",
|
||||||
"eslint-config-next": "15.4.6",
|
"eslint-config-next": "15.4.6",
|
||||||
"express": "5.1.0",
|
"express": "5.1.0",
|
||||||
"express-rate-limit": "7.5.1",
|
"express-rate-limit": "8.0.1",
|
||||||
"glob": "11.0.3",
|
"glob": "11.0.3",
|
||||||
"helmet": "8.1.0",
|
"helmet": "8.1.0",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
@@ -102,9 +101,10 @@
|
|||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"rebuild": "0.1.2",
|
"rebuild": "0.1.2",
|
||||||
"semver": "^7.7.2",
|
"semver": "^7.7.2",
|
||||||
|
"source-map-support": "0.5.21",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"tailwind-merge": "3.3.1",
|
"tailwind-merge": "3.3.1",
|
||||||
"tw-animate-css": "^1.3.6",
|
"tw-animate-css": "^1.3.7",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
"winston": "3.17.0",
|
"winston": "3.17.0",
|
||||||
@@ -115,9 +115,9 @@
|
|||||||
"zod-validation-error": "3.5.2"
|
"zod-validation-error": "3.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dotenvx/dotenvx": "1.48.4",
|
"@dotenvx/dotenvx": "1.49.0",
|
||||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.12",
|
||||||
"@types/better-sqlite3": "7.6.12",
|
"@types/better-sqlite3": "7.6.12",
|
||||||
"@types/cookie-parser": "1.4.9",
|
"@types/cookie-parser": "1.4.9",
|
||||||
"@types/cors": "2.8.19",
|
"@types/cors": "2.8.19",
|
||||||
@@ -130,8 +130,8 @@
|
|||||||
"@types/node": "^24",
|
"@types/node": "^24",
|
||||||
"@types/nodemailer": "6.4.17",
|
"@types/nodemailer": "6.4.17",
|
||||||
"@types/pg": "8.15.5",
|
"@types/pg": "8.15.5",
|
||||||
"@types/react": "19.1.10",
|
"@types/react": "19.1.12",
|
||||||
"@types/react-dom": "19.1.7",
|
"@types/react-dom": "19.1.9",
|
||||||
"@types/semver": "^7.7.0",
|
"@types/semver": "^7.7.0",
|
||||||
"@types/swagger-ui-express": "^4.1.8",
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
@@ -143,9 +143,9 @@
|
|||||||
"react-email": "4.2.8",
|
"react-email": "4.2.8",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
"tsx": "4.20.4",
|
"tsx": "4.20.5",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"typescript-eslint": "^8.39.1"
|
"typescript-eslint": "^8.40.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"emblor": {
|
"emblor": {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
|
|||||||
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
||||||
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
|
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
|
||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
import rateLimit from "express-rate-limit";
|
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "./types/HttpCode";
|
import HttpCode from "./types/HttpCode";
|
||||||
import requestTimeoutMiddleware from "./middlewares/requestTimeout";
|
import requestTimeoutMiddleware from "./middlewares/requestTimeout";
|
||||||
@@ -70,7 +70,7 @@ export function createApiServer() {
|
|||||||
60 *
|
60 *
|
||||||
1000,
|
1000,
|
||||||
max: config.getRawConfig().rate_limits.global.max_requests,
|
max: config.getRawConfig().rate_limits.global.max_requests,
|
||||||
keyGenerator: (req) => `apiServerGlobal:${req.ip}:${req.path}`,
|
keyGenerator: (req) => `apiServerGlobal:${ipKeyGenerator(req.ip || "")}:${req.path}`,
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `Rate limit exceeded. You can make ${config.getRawConfig().rate_limits.global.max_requests} requests every ${config.getRawConfig().rate_limits.global.window_minutes} minute(s).`;
|
const message = `Rate limit exceeded. You can make ${config.getRawConfig().rate_limits.global.max_requests} requests every ${config.getRawConfig().rate_limits.global.window_minutes} minute(s).`;
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export const SESSION_COOKIE_EXPIRES =
|
|||||||
60 *
|
60 *
|
||||||
60 *
|
60 *
|
||||||
config.getRawConfig().server.dashboard_session_length_hours;
|
config.getRawConfig().server.dashboard_session_length_hours;
|
||||||
export const COOKIE_DOMAIN =
|
export const COOKIE_DOMAIN = config.getRawConfig().app.dashboard_url ?
|
||||||
"." + new URL(config.getRawConfig().app.dashboard_url).hostname;
|
"." + new URL(config.getRawConfig().app.dashboard_url!).hostname : undefined;
|
||||||
|
|
||||||
export function generateSessionToken(): string {
|
export function generateSessionToken(): string {
|
||||||
const bytes = new Uint8Array(20);
|
const bytes = new Uint8Array(20);
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import { resourceSessions, ResourceSession } from "@server/db";
|
|||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
|
import axios from "axios";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { tokenManager } from "@server/lib/tokenManager";
|
||||||
|
|
||||||
export const SESSION_COOKIE_NAME =
|
export const SESSION_COOKIE_NAME =
|
||||||
config.getRawConfig().server.session_cookie_name;
|
config.getRawConfig().server.session_cookie_name;
|
||||||
@@ -62,6 +65,29 @@ export async function validateResourceSessionToken(
|
|||||||
token: string,
|
token: string,
|
||||||
resourceId: number
|
resourceId: number
|
||||||
): Promise<ResourceSessionValidationResult> {
|
): Promise<ResourceSessionValidationResult> {
|
||||||
|
if (config.isManagedMode()) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/session/validate`, {
|
||||||
|
token: token
|
||||||
|
}, await tokenManager.getAuthHeader());
|
||||||
|
return response.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
logger.error("Error validating resource session token in hybrid mode:", {
|
||||||
|
message: error.message,
|
||||||
|
code: error.code,
|
||||||
|
status: error.response?.status,
|
||||||
|
statusText: error.response?.statusText,
|
||||||
|
url: error.config?.url,
|
||||||
|
method: error.config?.method
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.error("Error validating resource session token in hybrid mode:", error);
|
||||||
|
}
|
||||||
|
return { resourceSession: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sessionId = encodeHexLowerCase(
|
const sessionId = encodeHexLowerCase(
|
||||||
sha256(new TextEncoder().encode(token))
|
sha256(new TextEncoder().encode(token))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -124,7 +124,10 @@ export const exitNodes = pgTable("exitNodes", {
|
|||||||
publicKey: varchar("publicKey").notNull(),
|
publicKey: varchar("publicKey").notNull(),
|
||||||
listenPort: integer("listenPort").notNull(),
|
listenPort: integer("listenPort").notNull(),
|
||||||
reachableAt: varchar("reachableAt"),
|
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
|
export const siteResources = pgTable("siteResources", { // this is for the clients
|
||||||
@@ -427,7 +430,7 @@ export const resourceRules = pgTable("resourceRules", {
|
|||||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
priority: integer("priority").notNull(),
|
priority: integer("priority").notNull(),
|
||||||
action: varchar("action").notNull(), // ACCEPT, DROP
|
action: varchar("action").notNull(), // ACCEPT, DROP, PASS
|
||||||
match: varchar("match").notNull(), // CIDR, PATH, IP
|
match: varchar("match").notNull(), // CIDR, PATH, IP
|
||||||
value: varchar("value").notNull()
|
value: varchar("value").notNull()
|
||||||
});
|
});
|
||||||
@@ -668,3 +671,4 @@ export type RoleClient = InferSelectModel<typeof roleClients>;
|
|||||||
export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
||||||
export type SiteResource = InferSelectModel<typeof siteResources>;
|
export type SiteResource = InferSelectModel<typeof siteResources>;
|
||||||
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
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;
|
||||||
|
}
|
||||||
@@ -136,7 +136,10 @@ export const exitNodes = sqliteTable("exitNodes", {
|
|||||||
publicKey: text("publicKey").notNull(),
|
publicKey: text("publicKey").notNull(),
|
||||||
listenPort: integer("listenPort").notNull(),
|
listenPort: integer("listenPort").notNull(),
|
||||||
reachableAt: text("reachableAt"), // this is the internal address of the gerbil http server for command control
|
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
|
export const siteResources = sqliteTable("siteResources", { // this is for the clients
|
||||||
@@ -567,7 +570,7 @@ export const resourceRules = sqliteTable("resourceRules", {
|
|||||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
priority: integer("priority").notNull(),
|
priority: integer("priority").notNull(),
|
||||||
action: text("action").notNull(), // ACCEPT, DROP
|
action: text("action").notNull(), // ACCEPT, DROP, PASS
|
||||||
match: text("match").notNull(), // CIDR, PATH, IP
|
match: text("match").notNull(), // CIDR, PATH, IP
|
||||||
value: text("value").notNull()
|
value: text("value").notNull()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import logger from "@server/logger";
|
|||||||
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||||
|
|
||||||
function createEmailClient() {
|
function createEmailClient() {
|
||||||
|
if (config.isManagedMode()) {
|
||||||
|
// LETS NOT WORRY ABOUT EMAILS IN HYBRID
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const emailConfig = config.getRawConfig().email;
|
const emailConfig = config.getRawConfig().email;
|
||||||
if (!emailConfig) {
|
if (!emailConfig) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|||||||
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,5 +1,6 @@
|
|||||||
#! /usr/bin/env node
|
#! /usr/bin/env node
|
||||||
import "./extendZod.ts";
|
import "./extendZod.ts";
|
||||||
|
import 'source-map-support/register.js'
|
||||||
|
|
||||||
import { runSetupFunctions } from "./setup";
|
import { runSetupFunctions } from "./setup";
|
||||||
import { createApiServer } from "./apiServer";
|
import { createApiServer } from "./apiServer";
|
||||||
@@ -7,9 +8,11 @@ import { createNextServer } from "./nextServer";
|
|||||||
import { createInternalServer } from "./internalServer";
|
import { createInternalServer } from "./internalServer";
|
||||||
import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "@server/db";
|
import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "@server/db";
|
||||||
import { createIntegrationApiServer } from "./integrationApiServer";
|
import { createIntegrationApiServer } from "./integrationApiServer";
|
||||||
|
import { createHybridClientServer } from "./hybridServer";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { setHostMeta } from "@server/lib/hostMeta";
|
import { setHostMeta } from "@server/lib/hostMeta";
|
||||||
import { initTelemetryClient } from "./lib/telemetry.js";
|
import { initTelemetryClient } from "./lib/telemetry.js";
|
||||||
|
import { TraefikConfigManager } from "./lib/traefikConfig.js";
|
||||||
|
|
||||||
async function startServers() {
|
async function startServers() {
|
||||||
await setHostMeta();
|
await setHostMeta();
|
||||||
@@ -22,7 +25,18 @@ async function startServers() {
|
|||||||
// Start all servers
|
// Start all servers
|
||||||
const apiServer = createApiServer();
|
const apiServer = createApiServer();
|
||||||
const internalServer = createInternalServer();
|
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;
|
let integrationServer;
|
||||||
if (config.getRawConfig().flags?.enable_integration_api) {
|
if (config.getRawConfig().flags?.enable_integration_api) {
|
||||||
@@ -33,7 +47,8 @@ async function startServers() {
|
|||||||
apiServer,
|
apiServer,
|
||||||
nextServer,
|
nextServer,
|
||||||
internalServer,
|
internalServer,
|
||||||
integrationServer
|
integrationServer,
|
||||||
|
hybridClientServer
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,16 +96,18 @@ export class Config {
|
|||||||
if (!this.rawConfig) {
|
if (!this.rawConfig) {
|
||||||
throw new Error("Config not loaded. Call load() first.");
|
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();
|
await this.checkKeyStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkKeyStatus() {
|
private async checkKeyStatus() {
|
||||||
const licenseStatus = await license.check();
|
const licenseStatus = await license.check();
|
||||||
if (
|
if (!licenseStatus.isHostLicensed) {
|
||||||
!licenseStatus.isHostLicensed
|
|
||||||
) {
|
|
||||||
this.checkSupporterKey();
|
this.checkSupporterKey();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,6 +149,10 @@ export class Config {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isManagedMode() {
|
||||||
|
return typeof this.rawConfig?.managed === "object";
|
||||||
|
}
|
||||||
|
|
||||||
public async checkSupporterKey() {
|
public async checkSupporterKey() {
|
||||||
const [key] = await db.select().from(supporterKey).limit(1);
|
const [key] = await db.select().from(supporterKey).limit(1);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "path";
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
// This is a placeholder value replaced by the build process
|
// This is a placeholder value replaced by the build process
|
||||||
export const APP_VERSION = "1.8.0";
|
export const APP_VERSION = "1.9.0";
|
||||||
|
|
||||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||||
export const __DIRNAME = path.dirname(__FILENAME);
|
export const __DIRNAME = path.dirname(__FILENAME);
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
32
server/lib/geoip.ts
Normal file
32
server/lib/geoip.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import config from "./config";
|
||||||
|
import { tokenManager } from "./tokenManager";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
|
export async function getCountryCodeForIp(
|
||||||
|
ip: string
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(
|
||||||
|
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/geoip/${ip}`,
|
||||||
|
await tokenManager.getAuthHeader()
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data.data.countryCode;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
logger.error("Error fetching config in verify session:", {
|
||||||
|
message: error.message,
|
||||||
|
code: error.code,
|
||||||
|
status: error.response?.status,
|
||||||
|
statusText: error.response?.statusText,
|
||||||
|
url: error.config?.url,
|
||||||
|
method: error.config?.method
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.error("Error fetching config in verify session:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
@@ -1 +1,3 @@
|
|||||||
export * from "./response";
|
export * from "./response";
|
||||||
|
export { tokenManager, TokenManager } from "./tokenManager";
|
||||||
|
export * from "./geoip";
|
||||||
|
|||||||
@@ -16,22 +16,38 @@ export const configSchema = z
|
|||||||
dashboard_url: z
|
dashboard_url: z
|
||||||
.string()
|
.string()
|
||||||
.url()
|
.url()
|
||||||
.optional()
|
|
||||||
.pipe(z.string().url())
|
.pipe(z.string().url())
|
||||||
.transform((url) => url.toLowerCase()),
|
.transform((url) => url.toLowerCase())
|
||||||
|
.optional(),
|
||||||
log_level: z
|
log_level: z
|
||||||
.enum(["debug", "info", "warn", "error"])
|
.enum(["debug", "info", "warn", "error"])
|
||||||
.optional()
|
.optional()
|
||||||
.default("info"),
|
.default("info"),
|
||||||
save_logs: z.boolean().optional().default(false),
|
save_logs: z.boolean().optional().default(false),
|
||||||
log_failed_attempts: z.boolean().optional().default(false),
|
log_failed_attempts: z.boolean().optional().default(false),
|
||||||
telmetry: z
|
telemetry: z
|
||||||
.object({
|
.object({
|
||||||
anonymous_usage: z.boolean().optional().default(true)
|
anonymous_usage: z.boolean().optional().default(true)
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({})
|
.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
|
domains: z
|
||||||
.record(
|
.record(
|
||||||
z.string(),
|
z.string(),
|
||||||
@@ -48,7 +64,7 @@ export const configSchema = z
|
|||||||
server: z.object({
|
server: z.object({
|
||||||
integration_port: portSchema
|
integration_port: portSchema
|
||||||
.optional()
|
.optional()
|
||||||
.default(3003)
|
.default(3004)
|
||||||
.transform(stoi)
|
.transform(stoi)
|
||||||
.pipe(portSchema.optional()),
|
.pipe(portSchema.optional()),
|
||||||
external_port: portSchema
|
external_port: portSchema
|
||||||
@@ -113,9 +129,24 @@ export const configSchema = z
|
|||||||
trust_proxy: z.number().int().gte(0).optional().default(1),
|
trust_proxy: z.number().int().gte(0).optional().default(1),
|
||||||
secret: z
|
secret: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
|
||||||
.transform(getEnvOrYaml("SERVER_SECRET"))
|
|
||||||
.pipe(z.string().min(8))
|
.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
|
postgres: z
|
||||||
.object({
|
.object({
|
||||||
@@ -135,7 +166,21 @@ export const configSchema = z
|
|||||||
https_entrypoint: z.string().optional().default("websecure"),
|
https_entrypoint: z.string().optional().default("websecure"),
|
||||||
additional_middlewares: z.array(z.string()).optional(),
|
additional_middlewares: z.array(z.string()).optional(),
|
||||||
cert_resolver: z.string().optional().default("letsencrypt"),
|
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"]),
|
||||||
|
allow_raw_resources: z.boolean().optional().default(true),
|
||||||
|
file_mode: z.boolean().optional().default(false)
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({}),
|
.default({}),
|
||||||
@@ -260,6 +305,10 @@ export const configSchema = z
|
|||||||
if (data.flags?.disable_config_managed_domains) {
|
if (data.flags?.disable_config_managed_domains) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// If hybrid is defined, domains are not required
|
||||||
|
if (data.managed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -268,6 +317,35 @@ export const configSchema = z
|
|||||||
{
|
{
|
||||||
message: "At least one domain must be defined"
|
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. If its not defined already then pull it from env
|
||||||
|
if (data.server?.secret === undefined) {
|
||||||
|
data.server.secret = process.env.SERVER_SECRET;
|
||||||
|
}
|
||||||
|
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() {
|
export function readConfigFile() {
|
||||||
|
|||||||
80
server/lib/remoteCertificates/certificates.ts
Normal file
80
server/lib/remoteCertificates/certificates.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { tokenManager } from "../tokenManager";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import config from "../config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get valid certificates for the specified domains
|
||||||
|
*/
|
||||||
|
export async function getValidCertificatesForDomainsHybrid(domains: Set<string>): Promise<
|
||||||
|
Array<{
|
||||||
|
id: number;
|
||||||
|
domain: string;
|
||||||
|
wildcard: boolean | null;
|
||||||
|
certFile: string | null;
|
||||||
|
keyFile: string | null;
|
||||||
|
expiresAt: 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;
|
||||||
|
wildcard: boolean | null;
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|||||||
export const subdomainSchema = z
|
export const subdomainSchema = z
|
||||||
.string()
|
.string()
|
||||||
.regex(
|
.regex(
|
||||||
/^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+$/,
|
/^(?!:\/\/)([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/,
|
||||||
"Invalid subdomain format"
|
"Invalid subdomain format"
|
||||||
)
|
)
|
||||||
.min(1, "Subdomain must be at least 1 character long")
|
.min(1, "Subdomain must be at least 1 character long")
|
||||||
@@ -12,7 +12,8 @@ export const subdomainSchema = z
|
|||||||
export const tlsNameSchema = z
|
export const tlsNameSchema = z
|
||||||
.string()
|
.string()
|
||||||
.regex(
|
.regex(
|
||||||
/^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+$|^$/,
|
/^(?!:\/\/)([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$|^$/,
|
||||||
"Invalid subdomain format"
|
"Invalid subdomain format"
|
||||||
)
|
)
|
||||||
.transform((val) => val.toLowerCase());
|
.transform((val) => val.toLowerCase());
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { eq, count, notInArray } from "drizzle-orm";
|
|||||||
import { APP_VERSION } from "./consts";
|
import { APP_VERSION } from "./consts";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { UserType } from "@server/types/UserTypes";
|
import { UserType } from "@server/types/UserTypes";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
class TelemetryClient {
|
class TelemetryClient {
|
||||||
private client: PostHog | null = null;
|
private client: PostHog | null = null;
|
||||||
@@ -15,11 +16,19 @@ class TelemetryClient {
|
|||||||
private intervalId: NodeJS.Timeout | null = null;
|
private intervalId: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const enabled = config.getRawConfig().app.telmetry.anonymous_usage;
|
const enabled = config.getRawConfig().app.telemetry.anonymous_usage;
|
||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
const dev = process.env.ENVIRONMENT !== "prod";
|
const dev = process.env.ENVIRONMENT !== "prod";
|
||||||
|
|
||||||
if (this.enabled && !dev) {
|
if (dev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (build !== "oss") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.enabled) {
|
||||||
this.client = new PostHog(
|
this.client = new PostHog(
|
||||||
"phc_QYuATSSZt6onzssWcYJbXLzQwnunIpdGGDTYhzK3VjX",
|
"phc_QYuATSSZt6onzssWcYJbXLzQwnunIpdGGDTYhzK3VjX",
|
||||||
{
|
{
|
||||||
@@ -40,7 +49,7 @@ class TelemetryClient {
|
|||||||
logger.info(
|
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"
|
"Pangolin now gathers anonymous usage data to help us better understand how the software is used and guide future improvements and feature development. You can find more details, including instructions for opting out of this anonymous data collection, at: https://docs.digpangolin.com/telemetry"
|
||||||
);
|
);
|
||||||
} else if (!this.enabled && !dev) {
|
} else if (!this.enabled) {
|
||||||
logger.info(
|
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"
|
"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"
|
||||||
);
|
);
|
||||||
|
|||||||
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();
|
||||||
235
server/lib/traefikConfig.test.ts
Normal file
235
server/lib/traefikConfig.test.ts
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import { assertEquals } from "@test/assert";
|
||||||
|
import { isDomainCoveredByWildcard } from "./traefikConfig";
|
||||||
|
|
||||||
|
function runTests() {
|
||||||
|
console.log('Running wildcard domain coverage tests...');
|
||||||
|
|
||||||
|
// Test case 1: Basic wildcard certificate at example.com
|
||||||
|
const basicWildcardCerts = new Map([
|
||||||
|
['example.com', { exists: true, wildcard: true }]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Should match first-level subdomains
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('level1.example.com', basicWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Wildcard cert at example.com should match level1.example.com'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('api.example.com', basicWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Wildcard cert at example.com should match api.example.com'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('www.example.com', basicWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Wildcard cert at example.com should match www.example.com'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should match the root domain (exact match)
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('example.com', basicWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Wildcard cert at example.com should match example.com itself'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should NOT match second-level subdomains
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('level2.level1.example.com', basicWildcardCerts),
|
||||||
|
false,
|
||||||
|
'Wildcard cert at example.com should NOT match level2.level1.example.com'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('deep.nested.subdomain.example.com', basicWildcardCerts),
|
||||||
|
false,
|
||||||
|
'Wildcard cert at example.com should NOT match deep.nested.subdomain.example.com'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should NOT match different domains
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('test.otherdomain.com', basicWildcardCerts),
|
||||||
|
false,
|
||||||
|
'Wildcard cert at example.com should NOT match test.otherdomain.com'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('notexample.com', basicWildcardCerts),
|
||||||
|
false,
|
||||||
|
'Wildcard cert at example.com should NOT match notexample.com'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test case 2: Multiple wildcard certificates
|
||||||
|
const multipleWildcardCerts = new Map([
|
||||||
|
['example.com', { exists: true, wildcard: true }],
|
||||||
|
['test.org', { exists: true, wildcard: true }],
|
||||||
|
['api.service.net', { exists: true, wildcard: true }]
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('app.example.com', multipleWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Should match subdomain of first wildcard cert'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('staging.test.org', multipleWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Should match subdomain of second wildcard cert'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('v1.api.service.net', multipleWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Should match subdomain of third wildcard cert'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('deep.nested.api.service.net', multipleWildcardCerts),
|
||||||
|
false,
|
||||||
|
'Should NOT match multi-level subdomain of third wildcard cert'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test exact domain matches for multiple certs
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('example.com', multipleWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Should match exact domain of first wildcard cert'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('test.org', multipleWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Should match exact domain of second wildcard cert'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('api.service.net', multipleWildcardCerts),
|
||||||
|
true,
|
||||||
|
'Should match exact domain of third wildcard cert'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test case 3: Non-wildcard certificates (should not match anything)
|
||||||
|
const nonWildcardCerts = new Map([
|
||||||
|
['example.com', { exists: true, wildcard: false }],
|
||||||
|
['specific.domain.com', { exists: true, wildcard: false }]
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('sub.example.com', nonWildcardCerts),
|
||||||
|
false,
|
||||||
|
'Non-wildcard cert should not match subdomains'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('example.com', nonWildcardCerts),
|
||||||
|
false,
|
||||||
|
'Non-wildcard cert should not match even exact domain via this function'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test case 4: Non-existent certificates (should not match)
|
||||||
|
const nonExistentCerts = new Map([
|
||||||
|
['example.com', { exists: false, wildcard: true }],
|
||||||
|
['missing.com', { exists: false, wildcard: true }]
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('sub.example.com', nonExistentCerts),
|
||||||
|
false,
|
||||||
|
'Non-existent wildcard cert should not match'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test case 5: Edge cases with special domain names
|
||||||
|
const specialDomainCerts = new Map([
|
||||||
|
['localhost', { exists: true, wildcard: true }],
|
||||||
|
['127-0-0-1.nip.io', { exists: true, wildcard: true }],
|
||||||
|
['xn--e1afmkfd.xn--p1ai', { exists: true, wildcard: true }] // IDN domain
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('app.localhost', specialDomainCerts),
|
||||||
|
true,
|
||||||
|
'Should match subdomain of localhost wildcard'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('test.127-0-0-1.nip.io', specialDomainCerts),
|
||||||
|
true,
|
||||||
|
'Should match subdomain of nip.io wildcard'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('sub.xn--e1afmkfd.xn--p1ai', specialDomainCerts),
|
||||||
|
true,
|
||||||
|
'Should match subdomain of IDN wildcard'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test case 6: Empty input and edge cases
|
||||||
|
const emptyCerts = new Map();
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('any.domain.com', emptyCerts),
|
||||||
|
false,
|
||||||
|
'Empty certificate map should not match any domain'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test case 7: Domains with single character components
|
||||||
|
const singleCharCerts = new Map([
|
||||||
|
['a.com', { exists: true, wildcard: true }],
|
||||||
|
['x.y.z', { exists: true, wildcard: true }]
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('b.a.com', singleCharCerts),
|
||||||
|
true,
|
||||||
|
'Should match single character subdomain'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('w.x.y.z', singleCharCerts),
|
||||||
|
true,
|
||||||
|
'Should match single character subdomain of multi-part domain'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('v.w.x.y.z', singleCharCerts),
|
||||||
|
false,
|
||||||
|
'Should NOT match multi-level subdomain of single char domain'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test case 8: Domains with numbers and hyphens
|
||||||
|
const numericCerts = new Map([
|
||||||
|
['api-v2.service-1.com', { exists: true, wildcard: true }],
|
||||||
|
['123.456.net', { exists: true, wildcard: true }]
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('staging.api-v2.service-1.com', numericCerts),
|
||||||
|
true,
|
||||||
|
'Should match subdomain with hyphens and numbers'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('test.123.456.net', numericCerts),
|
||||||
|
true,
|
||||||
|
'Should match subdomain with numeric components'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isDomainCoveredByWildcard('deep.staging.api-v2.service-1.com', numericCerts),
|
||||||
|
false,
|
||||||
|
'Should NOT match multi-level subdomain with hyphens and numbers'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('All wildcard domain coverage tests passed!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run all tests
|
||||||
|
try {
|
||||||
|
runTests();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Test failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
1061
server/lib/traefikConfig.ts
Normal file
1061
server/lib/traefikConfig.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -11,3 +11,4 @@ export * from "./verifyAccessTokenAccess";
|
|||||||
export * from "./verifyApiKeyIsRoot";
|
export * from "./verifyApiKeyIsRoot";
|
||||||
export * from "./verifyApiKeyApiKeyAccess";
|
export * from "./verifyApiKeyApiKeyAccess";
|
||||||
export * from "./verifyApiKeyClientAccess";
|
export * from "./verifyApiKeyClientAccess";
|
||||||
|
export * from "./verifyApiKeySiteResourceAccess";
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { siteResources, apiKeyOrg } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
|
||||||
|
export async function verifyApiKeySiteResourceAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const apiKey = req.apiKey;
|
||||||
|
const siteResourceId = parseInt(req.params.siteResourceId);
|
||||||
|
const siteId = parseInt(req.params.siteId);
|
||||||
|
const orgId = req.params.orgId;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!siteResourceId || !siteId || !orgId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Missing required parameters"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
// Root keys can access any resource in any org
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the API key has access to the organization
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
const apiKeyOrgRes = await db
|
||||||
|
.select()
|
||||||
|
.from(apiKeyOrg)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||||
|
eq(apiKeyOrg.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (apiKeyOrgRes.length === 0) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Key does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.apiKeyOrg = apiKeyOrgRes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach the siteResource to the request for use in the next middleware/route
|
||||||
|
// @ts-ignore - Extending Request type
|
||||||
|
req.siteResource = siteResource;
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error verifying site resource access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ export async function verifyRoleAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { roleIds } = req.body;
|
const roleIds = req.body?.roleIds;
|
||||||
const allRoleIds = roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]);
|
const allRoleIds = roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]);
|
||||||
|
|
||||||
if (allRoleIds.length === 0) {
|
if (allRoleIds.length === 0) {
|
||||||
|
|||||||
@@ -36,16 +36,16 @@ import { verifyTotpCode } from "@server/auth/totp";
|
|||||||
|
|
||||||
// The RP ID is the domain name of your application
|
// The RP ID is the domain name of your application
|
||||||
const rpID = (() => {
|
const rpID = (() => {
|
||||||
const url = new URL(config.getRawConfig().app.dashboard_url);
|
const url = config.getRawConfig().app.dashboard_url ? new URL(config.getRawConfig().app.dashboard_url!) : undefined;
|
||||||
// For localhost, we must use 'localhost' without port
|
// For localhost, we must use 'localhost' without port
|
||||||
if (url.hostname === 'localhost') {
|
if (url?.hostname === 'localhost' || !url) {
|
||||||
return 'localhost';
|
return 'localhost';
|
||||||
}
|
}
|
||||||
return url.hostname;
|
return url.hostname;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const rpName = "Pangolin";
|
const rpName = "Pangolin";
|
||||||
const origin = config.getRawConfig().app.dashboard_url;
|
const origin = config.getRawConfig().app.dashboard_url || "localhost";
|
||||||
|
|
||||||
// Database-based challenge storage (replaces in-memory storage)
|
// Database-based challenge storage (replaces in-memory storage)
|
||||||
// Challenges are stored in the webauthnChallenge table with automatic expiration
|
// Challenges are stored in the webauthnChallenge table with automatic expiration
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export async function exchangeSession(
|
|||||||
let cleanHost = host;
|
let cleanHost = host;
|
||||||
// if the host ends with :port
|
// if the host ends with :port
|
||||||
if (cleanHost.match(/:[0-9]{1,5}$/)) {
|
if (cleanHost.match(/:[0-9]{1,5}$/)) {
|
||||||
let matched = ''+cleanHost.match(/:[0-9]{1,5}$/);
|
const matched = ''+cleanHost.match(/:[0-9]{1,5}$/);
|
||||||
cleanHost = cleanHost.slice(0, -1*matched.length);
|
cleanHost = cleanHost.slice(0, -1*matched.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,21 +5,21 @@ import {
|
|||||||
validateResourceSessionToken
|
validateResourceSessionToken
|
||||||
} from "@server/auth/sessions/resource";
|
} from "@server/auth/sessions/resource";
|
||||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||||
import { db } from "@server/db";
|
import {
|
||||||
|
getResourceByDomain,
|
||||||
|
getUserSessionWithUser,
|
||||||
|
getUserOrgRole,
|
||||||
|
getRoleResourceAccess,
|
||||||
|
getUserResourceAccess,
|
||||||
|
getResourceRules
|
||||||
|
} from "@server/db/queries/verifySessionQueries";
|
||||||
import {
|
import {
|
||||||
Resource,
|
Resource,
|
||||||
ResourceAccessToken,
|
ResourceAccessToken,
|
||||||
ResourcePassword,
|
ResourcePassword,
|
||||||
resourcePassword,
|
|
||||||
ResourcePincode,
|
ResourcePincode,
|
||||||
resourcePincode,
|
|
||||||
ResourceRule,
|
ResourceRule,
|
||||||
resourceRules,
|
|
||||||
resources,
|
|
||||||
roleResources,
|
|
||||||
sessions,
|
sessions,
|
||||||
userOrgs,
|
|
||||||
userResources,
|
|
||||||
users
|
users
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
@@ -27,12 +27,12 @@ import { isIpInCidr } from "@server/lib/ip";
|
|||||||
import { response } from "@server/lib/response";
|
import { response } from "@server/lib/response";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { and, eq } from "drizzle-orm";
|
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import NodeCache from "node-cache";
|
import NodeCache from "node-cache";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { getCountryCodeForIp } from "@server/lib";
|
||||||
|
|
||||||
// We'll see if this speeds anything up
|
// We'll see if this speeds anything up
|
||||||
const cache = new NodeCache({
|
const cache = new NodeCache({
|
||||||
@@ -123,8 +123,8 @@ export async function verifyResourceSession(
|
|||||||
let cleanHost = host;
|
let cleanHost = host;
|
||||||
// if the host ends with :port, strip it
|
// if the host ends with :port, strip it
|
||||||
if (cleanHost.match(/:[0-9]{1,5}$/)) {
|
if (cleanHost.match(/:[0-9]{1,5}$/)) {
|
||||||
let matched = ''+cleanHost.match(/:[0-9]{1,5}$/);
|
const matched = "" + cleanHost.match(/:[0-9]{1,5}$/);
|
||||||
cleanHost = cleanHost.slice(0, -1*matched.length);
|
cleanHost = cleanHost.slice(0, -1 * matched.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceCacheKey = `resource:${cleanHost}`;
|
const resourceCacheKey = `resource:${cleanHost}`;
|
||||||
@@ -137,38 +137,21 @@ export async function verifyResourceSession(
|
|||||||
| undefined = cache.get(resourceCacheKey);
|
| undefined = cache.get(resourceCacheKey);
|
||||||
|
|
||||||
if (!resourceData) {
|
if (!resourceData) {
|
||||||
const [result] = await db
|
const result = await getResourceByDomain(cleanHost);
|
||||||
.select()
|
|
||||||
.from(resources)
|
|
||||||
.leftJoin(
|
|
||||||
resourcePincode,
|
|
||||||
eq(resourcePincode.resourceId, resources.resourceId)
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
resourcePassword,
|
|
||||||
eq(resourcePassword.resourceId, resources.resourceId)
|
|
||||||
)
|
|
||||||
.where(eq(resources.fullDomain, cleanHost))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
logger.debug("Resource not found", cleanHost);
|
logger.debug(`Resource not found ${cleanHost}`);
|
||||||
return notAllowed(res);
|
return notAllowed(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceData = {
|
resourceData = result;
|
||||||
resource: result.resources,
|
|
||||||
pincode: result.resourcePincode,
|
|
||||||
password: result.resourcePassword
|
|
||||||
};
|
|
||||||
|
|
||||||
cache.set(resourceCacheKey, resourceData);
|
cache.set(resourceCacheKey, resourceData);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resource, pincode, password } = resourceData;
|
const { resource, pincode, password } = resourceData;
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
logger.debug("Resource not found", cleanHost);
|
logger.debug(`Resource not found ${cleanHost}`);
|
||||||
return notAllowed(res);
|
return notAllowed(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +176,11 @@ export async function verifyResourceSession(
|
|||||||
} else if (action == "DROP") {
|
} else if (action == "DROP") {
|
||||||
logger.debug("Resource denied by rule");
|
logger.debug("Resource denied by rule");
|
||||||
return notAllowed(res);
|
return notAllowed(res);
|
||||||
|
} else if (action == "PASS") {
|
||||||
|
logger.debug(
|
||||||
|
"Resource passed by rule, continuing to auth checks"
|
||||||
|
);
|
||||||
|
// Continue to authentication checks below
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise its undefined and we pass
|
// otherwise its undefined and we pass
|
||||||
@@ -208,7 +196,16 @@ export async function verifyResourceSession(
|
|||||||
return allowed(res);
|
return allowed(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectUrl = `${config.getRawConfig().app.dashboard_url}/auth/resource/${encodeURIComponent(
|
let endpoint: string;
|
||||||
|
if (config.isManagedMode()) {
|
||||||
|
endpoint =
|
||||||
|
config.getRawConfig().managed?.redirect_endpoint ||
|
||||||
|
config.getRawConfig().managed?.endpoint ||
|
||||||
|
"";
|
||||||
|
} else {
|
||||||
|
endpoint = config.getRawConfig().app.dashboard_url!;
|
||||||
|
}
|
||||||
|
const redirectUrl = `${endpoint}/auth/resource/${encodeURIComponent(
|
||||||
resource.resourceId
|
resource.resourceId
|
||||||
)}?redirect=${encodeURIComponent(originalRequestURL)}`;
|
)}?redirect=${encodeURIComponent(originalRequestURL)}`;
|
||||||
|
|
||||||
@@ -529,14 +526,13 @@ async function isUserAllowedToAccessResource(
|
|||||||
userSessionId: string,
|
userSessionId: string,
|
||||||
resource: Resource
|
resource: Resource
|
||||||
): Promise<BasicUserData | null> {
|
): Promise<BasicUserData | null> {
|
||||||
const [res] = await db
|
const result = await getUserSessionWithUser(userSessionId);
|
||||||
.select()
|
|
||||||
.from(sessions)
|
|
||||||
.leftJoin(users, eq(users.userId, sessions.userId))
|
|
||||||
.where(eq(sessions.sessionId, userSessionId));
|
|
||||||
|
|
||||||
const user = res.user;
|
if (!result) {
|
||||||
const session = res.session;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user, session } = result;
|
||||||
|
|
||||||
if (!user || !session) {
|
if (!user || !session) {
|
||||||
return null;
|
return null;
|
||||||
@@ -549,33 +545,18 @@ async function isUserAllowedToAccessResource(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userOrgRole = await db
|
const userOrgRole = await getUserOrgRole(user.userId, resource.orgId);
|
||||||
.select()
|
|
||||||
.from(userOrgs)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(userOrgs.userId, user.userId),
|
|
||||||
eq(userOrgs.orgId, resource.orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (userOrgRole.length === 0) {
|
if (!userOrgRole) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleResourceAccess = await db
|
const roleResourceAccess = await getRoleResourceAccess(
|
||||||
.select()
|
resource.resourceId,
|
||||||
.from(roleResources)
|
userOrgRole.roleId
|
||||||
.where(
|
);
|
||||||
and(
|
|
||||||
eq(roleResources.resourceId, resource.resourceId),
|
|
||||||
eq(roleResources.roleId, userOrgRole[0].roleId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (roleResourceAccess.length > 0) {
|
if (roleResourceAccess) {
|
||||||
return {
|
return {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
@@ -583,18 +564,12 @@ async function isUserAllowedToAccessResource(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const userResourceAccess = await db
|
const userResourceAccess = await getUserResourceAccess(
|
||||||
.select()
|
user.userId,
|
||||||
.from(userResources)
|
resource.resourceId
|
||||||
.where(
|
);
|
||||||
and(
|
|
||||||
eq(userResources.userId, user.userId),
|
|
||||||
eq(userResources.resourceId, resource.resourceId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (userResourceAccess.length > 0) {
|
if (userResourceAccess) {
|
||||||
return {
|
return {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
@@ -609,17 +584,13 @@ async function checkRules(
|
|||||||
resourceId: number,
|
resourceId: number,
|
||||||
clientIp: string | undefined,
|
clientIp: string | undefined,
|
||||||
path: string | undefined
|
path: string | undefined
|
||||||
): Promise<"ACCEPT" | "DROP" | undefined> {
|
): Promise<"ACCEPT" | "DROP" | "PASS" | undefined> {
|
||||||
const ruleCacheKey = `rules:${resourceId}`;
|
const ruleCacheKey = `rules:${resourceId}`;
|
||||||
|
|
||||||
let rules: ResourceRule[] | undefined = cache.get(ruleCacheKey);
|
let rules: ResourceRule[] | undefined = cache.get(ruleCacheKey);
|
||||||
|
|
||||||
if (!rules) {
|
if (!rules) {
|
||||||
rules = await db
|
rules = await getResourceRules(resourceId);
|
||||||
.select()
|
|
||||||
.from(resourceRules)
|
|
||||||
.where(eq(resourceRules.resourceId, resourceId));
|
|
||||||
|
|
||||||
cache.set(ruleCacheKey, rules);
|
cache.set(ruleCacheKey, rules);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,6 +621,12 @@ async function checkRules(
|
|||||||
isPathAllowed(rule.value, path)
|
isPathAllowed(rule.value, path)
|
||||||
) {
|
) {
|
||||||
return rule.action as any;
|
return rule.action as any;
|
||||||
|
} else if (
|
||||||
|
clientIp &&
|
||||||
|
rule.match == "GEOIP" &&
|
||||||
|
(await isIpInGeoIP(clientIp, rule.value))
|
||||||
|
) {
|
||||||
|
return rule.action as any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -774,3 +751,23 @@ export function isPathAllowed(pattern: string, path: string): boolean {
|
|||||||
logger.debug(`Final result: ${result}`);
|
logger.debug(`Final result: ${result}`);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function isIpInGeoIP(ip: string, countryCode: string): Promise<boolean> {
|
||||||
|
if (countryCode == "ALL") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const geoIpCacheKey = `geoip:${ip}`;
|
||||||
|
|
||||||
|
let cachedCountryCode: string | undefined = cache.get(geoIpCacheKey);
|
||||||
|
|
||||||
|
if (!cachedCountryCode) {
|
||||||
|
cachedCountryCode = await getCountryCodeForIp(ip);
|
||||||
|
// Cache for longer since IP geolocation doesn't change frequently
|
||||||
|
cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`IP ${ip} is in country: ${cachedCountryCode}`);
|
||||||
|
|
||||||
|
return cachedCountryCode?.toUpperCase() === countryCode.toUpperCase();
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { hashPassword } from "@server/auth/password";
|
|||||||
import { isValidCIDR, isValidIP } from "@server/lib/validators";
|
import { isValidCIDR, isValidIP } from "@server/lib/validators";
|
||||||
import { isIpInCidr } from "@server/lib/ip";
|
import { isIpInCidr } from "@server/lib/ip";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { listExitNodes } from "@server/lib/exitNodes";
|
||||||
|
|
||||||
const createClientParamsSchema = z
|
const createClientParamsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -177,20 +178,9 @@ export async function createClient(
|
|||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
// TODO: more intelligent way to pick the exit node
|
// TODO: more intelligent way to pick the exit node
|
||||||
|
const exitNodesList = await listExitNodes(orgId);
|
||||||
// make sure there is an exit node by counting the exit nodes table
|
const randomExitNode =
|
||||||
const nodes = await db.select().from(exitNodes);
|
exitNodesList[Math.floor(Math.random() * exitNodesList.length)];
|
||||||
if (nodes.length === 0) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
"No exit nodes available"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the first exit node
|
|
||||||
const exitNode = nodes[0];
|
|
||||||
|
|
||||||
const adminRole = await trx
|
const adminRole = await trx
|
||||||
.select()
|
.select()
|
||||||
@@ -208,7 +198,7 @@ export async function createClient(
|
|||||||
const [newClient] = await trx
|
const [newClient] = await trx
|
||||||
.insert(clients)
|
.insert(clients)
|
||||||
.values({
|
.values({
|
||||||
exitNodeId: exitNode.exitNodeId,
|
exitNodeId: randomExitNode.exitNodeId,
|
||||||
orgId,
|
orgId,
|
||||||
name,
|
name,
|
||||||
subnet: updatedSubnet,
|
subnet: updatedSubnet,
|
||||||
|
|||||||
@@ -13,17 +13,16 @@ import { OpenAPITags, registry } from "@server/openApi";
|
|||||||
|
|
||||||
const getClientSchema = z
|
const getClientSchema = z
|
||||||
.object({
|
.object({
|
||||||
clientId: z.string().transform(stoi).pipe(z.number().int().positive()),
|
clientId: z.string().transform(stoi).pipe(z.number().int().positive())
|
||||||
orgId: z.string()
|
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
async function query(clientId: number, orgId: string) {
|
async function query(clientId: number) {
|
||||||
// Get the client
|
// Get the client
|
||||||
const [client] = await db
|
const [client] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(clients)
|
.from(clients)
|
||||||
.where(and(eq(clients.clientId, clientId), eq(clients.orgId, orgId)))
|
.where(and(eq(clients.clientId, clientId)))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
@@ -47,9 +46,9 @@ export type GetClientResponse = NonNullable<Awaited<ReturnType<typeof query>>>;
|
|||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/org/{orgId}/client/{clientId}",
|
path: "/client/{clientId}",
|
||||||
description: "Get a client by its client ID.",
|
description: "Get a client by its client ID.",
|
||||||
tags: [OpenAPITags.Client, OpenAPITags.Org],
|
tags: [OpenAPITags.Client],
|
||||||
request: {
|
request: {
|
||||||
params: getClientSchema
|
params: getClientSchema
|
||||||
},
|
},
|
||||||
@@ -75,9 +74,9 @@ export async function getClient(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { clientId, orgId } = parsedParams.data;
|
const { clientId } = parsedParams.data;
|
||||||
|
|
||||||
const client = await query(clientId, orgId);
|
const client = await query(clientId);
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@@ -5,11 +5,9 @@ export async function addTargets(
|
|||||||
destinationIp: string,
|
destinationIp: string,
|
||||||
destinationPort: number,
|
destinationPort: number,
|
||||||
protocol: string,
|
protocol: string,
|
||||||
port: number | null = null
|
port: number
|
||||||
) {
|
) {
|
||||||
const target = `${port ? port + ":" : ""}${
|
const target = `${port}:${destinationIp}:${destinationPort}`;
|
||||||
destinationIp
|
|
||||||
}:${destinationPort}`;
|
|
||||||
|
|
||||||
await sendToClient(newtId, {
|
await sendToClient(newtId, {
|
||||||
type: `newt/wg/${protocol}/add`,
|
type: `newt/wg/${protocol}/add`,
|
||||||
@@ -24,11 +22,9 @@ export async function removeTargets(
|
|||||||
destinationIp: string,
|
destinationIp: string,
|
||||||
destinationPort: number,
|
destinationPort: number,
|
||||||
protocol: string,
|
protocol: string,
|
||||||
port: number | null = null
|
port: number
|
||||||
) {
|
) {
|
||||||
const target = `${port ? port + ":" : ""}${
|
const target = `${port}:${destinationIp}:${destinationPort}`;
|
||||||
destinationIp
|
|
||||||
}:${destinationPort}`;
|
|
||||||
|
|
||||||
await sendToClient(newtId, {
|
await sendToClient(newtId, {
|
||||||
type: `newt/wg/${protocol}/remove`,
|
type: `newt/wg/${protocol}/remove`,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
addPeer as olmAddPeer,
|
addPeer as olmAddPeer,
|
||||||
deletePeer as olmDeletePeer
|
deletePeer as olmDeletePeer
|
||||||
} from "../olm/peers";
|
} from "../olm/peers";
|
||||||
import axios from "axios";
|
import { sendToExitNode } from "../../lib/exitNodeComms";
|
||||||
|
|
||||||
const updateClientParamsSchema = z
|
const updateClientParamsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -141,13 +141,15 @@ export async function updateClient(
|
|||||||
const isRelayed = true;
|
const isRelayed = true;
|
||||||
|
|
||||||
// get the clientsite
|
// get the clientsite
|
||||||
const [clientSite] = await db
|
const [clientSite] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(clientSites)
|
.from(clientSites)
|
||||||
.where(and(
|
.where(
|
||||||
eq(clientSites.clientId, client.clientId),
|
and(
|
||||||
eq(clientSites.siteId, siteId)
|
eq(clientSites.clientId, client.clientId),
|
||||||
))
|
eq(clientSites.siteId, siteId)
|
||||||
|
)
|
||||||
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!clientSite || !clientSite.endpoint) {
|
if (!clientSite || !clientSite.endpoint) {
|
||||||
@@ -270,114 +272,102 @@ export async function updateClient(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// get all sites for this client and join with exit nodes with site.exitNodeId
|
// get all sites for this client and join with exit nodes with site.exitNodeId
|
||||||
const sitesData = await db
|
const sitesData = await db
|
||||||
.select()
|
.select()
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.innerJoin(
|
.innerJoin(clientSites, eq(sites.siteId, clientSites.siteId))
|
||||||
clientSites,
|
.leftJoin(exitNodes, eq(sites.exitNodeId, exitNodes.exitNodeId))
|
||||||
eq(sites.siteId, clientSites.siteId)
|
.where(eq(clientSites.clientId, client.clientId));
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
exitNodes,
|
|
||||||
eq(sites.exitNodeId, exitNodes.exitNodeId)
|
|
||||||
)
|
|
||||||
.where(eq(clientSites.clientId, client.clientId));
|
|
||||||
|
|
||||||
let exitNodeDestinations: {
|
let exitNodeDestinations: {
|
||||||
reachableAt: string;
|
reachableAt: string;
|
||||||
sourceIp: string;
|
exitNodeId: number;
|
||||||
sourcePort: number;
|
type: string;
|
||||||
destinations: PeerDestination[];
|
sourceIp: string;
|
||||||
}[] = [];
|
sourcePort: number;
|
||||||
|
destinations: PeerDestination[];
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
for (const site of sitesData) {
|
for (const site of sitesData) {
|
||||||
if (!site.sites.subnet) {
|
if (!site.sites.subnet) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Site ${site.sites.siteId} has no subnet, skipping`
|
`Site ${site.sites.siteId} has no subnet, skipping`
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!site.clientSites.endpoint) {
|
|
||||||
logger.warn(
|
|
||||||
`Site ${site.sites.siteId} has no endpoint, skipping`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// find the destinations in the array
|
|
||||||
let destinations = exitNodeDestinations.find(
|
|
||||||
(d) => d.reachableAt === site.exitNodes?.reachableAt
|
|
||||||
);
|
);
|
||||||
|
continue;
|
||||||
if (!destinations) {
|
|
||||||
destinations = {
|
|
||||||
reachableAt: site.exitNodes?.reachableAt || "",
|
|
||||||
sourceIp: site.clientSites.endpoint.split(":")[0] || "",
|
|
||||||
sourcePort: parseInt(site.clientSites.endpoint.split(":")[1]) || 0,
|
|
||||||
destinations: [
|
|
||||||
{
|
|
||||||
destinationIP:
|
|
||||||
site.sites.subnet.split("/")[0],
|
|
||||||
destinationPort: site.sites.listenPort || 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// add to the existing destinations
|
|
||||||
destinations.destinations.push({
|
|
||||||
destinationIP: site.sites.subnet.split("/")[0],
|
|
||||||
destinationPort: site.sites.listenPort || 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// update it in the array
|
|
||||||
exitNodeDestinations = exitNodeDestinations.filter(
|
|
||||||
(d) => d.reachableAt !== site.exitNodes?.reachableAt
|
|
||||||
);
|
|
||||||
exitNodeDestinations.push(destinations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const destination of exitNodeDestinations) {
|
if (!site.clientSites.endpoint) {
|
||||||
try {
|
logger.warn(
|
||||||
logger.info(
|
`Site ${site.sites.siteId} has no endpoint, skipping`
|
||||||
`Updating destinations for exit node at ${destination.reachableAt}`
|
);
|
||||||
);
|
continue;
|
||||||
const payload = {
|
}
|
||||||
sourceIp: destination.sourceIp,
|
|
||||||
sourcePort: destination.sourcePort,
|
// find the destinations in the array
|
||||||
destinations: destination.destinations
|
let destinations = exitNodeDestinations.find(
|
||||||
};
|
(d) => d.reachableAt === site.exitNodes?.reachableAt
|
||||||
logger.info(
|
);
|
||||||
`Payload for update-destinations: ${JSON.stringify(payload, null, 2)}`
|
|
||||||
);
|
if (!destinations) {
|
||||||
const response = await axios.post(
|
destinations = {
|
||||||
`${destination.reachableAt}/update-destinations`,
|
reachableAt: site.exitNodes?.reachableAt || "",
|
||||||
payload,
|
exitNodeId: site.exitNodes?.exitNodeId || 0,
|
||||||
|
type: site.exitNodes?.type || "",
|
||||||
|
sourceIp: site.clientSites.endpoint.split(":")[0] || "",
|
||||||
|
sourcePort:
|
||||||
|
parseInt(site.clientSites.endpoint.split(":")[1]) ||
|
||||||
|
0,
|
||||||
|
destinations: [
|
||||||
{
|
{
|
||||||
headers: {
|
destinationIP: site.sites.subnet.split("/")[0],
|
||||||
"Content-Type": "application/json"
|
destinationPort: site.sites.listenPort || 0
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
]
|
||||||
|
};
|
||||||
logger.info("Destinations updated:", {
|
} else {
|
||||||
peer: response.data.status
|
// add to the existing destinations
|
||||||
});
|
destinations.destinations.push({
|
||||||
} catch (error) {
|
destinationIP: site.sites.subnet.split("/")[0],
|
||||||
if (axios.isAxiosError(error)) {
|
destinationPort: site.sites.listenPort || 0
|
||||||
logger.error(
|
});
|
||||||
`Error updating destinations (can Pangolin see Gerbil HTTP API?) for exit node at ${destination.reachableAt} (status: ${error.response?.status}): ${JSON.stringify(error.response?.data, null, 2)}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.error(
|
|
||||||
`Error updating destinations for exit node at ${destination.reachableAt}: ${error}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update it in the array
|
||||||
|
exitNodeDestinations = exitNodeDestinations.filter(
|
||||||
|
(d) => d.reachableAt !== site.exitNodes?.reachableAt
|
||||||
|
);
|
||||||
|
exitNodeDestinations.push(destinations);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const destination of exitNodeDestinations) {
|
||||||
|
logger.info(
|
||||||
|
`Updating destinations for exit node at ${destination.reachableAt}`
|
||||||
|
);
|
||||||
|
const payload = {
|
||||||
|
sourceIp: destination.sourceIp,
|
||||||
|
sourcePort: destination.sourcePort,
|
||||||
|
destinations: destination.destinations
|
||||||
|
};
|
||||||
|
logger.info(
|
||||||
|
`Payload for update-destinations: ${JSON.stringify(payload, null, 2)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create an ExitNode-like object for sendToExitNode
|
||||||
|
const exitNodeForComm = {
|
||||||
|
exitNodeId: destination.exitNodeId,
|
||||||
|
type: destination.type,
|
||||||
|
reachableAt: destination.reachableAt
|
||||||
|
} as any; // Using 'as any' since we know sendToExitNode will handle this correctly
|
||||||
|
|
||||||
|
await sendToExitNode(exitNodeForComm, {
|
||||||
|
remoteType: "remoteExitNode/update-destinations",
|
||||||
|
localPath: "/update-destinations",
|
||||||
|
method: "POST",
|
||||||
|
data: payload
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch the updated client
|
// Fetch the updated client
|
||||||
const [updatedClient] = await trx
|
const [updatedClient] = await trx
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import { createStore } from "@server/lib/rateLimitStore";
|
|||||||
import { ActionsEnum } from "@server/auth/actions";
|
import { ActionsEnum } from "@server/auth/actions";
|
||||||
import { createNewt, getNewtToken } from "./newt";
|
import { createNewt, getNewtToken } from "./newt";
|
||||||
import { getOlmToken } from "./olm";
|
import { getOlmToken } from "./olm";
|
||||||
import rateLimit from "express-rate-limit";
|
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
@@ -78,12 +78,16 @@ authenticated.post(
|
|||||||
verifyUserHasAction(ActionsEnum.updateOrg),
|
verifyUserHasAction(ActionsEnum.updateOrg),
|
||||||
org.updateOrg
|
org.updateOrg
|
||||||
);
|
);
|
||||||
authenticated.delete(
|
|
||||||
"/org/:orgId",
|
if (build !== "saas") {
|
||||||
verifyOrgAccess,
|
authenticated.delete(
|
||||||
verifyUserIsOrgOwner,
|
"/org/:orgId",
|
||||||
org.deleteOrg
|
verifyOrgAccess,
|
||||||
);
|
verifyUserIsOrgOwner,
|
||||||
|
verifyUserHasAction(ActionsEnum.deleteOrg),
|
||||||
|
org.deleteOrg
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
authenticated.put(
|
authenticated.put(
|
||||||
"/org/:orgId/site",
|
"/org/:orgId/site",
|
||||||
@@ -134,9 +138,9 @@ authenticated.get(
|
|||||||
);
|
);
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
"/org/:orgId/client/:clientId",
|
"/client/:clientId",
|
||||||
verifyClientsEnabled,
|
verifyClientsEnabled,
|
||||||
verifyOrgAccess,
|
verifyClientAccess,
|
||||||
verifyUserHasAction(ActionsEnum.getClient),
|
verifyUserHasAction(ActionsEnum.getClient),
|
||||||
client.getClient
|
client.getClient
|
||||||
);
|
);
|
||||||
@@ -815,7 +819,7 @@ authRouter.use(
|
|||||||
rateLimit({
|
rateLimit({
|
||||||
windowMs: config.getRawConfig().rate_limits.auth.window_minutes,
|
windowMs: config.getRawConfig().rate_limits.auth.window_minutes,
|
||||||
max: config.getRawConfig().rate_limits.auth.max_requests,
|
max: config.getRawConfig().rate_limits.auth.max_requests,
|
||||||
keyGenerator: (req) => `authRouterGlobal:${req.ip}:${req.path}`,
|
keyGenerator: (req) => `authRouterGlobal:${ipKeyGenerator(req.ip || "")}:${req.path}`,
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `Rate limit exceeded. You can make ${config.getRawConfig().rate_limits.auth.max_requests} requests every ${config.getRawConfig().rate_limits.auth.window_minutes} minute(s).`;
|
const message = `Rate limit exceeded. You can make ${config.getRawConfig().rate_limits.auth.max_requests} requests every ${config.getRawConfig().rate_limits.auth.window_minutes} minute(s).`;
|
||||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||||
@@ -829,7 +833,7 @@ authRouter.put(
|
|||||||
rateLimit({
|
rateLimit({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 15,
|
max: 15,
|
||||||
keyGenerator: (req) => `signup:${req.ip}:${req.body.email}`,
|
keyGenerator: (req) => `signup:${ipKeyGenerator(req.ip || "")}:${req.body.email}`,
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `You can only sign up ${15} times every ${15} minutes. Please try again later.`;
|
const message = `You can only sign up ${15} times every ${15} minutes. Please try again later.`;
|
||||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||||
@@ -843,7 +847,7 @@ authRouter.post(
|
|||||||
rateLimit({
|
rateLimit({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 15,
|
max: 15,
|
||||||
keyGenerator: (req) => `login:${req.body.email || req.ip}`,
|
keyGenerator: (req) => `login:${req.body.email || ipKeyGenerator(req.ip || "")}`,
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `You can only log in ${15} times every ${15} minutes. Please try again later.`;
|
const message = `You can only log in ${15} times every ${15} minutes. Please try again later.`;
|
||||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||||
@@ -858,7 +862,7 @@ authRouter.post(
|
|||||||
rateLimit({
|
rateLimit({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 900,
|
max: 900,
|
||||||
keyGenerator: (req) => `newtGetToken:${req.body.newtId || req.ip}`,
|
keyGenerator: (req) => `newtGetToken:${req.body.newtId || ipKeyGenerator(req.ip || "")}`,
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `You can only request a Newt token ${900} times every ${15} minutes. Please try again later.`;
|
const message = `You can only request a Newt token ${900} times every ${15} minutes. Please try again later.`;
|
||||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||||
@@ -872,7 +876,7 @@ authRouter.post(
|
|||||||
rateLimit({
|
rateLimit({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 900,
|
max: 900,
|
||||||
keyGenerator: (req) => `newtGetToken:${req.body.newtId || req.ip}`,
|
keyGenerator: (req) => `olmGetToken:${req.body.newtId || ipKeyGenerator(req.ip || "")}`,
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `You can only request an Olm token ${900} times every ${15} minutes. Please try again later.`;
|
const message = `You can only request an Olm token ${900} times every ${15} minutes. Please try again later.`;
|
||||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||||
@@ -888,7 +892,7 @@ authRouter.post(
|
|||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 15,
|
max: 15,
|
||||||
keyGenerator: (req) => {
|
keyGenerator: (req) => {
|
||||||
return `signup:${req.body.email || req.user?.userId || req.ip}`;
|
return `signup:${req.body.email || req.user?.userId || ipKeyGenerator(req.ip || "")}`;
|
||||||
},
|
},
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `You can only enable 2FA ${15} times every ${15} minutes. Please try again later.`;
|
const message = `You can only enable 2FA ${15} times every ${15} minutes. Please try again later.`;
|
||||||
@@ -904,7 +908,7 @@ authRouter.post(
|
|||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 15,
|
max: 15,
|
||||||
keyGenerator: (req) => {
|
keyGenerator: (req) => {
|
||||||
return `signup:${req.body.email || req.user?.userId || req.ip}`;
|
return `signup:${req.body.email || req.user?.userId || ipKeyGenerator(req.ip || "")}`;
|
||||||
},
|
},
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `You can only request a 2FA code ${15} times every ${15} minutes. Please try again later.`;
|
const message = `You can only request a 2FA code ${15} times every ${15} minutes. Please try again later.`;
|
||||||
@@ -920,7 +924,7 @@ authRouter.post(
|
|||||||
rateLimit({
|
rateLimit({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 15,
|
max: 15,
|
||||||
keyGenerator: (req) => `signup:${req.user?.userId || req.ip}`,
|
keyGenerator: (req) => `signup:${req.user?.userId || ipKeyGenerator(req.ip || "")}`,
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `You can only disable 2FA ${15} times every ${15} minutes. Please try again later.`;
|
const message = `You can only disable 2FA ${15} times every ${15} minutes. Please try again later.`;
|
||||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||||
@@ -934,7 +938,7 @@ authRouter.post(
|
|||||||
rateLimit({
|
rateLimit({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 15,
|
max: 15,
|
||||||
keyGenerator: (req) => `signup:${req.body.email || req.ip}`,
|
keyGenerator: (req) => `signup:${req.body.email || ipKeyGenerator(req.ip || "")}`,
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `You can only sign up ${15} times every ${15} minutes. Please try again later.`;
|
const message = `You can only sign up ${15} times every ${15} minutes. Please try again later.`;
|
||||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||||
@@ -951,7 +955,8 @@ authRouter.post(
|
|||||||
rateLimit({
|
rateLimit({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 15,
|
max: 15,
|
||||||
keyGenerator: (req) => `requestEmailVerificationCode:${req.body.email || req.ip}`,
|
keyGenerator: (req) =>
|
||||||
|
`requestEmailVerificationCode:${req.user?.email || ipKeyGenerator(req.ip || "")}`,
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `You can only request an email verification code ${15} times every ${15} minutes. Please try again later.`;
|
const message = `You can only request an email verification code ${15} times every ${15} minutes. Please try again later.`;
|
||||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||||
@@ -972,7 +977,8 @@ authRouter.post(
|
|||||||
rateLimit({
|
rateLimit({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 15,
|
max: 15,
|
||||||
keyGenerator: (req) => `requestPasswordReset:${req.body.email || req.ip}`,
|
keyGenerator: (req) =>
|
||||||
|
`requestPasswordReset:${req.body.email || ipKeyGenerator(req.ip || "")}`,
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `You can only request a password reset ${15} times every ${15} minutes. Please try again later.`;
|
const message = `You can only request a password reset ${15} times every ${15} minutes. Please try again later.`;
|
||||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||||
@@ -987,7 +993,7 @@ authRouter.post(
|
|||||||
rateLimit({
|
rateLimit({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 15,
|
max: 15,
|
||||||
keyGenerator: (req) => `resetPassword:${req.body.email || req.ip}`,
|
keyGenerator: (req) => `resetPassword:${req.body.email || ipKeyGenerator(req.ip || "")}`,
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `You can only request a password reset ${15} times every ${15} minutes. Please try again later.`;
|
const message = `You can only request a password reset ${15} times every ${15} minutes. Please try again later.`;
|
||||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||||
@@ -1003,7 +1009,7 @@ authRouter.post(
|
|||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 15,
|
max: 15,
|
||||||
keyGenerator: (req) =>
|
keyGenerator: (req) =>
|
||||||
`authWithPassword:${req.ip}:${req.params.resourceId || req.ip}`,
|
`authWithPassword:${ipKeyGenerator(req.ip || "")}:${req.params.resourceId || ipKeyGenerator(req.ip || "")}`,
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `You can only authenticate with password ${15} times every ${15} minutes. Please try again later.`;
|
const message = `You can only authenticate with password ${15} times every ${15} minutes. Please try again later.`;
|
||||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||||
@@ -1018,7 +1024,7 @@ authRouter.post(
|
|||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 15,
|
max: 15,
|
||||||
keyGenerator: (req) =>
|
keyGenerator: (req) =>
|
||||||
`authWithPincode:${req.ip}:${req.params.resourceId || req.ip}`,
|
`authWithPincode:${ipKeyGenerator(req.ip || "")}:${req.params.resourceId || ipKeyGenerator(req.ip || "")}`,
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `You can only authenticate with pincode ${15} times every ${15} minutes. Please try again later.`;
|
const message = `You can only authenticate with pincode ${15} times every ${15} minutes. Please try again later.`;
|
||||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||||
@@ -1034,7 +1040,7 @@ authRouter.post(
|
|||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 15,
|
max: 15,
|
||||||
keyGenerator: (req) =>
|
keyGenerator: (req) =>
|
||||||
`authWithWhitelist:${req.ip}:${req.body.email}:${req.params.resourceId}`,
|
`authWithWhitelist:${ipKeyGenerator(req.ip || "")}:${req.body.email}:${req.params.resourceId}`,
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `You can only request an email OTP ${15} times every ${15} minutes. Please try again later.`;
|
const message = `You can only request an email OTP ${15} times every ${15} minutes. Please try again later.`;
|
||||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||||
@@ -1066,7 +1072,8 @@ authRouter.post(
|
|||||||
rateLimit({
|
rateLimit({
|
||||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
max: 5, // Allow 5 security key registrations per 15 minutes
|
max: 5, // Allow 5 security key registrations per 15 minutes
|
||||||
keyGenerator: (req) => `securityKeyRegister:${req.user?.userId || req.ip}`,
|
keyGenerator: (req) =>
|
||||||
|
`securityKeyRegister:${req.user?.userId || ipKeyGenerator(req.ip || "")}`,
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `You can only register a security key ${5} times every ${15} minutes. Please try again later.`;
|
const message = `You can only register a security key ${5} times every ${15} minutes. Please try again later.`;
|
||||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||||
@@ -1086,7 +1093,7 @@ authRouter.post(
|
|||||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
max: 10, // Allow 10 authentication attempts per 15 minutes per IP
|
max: 10, // Allow 10 authentication attempts per 15 minutes per IP
|
||||||
keyGenerator: (req) => {
|
keyGenerator: (req) => {
|
||||||
return `securityKeyAuth:${req.body.email || req.ip}`;
|
return `securityKeyAuth:${req.body.email || ipKeyGenerator(req.ip || "")}`;
|
||||||
},
|
},
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `You can only attempt security key authentication ${10} times every ${15} minutes. Please try again later.`;
|
const message = `You can only attempt security key authentication ${10} times every ${15} minutes. Please try again later.`;
|
||||||
@@ -1108,7 +1115,7 @@ authRouter.delete(
|
|||||||
rateLimit({
|
rateLimit({
|
||||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
max: 20, // Allow 10 authentication attempts per 15 minutes per IP
|
max: 20, // Allow 10 authentication attempts per 15 minutes per IP
|
||||||
keyGenerator: (req) => `securityKeyAuth:${req.user?.userId || req.ip}`,
|
keyGenerator: (req) => `securityKeyAuth:${req.user?.userId || ipKeyGenerator(req.ip || "")}`,
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `You can only delete a security key ${10} times every ${15} minutes. Please try again later.`;
|
const message = `You can only delete a security key ${10} times every ${15} minutes. Please try again later.`;
|
||||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||||
|
|||||||
58
server/routers/gerbil/createExitNode.ts
Normal file
58
server/routers/gerbil/createExitNode.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { db, ExitNode, exitNodes } from "@server/db";
|
||||||
|
import { getUniqueExitNodeEndpointName } from "@server/db/names";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
import { getNextAvailableSubnet } from "@server/lib/exitNodes";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
export async function createExitNode(publicKey: string, reachableAt: string | undefined) {
|
||||||
|
// Fetch exit node
|
||||||
|
const [exitNodeQuery] = await db.select().from(exitNodes).limit(1);
|
||||||
|
let exitNode: ExitNode;
|
||||||
|
if (!exitNodeQuery) {
|
||||||
|
const address = await getNextAvailableSubnet();
|
||||||
|
// TODO: eventually we will want to get the next available port so that we can multiple exit nodes
|
||||||
|
// const listenPort = await getNextAvailablePort();
|
||||||
|
const listenPort = config.getRawConfig().gerbil.start_port;
|
||||||
|
let subEndpoint = "";
|
||||||
|
if (config.getRawConfig().gerbil.use_subdomain) {
|
||||||
|
subEndpoint = await getUniqueExitNodeEndpointName();
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitNodeName =
|
||||||
|
config.getRawConfig().gerbil.exit_node_name ||
|
||||||
|
`Exit Node ${publicKey.slice(0, 8)}`;
|
||||||
|
|
||||||
|
// create a new exit node
|
||||||
|
[exitNode] = await db
|
||||||
|
.insert(exitNodes)
|
||||||
|
.values({
|
||||||
|
publicKey,
|
||||||
|
endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
|
||||||
|
address,
|
||||||
|
listenPort,
|
||||||
|
reachableAt,
|
||||||
|
name: exitNodeName
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// update the existing exit node
|
||||||
|
[exitNode] = await db
|
||||||
|
.update(exitNodes)
|
||||||
|
.set({
|
||||||
|
reachableAt,
|
||||||
|
publicKey
|
||||||
|
})
|
||||||
|
.where(eq(exitNodes.publicKey, publicKey))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
logger.info(`Updated exit node`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return exitNode;
|
||||||
|
}
|
||||||
@@ -1,6 +1,15 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { clients, exitNodes, newts, olms, Site, sites, clientSites } from "@server/db";
|
import {
|
||||||
|
clients,
|
||||||
|
exitNodes,
|
||||||
|
newts,
|
||||||
|
olms,
|
||||||
|
Site,
|
||||||
|
sites,
|
||||||
|
clientSites,
|
||||||
|
ExitNode
|
||||||
|
} from "@server/db";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -10,7 +19,7 @@ import { fromError } from "zod-validation-error";
|
|||||||
|
|
||||||
// Define Zod schema for request validation
|
// Define Zod schema for request validation
|
||||||
const getAllRelaysSchema = z.object({
|
const getAllRelaysSchema = z.object({
|
||||||
publicKey: z.string().optional(),
|
publicKey: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Type for peer destination
|
// Type for peer destination
|
||||||
@@ -44,103 +53,27 @@ export async function getAllRelays(
|
|||||||
const { publicKey } = parsedParams.data;
|
const { publicKey } = parsedParams.data;
|
||||||
|
|
||||||
if (!publicKey) {
|
if (!publicKey) {
|
||||||
return next(createHttpError(HttpCode.BAD_REQUEST, 'publicKey is required'));
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "publicKey is required")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch exit node
|
// Fetch exit node
|
||||||
const [exitNode] = await db.select().from(exitNodes).where(eq(exitNodes.publicKey, publicKey));
|
const [exitNode] = await db
|
||||||
|
.select()
|
||||||
|
.from(exitNodes)
|
||||||
|
.where(eq(exitNodes.publicKey, publicKey));
|
||||||
if (!exitNode) {
|
if (!exitNode) {
|
||||||
return next(createHttpError(HttpCode.NOT_FOUND, "Exit node not found"));
|
return next(
|
||||||
|
createHttpError(HttpCode.NOT_FOUND, "Exit node not found")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch sites for this exit node
|
const mappings = await generateRelayMappings(exitNode);
|
||||||
const sitesRes = await db.select().from(sites).where(eq(sites.exitNodeId, exitNode.exitNodeId));
|
|
||||||
|
|
||||||
if (sitesRes.length === 0) {
|
logger.debug(
|
||||||
return res.status(HttpCode.OK).send({
|
`Returning mappings for ${Object.keys(mappings).length} endpoints`
|
||||||
mappings: {}
|
);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize mappings object for multi-peer support
|
|
||||||
const mappings: { [key: string]: ProxyMapping } = {};
|
|
||||||
|
|
||||||
// Process each site
|
|
||||||
for (const site of sitesRes) {
|
|
||||||
if (!site.endpoint || !site.subnet || !site.listenPort) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find all clients associated with this site through clientSites
|
|
||||||
const clientSitesRes = await db
|
|
||||||
.select()
|
|
||||||
.from(clientSites)
|
|
||||||
.where(eq(clientSites.siteId, site.siteId));
|
|
||||||
|
|
||||||
for (const clientSite of clientSitesRes) {
|
|
||||||
if (!clientSite.endpoint) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add this site as a destination for the client
|
|
||||||
if (!mappings[clientSite.endpoint]) {
|
|
||||||
mappings[clientSite.endpoint] = { destinations: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add site as a destination for this client
|
|
||||||
const destination: PeerDestination = {
|
|
||||||
destinationIP: site.subnet.split("/")[0],
|
|
||||||
destinationPort: site.listenPort
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if this destination is already in the array to avoid duplicates
|
|
||||||
const isDuplicate = mappings[clientSite.endpoint].destinations.some(
|
|
||||||
dest => dest.destinationIP === destination.destinationIP &&
|
|
||||||
dest.destinationPort === destination.destinationPort
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isDuplicate) {
|
|
||||||
mappings[clientSite.endpoint].destinations.push(destination);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also handle site-to-site communication (all sites in the same org)
|
|
||||||
if (site.orgId) {
|
|
||||||
const orgSites = await db
|
|
||||||
.select()
|
|
||||||
.from(sites)
|
|
||||||
.where(eq(sites.orgId, site.orgId));
|
|
||||||
|
|
||||||
for (const peer of orgSites) {
|
|
||||||
// Skip self
|
|
||||||
if (peer.siteId === site.siteId || !peer.endpoint || !peer.subnet || !peer.listenPort) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add peer site as a destination for this site
|
|
||||||
if (!mappings[site.endpoint]) {
|
|
||||||
mappings[site.endpoint] = { destinations: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const destination: PeerDestination = {
|
|
||||||
destinationIP: peer.subnet.split("/")[0],
|
|
||||||
destinationPort: peer.listenPort
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check for duplicates
|
|
||||||
const isDuplicate = mappings[site.endpoint].destinations.some(
|
|
||||||
dest => dest.destinationIP === destination.destinationIP &&
|
|
||||||
dest.destinationPort === destination.destinationPort
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isDuplicate) {
|
|
||||||
mappings[site.endpoint].destinations.push(destination);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Returning mappings for ${Object.keys(mappings).length} endpoints`);
|
|
||||||
return res.status(HttpCode.OK).send({ mappings });
|
return res.status(HttpCode.OK).send({ mappings });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
@@ -152,3 +85,102 @@ export async function getAllRelays(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generateRelayMappings(exitNode: ExitNode) {
|
||||||
|
// Fetch sites for this exit node
|
||||||
|
const sitesRes = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.exitNodeId, exitNode.exitNodeId));
|
||||||
|
|
||||||
|
if (sitesRes.length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize mappings object for multi-peer support
|
||||||
|
const mappings: { [key: string]: ProxyMapping } = {};
|
||||||
|
|
||||||
|
// Process each site
|
||||||
|
for (const site of sitesRes) {
|
||||||
|
if (!site.endpoint || !site.subnet || !site.listenPort) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all clients associated with this site through clientSites
|
||||||
|
const clientSitesRes = await db
|
||||||
|
.select()
|
||||||
|
.from(clientSites)
|
||||||
|
.where(eq(clientSites.siteId, site.siteId));
|
||||||
|
|
||||||
|
for (const clientSite of clientSitesRes) {
|
||||||
|
if (!clientSite.endpoint) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this site as a destination for the client
|
||||||
|
if (!mappings[clientSite.endpoint]) {
|
||||||
|
mappings[clientSite.endpoint] = { destinations: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add site as a destination for this client
|
||||||
|
const destination: PeerDestination = {
|
||||||
|
destinationIP: site.subnet.split("/")[0],
|
||||||
|
destinationPort: site.listenPort
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if this destination is already in the array to avoid duplicates
|
||||||
|
const isDuplicate = mappings[clientSite.endpoint].destinations.some(
|
||||||
|
(dest) =>
|
||||||
|
dest.destinationIP === destination.destinationIP &&
|
||||||
|
dest.destinationPort === destination.destinationPort
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isDuplicate) {
|
||||||
|
mappings[clientSite.endpoint].destinations.push(destination);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also handle site-to-site communication (all sites in the same org)
|
||||||
|
if (site.orgId) {
|
||||||
|
const orgSites = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.orgId, site.orgId));
|
||||||
|
|
||||||
|
for (const peer of orgSites) {
|
||||||
|
// Skip self
|
||||||
|
if (
|
||||||
|
peer.siteId === site.siteId ||
|
||||||
|
!peer.endpoint ||
|
||||||
|
!peer.subnet ||
|
||||||
|
!peer.listenPort
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add peer site as a destination for this site
|
||||||
|
if (!mappings[site.endpoint]) {
|
||||||
|
mappings[site.endpoint] = { destinations: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const destination: PeerDestination = {
|
||||||
|
destinationIP: peer.subnet.split("/")[0],
|
||||||
|
destinationPort: peer.listenPort
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for duplicates
|
||||||
|
const isDuplicate = mappings[site.endpoint].destinations.some(
|
||||||
|
(dest) =>
|
||||||
|
dest.destinationIP === destination.destinationIP &&
|
||||||
|
dest.destinationPort === destination.destinationPort
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isDuplicate) {
|
||||||
|
mappings[site.endpoint].destinations.push(destination);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappings;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { sites, resources, targets, exitNodes } from "@server/db";
|
import { sites, resources, targets, exitNodes, ExitNode } from "@server/db";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { eq, isNotNull, and } from "drizzle-orm";
|
import { eq, isNotNull, and } from "drizzle-orm";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -11,6 +11,10 @@ import { getUniqueExitNodeEndpointName } from "../../db/names";
|
|||||||
import { findNextAvailableCidr } from "@server/lib/ip";
|
import { findNextAvailableCidr } from "@server/lib/ip";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { getAllowedIps } from "../target/helpers";
|
import { getAllowedIps } from "../target/helpers";
|
||||||
|
import { proxyToRemote } from "@server/lib/remoteProxy";
|
||||||
|
import { getNextAvailableSubnet } from "@server/lib/exitNodes";
|
||||||
|
import { createExitNode } from "./createExitNode";
|
||||||
|
|
||||||
// Define Zod schema for request validation
|
// Define Zod schema for request validation
|
||||||
const getConfigSchema = z.object({
|
const getConfigSchema = z.object({
|
||||||
publicKey: z.string(),
|
publicKey: z.string(),
|
||||||
@@ -51,46 +55,7 @@ export async function getConfig(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch exit node
|
const exitNode = await createExitNode(publicKey, reachableAt);
|
||||||
const exitNodeQuery = await db
|
|
||||||
.select()
|
|
||||||
.from(exitNodes)
|
|
||||||
.where(eq(exitNodes.publicKey, publicKey));
|
|
||||||
let exitNode;
|
|
||||||
if (exitNodeQuery.length === 0) {
|
|
||||||
const address = await getNextAvailableSubnet();
|
|
||||||
// TODO: eventually we will want to get the next available port so that we can multiple exit nodes
|
|
||||||
// const listenPort = await getNextAvailablePort();
|
|
||||||
const listenPort = config.getRawConfig().gerbil.start_port;
|
|
||||||
let subEndpoint = "";
|
|
||||||
if (config.getRawConfig().gerbil.use_subdomain) {
|
|
||||||
subEndpoint = await getUniqueExitNodeEndpointName();
|
|
||||||
}
|
|
||||||
|
|
||||||
const exitNodeName =
|
|
||||||
config.getRawConfig().gerbil.exit_node_name ||
|
|
||||||
`Exit Node ${publicKey.slice(0, 8)}`;
|
|
||||||
|
|
||||||
// create a new exit node
|
|
||||||
exitNode = await db
|
|
||||||
.insert(exitNodes)
|
|
||||||
.values({
|
|
||||||
publicKey,
|
|
||||||
endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
|
|
||||||
address,
|
|
||||||
listenPort,
|
|
||||||
reachableAt,
|
|
||||||
name: exitNodeName
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`Created new exit node ${exitNode[0].name} with address ${exitNode[0].address} and port ${exitNode[0].listenPort}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
exitNode = exitNodeQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!exitNode) {
|
if (!exitNode) {
|
||||||
return next(
|
return next(
|
||||||
@@ -101,42 +66,17 @@ export async function getConfig(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sitesRes = await db
|
// STOP HERE IN HYBRID MODE
|
||||||
.select()
|
if (config.isManagedMode()) {
|
||||||
.from(sites)
|
req.body = {
|
||||||
.where(
|
...req.body,
|
||||||
and(
|
endpoint: exitNode.endpoint,
|
||||||
eq(sites.exitNodeId, exitNode[0].exitNodeId),
|
listenPort: exitNode.listenPort
|
||||||
isNotNull(sites.pubKey),
|
};
|
||||||
isNotNull(sites.subnet)
|
return proxyToRemote(req, res, next, "hybrid/gerbil/get-config");
|
||||||
)
|
}
|
||||||
);
|
|
||||||
|
|
||||||
const peers = await Promise.all(
|
const configResponse = await generateGerbilConfig(exitNode);
|
||||||
sitesRes.map(async (site) => {
|
|
||||||
if (site.type === "wireguard") {
|
|
||||||
return {
|
|
||||||
publicKey: site.pubKey,
|
|
||||||
allowedIps: await getAllowedIps(site.siteId)
|
|
||||||
};
|
|
||||||
} else if (site.type === "newt") {
|
|
||||||
return {
|
|
||||||
publicKey: site.pubKey,
|
|
||||||
allowedIps: [site.subnet!]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
publicKey: null,
|
|
||||||
allowedIps: []
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const configResponse: GetConfigResponse = {
|
|
||||||
listenPort: exitNode[0].listenPort || 51820,
|
|
||||||
ipAddress: exitNode[0].address,
|
|
||||||
peers
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.debug("Sending config: ", configResponse);
|
logger.debug("Sending config: ", configResponse);
|
||||||
|
|
||||||
@@ -152,31 +92,45 @@ export async function getConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getNextAvailableSubnet(): Promise<string> {
|
export async function generateGerbilConfig(exitNode: ExitNode) {
|
||||||
// Get all existing subnets from routes table
|
const sitesRes = await db
|
||||||
const existingAddresses = await db
|
.select()
|
||||||
.select({
|
.from(sites)
|
||||||
address: exitNodes.address
|
.where(
|
||||||
|
and(
|
||||||
|
eq(sites.exitNodeId, exitNode.exitNodeId),
|
||||||
|
isNotNull(sites.pubKey),
|
||||||
|
isNotNull(sites.subnet)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const peers = await Promise.all(
|
||||||
|
sitesRes.map(async (site) => {
|
||||||
|
if (site.type === "wireguard") {
|
||||||
|
return {
|
||||||
|
publicKey: site.pubKey,
|
||||||
|
allowedIps: await getAllowedIps(site.siteId)
|
||||||
|
};
|
||||||
|
} else if (site.type === "newt") {
|
||||||
|
return {
|
||||||
|
publicKey: site.pubKey,
|
||||||
|
allowedIps: [site.subnet!]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
publicKey: null,
|
||||||
|
allowedIps: []
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.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
|
const configResponse: GetConfigResponse = {
|
||||||
subnet =
|
listenPort: exitNode.listenPort || 51820,
|
||||||
subnet.split(".").slice(0, 3).join(".") +
|
ipAddress: exitNode.address,
|
||||||
".1" +
|
peers
|
||||||
"/" +
|
};
|
||||||
subnet.split("/")[1];
|
|
||||||
return subnet;
|
return configResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getNextAvailablePort(): Promise<number> {
|
async function getNextAvailablePort(): Promise<number> {
|
||||||
|
|||||||
46
server/routers/gerbil/getResolvedHostname.ts
Normal file
46
server/routers/gerbil/getResolvedHostname.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
|
||||||
|
// Define Zod schema for request validation
|
||||||
|
const getResolvedHostnameSchema = z.object({
|
||||||
|
hostname: z.string(),
|
||||||
|
publicKey: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function getResolvedHostname(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
// Validate request parameters
|
||||||
|
const parsedParams = getResolvedHostnameSchema.safeParse(
|
||||||
|
req.body
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the endpoints
|
||||||
|
return res.status(HttpCode.OK).send({
|
||||||
|
endpoints: [] // ALWAYS ROUTE LOCALLY
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"An error occurred..."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,3 +2,4 @@ export * from "./getConfig";
|
|||||||
export * from "./receiveBandwidth";
|
export * from "./receiveBandwidth";
|
||||||
export * from "./updateHolePunch";
|
export * from "./updateHolePunch";
|
||||||
export * from "./getAllRelays";
|
export * from "./getAllRelays";
|
||||||
|
export * from "./getResolvedHostname";
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import axios from "axios";
|
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { exitNodes } from "@server/db";
|
import { exitNodes } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import { sendToExitNode } from "../../lib/exitNodeComms";
|
||||||
|
|
||||||
export async function addPeer(
|
export async function addPeer(
|
||||||
exitNodeId: number,
|
exitNodeId: number,
|
||||||
@@ -22,34 +22,13 @@ export async function addPeer(
|
|||||||
if (!exitNode) {
|
if (!exitNode) {
|
||||||
throw new Error(`Exit node with ID ${exitNodeId} not found`);
|
throw new Error(`Exit node with ID ${exitNodeId} not found`);
|
||||||
}
|
}
|
||||||
if (!exitNode.reachableAt) {
|
|
||||||
throw new Error(`Exit node with ID ${exitNodeId} is not reachable`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
return await sendToExitNode(exitNode, {
|
||||||
const response = await axios.post(
|
remoteType: "remoteExitNode/peers/add",
|
||||||
`${exitNode.reachableAt}/peer`,
|
localPath: "/peer",
|
||||||
peer,
|
method: "POST",
|
||||||
{
|
data: peer
|
||||||
headers: {
|
});
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info("Peer added successfully:", { peer: response.data.status });
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error(
|
|
||||||
`Error adding peer (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.error(
|
|
||||||
`Error adding peer for exit node at ${exitNode.reachableAt}: ${error}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deletePeer(exitNodeId: number, publicKey: string) {
|
export async function deletePeer(exitNodeId: number, publicKey: string) {
|
||||||
@@ -64,24 +43,16 @@ export async function deletePeer(exitNodeId: number, publicKey: string) {
|
|||||||
if (!exitNode) {
|
if (!exitNode) {
|
||||||
throw new Error(`Exit node with ID ${exitNodeId} not found`);
|
throw new Error(`Exit node with ID ${exitNodeId} not found`);
|
||||||
}
|
}
|
||||||
if (!exitNode.reachableAt) {
|
|
||||||
throw new Error(`Exit node with ID ${exitNodeId} is not reachable`);
|
return await sendToExitNode(exitNode, {
|
||||||
}
|
remoteType: "remoteExitNode/peers/remove",
|
||||||
try {
|
localPath: "/peer",
|
||||||
const response = await axios.delete(
|
method: "DELETE",
|
||||||
`${exitNode.reachableAt}/peer?public_key=${encodeURIComponent(publicKey)}`
|
data: {
|
||||||
);
|
publicKey: publicKey
|
||||||
logger.info("Peer deleted successfully:", response.data.status);
|
},
|
||||||
return response.data;
|
queryParams: {
|
||||||
} catch (error) {
|
public_key: publicKey
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error(
|
|
||||||
`Error deleting peer (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.error(
|
|
||||||
`Error deleting peer for exit node at ${exitNode.reachableAt}: ${error}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import logger from "@server/logger";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
|
import { checkExitNodeOrg } from "@server/lib/exitNodes";
|
||||||
|
|
||||||
// Track sites that are already offline to avoid unnecessary queries
|
// Track sites that are already offline to avoid unnecessary queries
|
||||||
const offlineSites = new Set<string>();
|
const offlineSites = new Set<string>();
|
||||||
@@ -28,103 +29,7 @@ export const receiveBandwidth = async (
|
|||||||
throw new Error("Invalid bandwidth data");
|
throw new Error("Invalid bandwidth data");
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentTime = new Date();
|
await updateSiteBandwidth(bandwidthData);
|
||||||
const oneMinuteAgo = new Date(currentTime.getTime() - 60000); // 1 minute ago
|
|
||||||
|
|
||||||
// logger.debug(`Received data: ${JSON.stringify(bandwidthData)}`);
|
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
|
||||||
// First, handle sites that are actively reporting bandwidth
|
|
||||||
const activePeers = bandwidthData.filter(peer => peer.bytesIn > 0); // Bytesout will have data as it tries to send keep alive messages
|
|
||||||
|
|
||||||
if (activePeers.length > 0) {
|
|
||||||
// Remove any active peers from offline tracking since they're sending data
|
|
||||||
activePeers.forEach(peer => offlineSites.delete(peer.publicKey));
|
|
||||||
|
|
||||||
// Aggregate usage data by organization
|
|
||||||
const orgUsageMap = new Map<string, number>();
|
|
||||||
const orgUptimeMap = new Map<string, number>();
|
|
||||||
|
|
||||||
// Update all active sites with bandwidth data and get the site data in one operation
|
|
||||||
const updatedSites = [];
|
|
||||||
for (const peer of activePeers) {
|
|
||||||
const updatedSite = await trx
|
|
||||||
.update(sites)
|
|
||||||
.set({
|
|
||||||
megabytesOut: sql`${sites.megabytesOut} + ${peer.bytesIn}`,
|
|
||||||
megabytesIn: sql`${sites.megabytesIn} + ${peer.bytesOut}`,
|
|
||||||
lastBandwidthUpdate: currentTime.toISOString(),
|
|
||||||
online: true
|
|
||||||
})
|
|
||||||
.where(eq(sites.pubKey, peer.publicKey))
|
|
||||||
.returning({
|
|
||||||
online: sites.online,
|
|
||||||
orgId: sites.orgId,
|
|
||||||
siteId: sites.siteId,
|
|
||||||
lastBandwidthUpdate: sites.lastBandwidthUpdate,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (updatedSite.length > 0) {
|
|
||||||
updatedSites.push({ ...updatedSite[0], peer });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate org usage aggregations using the updated site data
|
|
||||||
for (const { peer, ...site } of updatedSites) {
|
|
||||||
// Aggregate bandwidth usage for the org
|
|
||||||
const totalBandwidth = peer.bytesIn + peer.bytesOut;
|
|
||||||
const currentOrgUsage = orgUsageMap.get(site.orgId) || 0;
|
|
||||||
orgUsageMap.set(site.orgId, currentOrgUsage + totalBandwidth);
|
|
||||||
|
|
||||||
// Add 10 seconds of uptime for each active site
|
|
||||||
const currentOrgUptime = orgUptimeMap.get(site.orgId) || 0;
|
|
||||||
orgUptimeMap.set(site.orgId, currentOrgUptime + 10 / 60); // Store in minutes and jut add 10 seconds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle sites that reported zero bandwidth but need online status updated
|
|
||||||
const zeroBandwidthPeers = bandwidthData.filter(peer =>
|
|
||||||
peer.bytesIn === 0 && !offlineSites.has(peer.publicKey) // Bytesout will have data as it tries to send keep alive messages
|
|
||||||
);
|
|
||||||
|
|
||||||
if (zeroBandwidthPeers.length > 0) {
|
|
||||||
const zeroBandwidthSites = await trx
|
|
||||||
.select()
|
|
||||||
.from(sites)
|
|
||||||
.where(inArray(sites.pubKey, zeroBandwidthPeers.map(p => p.publicKey)));
|
|
||||||
|
|
||||||
for (const site of zeroBandwidthSites) {
|
|
||||||
let newOnlineStatus = site.online;
|
|
||||||
|
|
||||||
// Check if site should go offline based on last bandwidth update WITH DATA
|
|
||||||
if (site.lastBandwidthUpdate) {
|
|
||||||
const lastUpdateWithData = new Date(site.lastBandwidthUpdate);
|
|
||||||
if (lastUpdateWithData < oneMinuteAgo) {
|
|
||||||
newOnlineStatus = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No previous data update recorded, set to offline
|
|
||||||
newOnlineStatus = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always update lastBandwidthUpdate to show this instance is receiving reports
|
|
||||||
// Only update online status if it changed
|
|
||||||
if (site.online !== newOnlineStatus) {
|
|
||||||
await trx
|
|
||||||
.update(sites)
|
|
||||||
.set({
|
|
||||||
online: newOnlineStatus
|
|
||||||
})
|
|
||||||
.where(eq(sites.siteId, site.siteId));
|
|
||||||
|
|
||||||
// If site went offline, add it to our tracking set
|
|
||||||
if (!newOnlineStatus && site.pubKey) {
|
|
||||||
offlineSites.add(site.pubKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: {},
|
data: {},
|
||||||
@@ -143,3 +48,141 @@ export const receiveBandwidth = async (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function updateSiteBandwidth(
|
||||||
|
bandwidthData: PeerBandwidth[],
|
||||||
|
exitNodeId?: number
|
||||||
|
) {
|
||||||
|
const currentTime = new Date();
|
||||||
|
const oneMinuteAgo = new Date(currentTime.getTime() - 60000); // 1 minute ago
|
||||||
|
|
||||||
|
// logger.debug(`Received data: ${JSON.stringify(bandwidthData)}`);
|
||||||
|
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
// First, handle sites that are actively reporting bandwidth
|
||||||
|
const activePeers = bandwidthData.filter((peer) => peer.bytesIn > 0); // Bytesout will have data as it tries to send keep alive messages
|
||||||
|
|
||||||
|
if (activePeers.length > 0) {
|
||||||
|
// Remove any active peers from offline tracking since they're sending data
|
||||||
|
activePeers.forEach((peer) => offlineSites.delete(peer.publicKey));
|
||||||
|
|
||||||
|
// Aggregate usage data by organization
|
||||||
|
const orgUsageMap = new Map<string, number>();
|
||||||
|
const orgUptimeMap = new Map<string, number>();
|
||||||
|
|
||||||
|
// Update all active sites with bandwidth data and get the site data in one operation
|
||||||
|
const updatedSites = [];
|
||||||
|
for (const peer of activePeers) {
|
||||||
|
const [updatedSite] = await trx
|
||||||
|
.update(sites)
|
||||||
|
.set({
|
||||||
|
megabytesOut: sql`${sites.megabytesOut} + ${peer.bytesIn}`,
|
||||||
|
megabytesIn: sql`${sites.megabytesIn} + ${peer.bytesOut}`,
|
||||||
|
lastBandwidthUpdate: currentTime.toISOString(),
|
||||||
|
online: true
|
||||||
|
})
|
||||||
|
.where(eq(sites.pubKey, peer.publicKey))
|
||||||
|
.returning({
|
||||||
|
online: sites.online,
|
||||||
|
orgId: sites.orgId,
|
||||||
|
siteId: sites.siteId,
|
||||||
|
lastBandwidthUpdate: sites.lastBandwidthUpdate
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exitNodeId) {
|
||||||
|
if (await checkExitNodeOrg(exitNodeId, updatedSite.orgId)) {
|
||||||
|
// not allowed
|
||||||
|
logger.warn(
|
||||||
|
`Exit node ${exitNodeId} is not allowed for org ${updatedSite.orgId}`
|
||||||
|
);
|
||||||
|
// THIS SHOULD TRIGGER THE TRANSACTION TO FAIL?
|
||||||
|
throw new Error("Exit node not allowed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedSite) {
|
||||||
|
updatedSites.push({ ...updatedSite, peer });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate org usage aggregations using the updated site data
|
||||||
|
for (const { peer, ...site } of updatedSites) {
|
||||||
|
// Aggregate bandwidth usage for the org
|
||||||
|
const totalBandwidth = peer.bytesIn + peer.bytesOut;
|
||||||
|
const currentOrgUsage = orgUsageMap.get(site.orgId) || 0;
|
||||||
|
orgUsageMap.set(site.orgId, currentOrgUsage + totalBandwidth);
|
||||||
|
|
||||||
|
// Add 10 seconds of uptime for each active site
|
||||||
|
const currentOrgUptime = orgUptimeMap.get(site.orgId) || 0;
|
||||||
|
orgUptimeMap.set(site.orgId, currentOrgUptime + 10 / 60); // Store in minutes and jut add 10 seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle sites that reported zero bandwidth but need online status updated
|
||||||
|
const zeroBandwidthPeers = bandwidthData.filter(
|
||||||
|
(peer) => peer.bytesIn === 0 && !offlineSites.has(peer.publicKey) // Bytesout will have data as it tries to send keep alive messages
|
||||||
|
);
|
||||||
|
|
||||||
|
if (zeroBandwidthPeers.length > 0) {
|
||||||
|
const zeroBandwidthSites = await trx
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(
|
||||||
|
inArray(
|
||||||
|
sites.pubKey,
|
||||||
|
zeroBandwidthPeers.map((p) => p.publicKey)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const site of zeroBandwidthSites) {
|
||||||
|
let newOnlineStatus = site.online;
|
||||||
|
|
||||||
|
// Check if site should go offline based on last bandwidth update WITH DATA
|
||||||
|
if (site.lastBandwidthUpdate) {
|
||||||
|
const lastUpdateWithData = new Date(
|
||||||
|
site.lastBandwidthUpdate
|
||||||
|
);
|
||||||
|
if (lastUpdateWithData < oneMinuteAgo) {
|
||||||
|
newOnlineStatus = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No previous data update recorded, set to offline
|
||||||
|
newOnlineStatus = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always update lastBandwidthUpdate to show this instance is receiving reports
|
||||||
|
// Only update online status if it changed
|
||||||
|
if (site.online !== newOnlineStatus) {
|
||||||
|
const [updatedSite] = await trx
|
||||||
|
.update(sites)
|
||||||
|
.set({
|
||||||
|
online: newOnlineStatus
|
||||||
|
})
|
||||||
|
.where(eq(sites.siteId, site.siteId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (exitNodeId) {
|
||||||
|
if (
|
||||||
|
await checkExitNodeOrg(
|
||||||
|
exitNodeId,
|
||||||
|
updatedSite.orgId
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// not allowed
|
||||||
|
logger.warn(
|
||||||
|
`Exit node ${exitNodeId} is not allowed for org ${updatedSite.orgId}`
|
||||||
|
);
|
||||||
|
// THIS SHOULD TRIGGER THE TRANSACTION TO FAIL?
|
||||||
|
throw new Error("Exit node not allowed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If site went offline, add it to our tracking set
|
||||||
|
if (!newOnlineStatus && site.pubKey) {
|
||||||
|
offlineSites.add(site.pubKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { fromError } from "zod-validation-error";
|
|||||||
import { validateNewtSessionToken } from "@server/auth/sessions/newt";
|
import { validateNewtSessionToken } from "@server/auth/sessions/newt";
|
||||||
import { validateOlmSessionToken } from "@server/auth/sessions/olm";
|
import { validateOlmSessionToken } from "@server/auth/sessions/olm";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { checkExitNodeOrg } from "@server/lib/exitNodes";
|
||||||
|
|
||||||
// Define Zod schema for request validation
|
// Define Zod schema for request validation
|
||||||
const updateHolePunchSchema = z.object({
|
const updateHolePunchSchema = z.object({
|
||||||
@@ -66,228 +67,34 @@ export async function updateHolePunch(
|
|||||||
publicKey
|
publicKey
|
||||||
} = parsedParams.data;
|
} = parsedParams.data;
|
||||||
|
|
||||||
let currentSiteId: number | undefined;
|
let exitNode: ExitNode | undefined;
|
||||||
let destinations: PeerDestination[] = [];
|
if (publicKey) {
|
||||||
|
// Get the exit node by public key
|
||||||
if (olmId) {
|
[exitNode] = await db
|
||||||
logger.debug(
|
|
||||||
`Got hole punch with ip: ${ip}, port: ${port} for olmId: ${olmId}${publicKey ? ` with exit node publicKey: ${publicKey}` : ""}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const { session, olm: olmSession } =
|
|
||||||
await validateOlmSessionToken(token);
|
|
||||||
if (!session || !olmSession) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "Unauthorized")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (olmId !== olmSession.olmId) {
|
|
||||||
logger.warn(
|
|
||||||
`Olm ID mismatch: ${olmId} !== ${olmSession.olmId}`
|
|
||||||
);
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "Unauthorized")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [olm] = await db
|
|
||||||
.select()
|
.select()
|
||||||
.from(olms)
|
.from(exitNodes)
|
||||||
.where(eq(olms.olmId, olmId));
|
.where(eq(exitNodes.publicKey, publicKey));
|
||||||
|
} else {
|
||||||
if (!olm || !olm.clientId) {
|
// FOR BACKWARDS COMPATIBILITY IF GERBIL IS STILL =<1.1.0
|
||||||
logger.warn(`Olm not found: ${olmId}`);
|
[exitNode] = await db.select().from(exitNodes).limit(1);
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.NOT_FOUND, "Olm not found")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [client] = await db
|
|
||||||
.update(clients)
|
|
||||||
.set({
|
|
||||||
lastHolePunch: timestamp
|
|
||||||
})
|
|
||||||
.where(eq(clients.clientId, olm.clientId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
let exitNode: ExitNode | undefined;
|
|
||||||
if (publicKey) {
|
|
||||||
// Get the exit node by public key
|
|
||||||
[exitNode] = await db
|
|
||||||
.select()
|
|
||||||
.from(exitNodes)
|
|
||||||
.where(eq(exitNodes.publicKey, publicKey));
|
|
||||||
} else {
|
|
||||||
// FOR BACKWARDS COMPATIBILITY IF GERBIL IS STILL =<1.1.0
|
|
||||||
[exitNode] = await db.select().from(exitNodes).limit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!exitNode) {
|
|
||||||
logger.warn(`Exit node not found for publicKey: ${publicKey}`);
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.NOT_FOUND, "Exit node not found")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get sites that are on this specific exit node and connected to this client
|
|
||||||
const sitesOnExitNode = await db
|
|
||||||
.select({ siteId: sites.siteId, subnet: sites.subnet, listenPort: sites.listenPort })
|
|
||||||
.from(sites)
|
|
||||||
.innerJoin(clientSites, eq(sites.siteId, clientSites.siteId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(sites.exitNodeId, exitNode.exitNodeId),
|
|
||||||
eq(clientSites.clientId, olm.clientId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update clientSites for each site on this exit node
|
|
||||||
for (const site of sitesOnExitNode) {
|
|
||||||
logger.debug(
|
|
||||||
`Updating site ${site.siteId} on exit node with publicKey: ${publicKey}`
|
|
||||||
);
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(clientSites)
|
|
||||||
.set({
|
|
||||||
endpoint: `${ip}:${port}`
|
|
||||||
})
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(clientSites.clientId, olm.clientId),
|
|
||||||
eq(clientSites.siteId, site.siteId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`Updated ${sitesOnExitNode.length} sites on exit node with publicKey: ${publicKey}`
|
|
||||||
);
|
|
||||||
if (!client) {
|
|
||||||
logger.warn(`Client not found for olm: ${olmId}`);
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.NOT_FOUND, "Client not found")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a list of the destinations from the sites
|
|
||||||
for (const site of sitesOnExitNode) {
|
|
||||||
if (site.subnet && site.listenPort) {
|
|
||||||
destinations.push({
|
|
||||||
destinationIP: site.subnet.split("/")[0],
|
|
||||||
destinationPort: site.listenPort
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (newtId) {
|
|
||||||
logger.debug(
|
|
||||||
`Got hole punch with ip: ${ip}, port: ${port} for newtId: ${newtId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const { session, newt: newtSession } =
|
|
||||||
await validateNewtSessionToken(token);
|
|
||||||
|
|
||||||
if (!session || !newtSession) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "Unauthorized")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newtId !== newtSession.newtId) {
|
|
||||||
logger.warn(
|
|
||||||
`Newt ID mismatch: ${newtId} !== ${newtSession.newtId}`
|
|
||||||
);
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "Unauthorized")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [newt] = await db
|
|
||||||
.select()
|
|
||||||
.from(newts)
|
|
||||||
.where(eq(newts.newtId, newtId));
|
|
||||||
|
|
||||||
if (!newt || !newt.siteId) {
|
|
||||||
logger.warn(`Newt not found: ${newtId}`);
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.NOT_FOUND, "New not found")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentSiteId = newt.siteId;
|
|
||||||
|
|
||||||
// Update the current site with the new endpoint
|
|
||||||
const [updatedSite] = await db
|
|
||||||
.update(sites)
|
|
||||||
.set({
|
|
||||||
endpoint: `${ip}:${port}`,
|
|
||||||
lastHolePunch: timestamp
|
|
||||||
})
|
|
||||||
.where(eq(sites.siteId, newt.siteId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!updatedSite || !updatedSite.subnet) {
|
|
||||||
logger.warn(`Site not found: ${newt.siteId}`);
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.NOT_FOUND, "Site not found")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find all clients that connect to this site
|
|
||||||
// const sitesClientPairs = await db
|
|
||||||
// .select()
|
|
||||||
// .from(clientSites)
|
|
||||||
// .where(eq(clientSites.siteId, newt.siteId));
|
|
||||||
|
|
||||||
// THE NEWT IS NOT SENDING RAW WG TO THE GERBIL SO IDK IF WE REALLY NEED THIS - REMOVING
|
|
||||||
// Get client details for each client
|
|
||||||
// for (const pair of sitesClientPairs) {
|
|
||||||
// const [client] = await db
|
|
||||||
// .select()
|
|
||||||
// .from(clients)
|
|
||||||
// .where(eq(clients.clientId, pair.clientId));
|
|
||||||
|
|
||||||
// if (client && client.endpoint) {
|
|
||||||
// const [host, portStr] = client.endpoint.split(':');
|
|
||||||
// if (host && portStr) {
|
|
||||||
// destinations.push({
|
|
||||||
// destinationIP: host,
|
|
||||||
// destinationPort: parseInt(portStr, 10)
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// If this is a newt/site, also add other sites in the same org
|
|
||||||
// if (updatedSite.orgId) {
|
|
||||||
// const orgSites = await db
|
|
||||||
// .select()
|
|
||||||
// .from(sites)
|
|
||||||
// .where(eq(sites.orgId, updatedSite.orgId));
|
|
||||||
|
|
||||||
// for (const site of orgSites) {
|
|
||||||
// // Don't add the current site to the destinations
|
|
||||||
// if (site.siteId !== currentSiteId && site.subnet && site.endpoint && site.listenPort) {
|
|
||||||
// const [host, portStr] = site.endpoint.split(':');
|
|
||||||
// if (host && portStr) {
|
|
||||||
// destinations.push({
|
|
||||||
// destinationIP: host,
|
|
||||||
// destinationPort: site.listenPort
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (destinations.length === 0) {
|
if (!exitNode) {
|
||||||
// logger.warn(
|
logger.warn(`Exit node not found for publicKey: ${publicKey}`);
|
||||||
// `No peer destinations found for olmId: ${olmId} or newtId: ${newtId}`
|
return next(
|
||||||
// );
|
createHttpError(HttpCode.NOT_FOUND, "Exit node not found")
|
||||||
// return next(createHttpError(HttpCode.NOT_FOUND, "No peer destinations found"));
|
);
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
const destinations = await updateAndGenerateEndpointDestinations(
|
||||||
|
olmId,
|
||||||
|
newtId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
timestamp,
|
||||||
|
token,
|
||||||
|
exitNode
|
||||||
|
);
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Returning ${destinations.length} peer destinations for olmId: ${olmId} or newtId: ${newtId}: ${JSON.stringify(destinations, null, 2)}`
|
`Returning ${destinations.length} peer destinations for olmId: ${olmId} or newtId: ${newtId}: ${JSON.stringify(destinations, null, 2)}`
|
||||||
@@ -307,3 +114,215 @@ export async function updateHolePunch(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateAndGenerateEndpointDestinations(
|
||||||
|
olmId: string | undefined,
|
||||||
|
newtId: string | undefined,
|
||||||
|
ip: string,
|
||||||
|
port: number,
|
||||||
|
timestamp: number,
|
||||||
|
token: string,
|
||||||
|
exitNode: ExitNode
|
||||||
|
) {
|
||||||
|
let currentSiteId: number | undefined;
|
||||||
|
const destinations: PeerDestination[] = [];
|
||||||
|
|
||||||
|
if (olmId) {
|
||||||
|
logger.debug(
|
||||||
|
`Got hole punch with ip: ${ip}, port: ${port} for olmId: ${olmId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const { session, olm: olmSession } =
|
||||||
|
await validateOlmSessionToken(token);
|
||||||
|
if (!session || !olmSession) {
|
||||||
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (olmId !== olmSession.olmId) {
|
||||||
|
logger.warn(`Olm ID mismatch: ${olmId} !== ${olmSession.olmId}`);
|
||||||
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [olm] = await db.select().from(olms).where(eq(olms.olmId, olmId));
|
||||||
|
|
||||||
|
if (!olm || !olm.clientId) {
|
||||||
|
logger.warn(`Olm not found: ${olmId}`);
|
||||||
|
throw new Error("Olm not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [client] = await db
|
||||||
|
.update(clients)
|
||||||
|
.set({
|
||||||
|
lastHolePunch: timestamp
|
||||||
|
})
|
||||||
|
.where(eq(clients.clientId, olm.clientId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (await checkExitNodeOrg(exitNode.exitNodeId, client.orgId)) {
|
||||||
|
// not allowed
|
||||||
|
logger.warn(
|
||||||
|
`Exit node ${exitNode.exitNodeId} is not allowed for org ${client.orgId}`
|
||||||
|
);
|
||||||
|
throw new Error("Exit node not allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get sites that are on this specific exit node and connected to this client
|
||||||
|
const sitesOnExitNode = await db
|
||||||
|
.select({
|
||||||
|
siteId: sites.siteId,
|
||||||
|
subnet: sites.subnet,
|
||||||
|
listenPort: sites.listenPort
|
||||||
|
})
|
||||||
|
.from(sites)
|
||||||
|
.innerJoin(clientSites, eq(sites.siteId, clientSites.siteId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(sites.exitNodeId, exitNode.exitNodeId),
|
||||||
|
eq(clientSites.clientId, olm.clientId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update clientSites for each site on this exit node
|
||||||
|
for (const site of sitesOnExitNode) {
|
||||||
|
logger.debug(
|
||||||
|
`Updating site ${site.siteId} on exit node ${exitNode.exitNodeId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(clientSites)
|
||||||
|
.set({
|
||||||
|
endpoint: `${ip}:${port}`
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clientSites.clientId, olm.clientId),
|
||||||
|
eq(clientSites.siteId, site.siteId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Updated ${sitesOnExitNode.length} sites on exit node ${exitNode.exitNodeId}`
|
||||||
|
);
|
||||||
|
if (!client) {
|
||||||
|
logger.warn(`Client not found for olm: ${olmId}`);
|
||||||
|
throw new Error("Client not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a list of the destinations from the sites
|
||||||
|
for (const site of sitesOnExitNode) {
|
||||||
|
if (site.subnet && site.listenPort) {
|
||||||
|
destinations.push({
|
||||||
|
destinationIP: site.subnet.split("/")[0],
|
||||||
|
destinationPort: site.listenPort
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (newtId) {
|
||||||
|
logger.debug(
|
||||||
|
`Got hole punch with ip: ${ip}, port: ${port} for newtId: ${newtId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const { session, newt: newtSession } =
|
||||||
|
await validateNewtSessionToken(token);
|
||||||
|
|
||||||
|
if (!session || !newtSession) {
|
||||||
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newtId !== newtSession.newtId) {
|
||||||
|
logger.warn(
|
||||||
|
`Newt ID mismatch: ${newtId} !== ${newtSession.newtId}`
|
||||||
|
);
|
||||||
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [newt] = await db
|
||||||
|
.select()
|
||||||
|
.from(newts)
|
||||||
|
.where(eq(newts.newtId, newtId));
|
||||||
|
|
||||||
|
if (!newt || !newt.siteId) {
|
||||||
|
logger.warn(`Newt not found: ${newtId}`);
|
||||||
|
throw new Error("Newt not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [site] = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.siteId, newt.siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (await checkExitNodeOrg(exitNode.exitNodeId, site.orgId)) {
|
||||||
|
// not allowed
|
||||||
|
logger.warn(
|
||||||
|
`Exit node ${exitNode.exitNodeId} is not allowed for org ${site.orgId}`
|
||||||
|
);
|
||||||
|
throw new Error("Exit node not allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSiteId = newt.siteId;
|
||||||
|
|
||||||
|
// Update the current site with the new endpoint
|
||||||
|
const [updatedSite] = await db
|
||||||
|
.update(sites)
|
||||||
|
.set({
|
||||||
|
endpoint: `${ip}:${port}`,
|
||||||
|
lastHolePunch: timestamp
|
||||||
|
})
|
||||||
|
.where(eq(sites.siteId, newt.siteId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!updatedSite || !updatedSite.subnet) {
|
||||||
|
logger.warn(`Site not found: ${newt.siteId}`);
|
||||||
|
throw new Error("Site not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all clients that connect to this site
|
||||||
|
// const sitesClientPairs = await db
|
||||||
|
// .select()
|
||||||
|
// .from(clientSites)
|
||||||
|
// .where(eq(clientSites.siteId, newt.siteId));
|
||||||
|
|
||||||
|
// THE NEWT IS NOT SENDING RAW WG TO THE GERBIL SO IDK IF WE REALLY NEED THIS - REMOVING
|
||||||
|
// Get client details for each client
|
||||||
|
// for (const pair of sitesClientPairs) {
|
||||||
|
// const [client] = await db
|
||||||
|
// .select()
|
||||||
|
// .from(clients)
|
||||||
|
// .where(eq(clients.clientId, pair.clientId));
|
||||||
|
|
||||||
|
// if (client && client.endpoint) {
|
||||||
|
// const [host, portStr] = client.endpoint.split(':');
|
||||||
|
// if (host && portStr) {
|
||||||
|
// destinations.push({
|
||||||
|
// destinationIP: host,
|
||||||
|
// destinationPort: parseInt(portStr, 10)
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// If this is a newt/site, also add other sites in the same org
|
||||||
|
// if (updatedSite.orgId) {
|
||||||
|
// const orgSites = await db
|
||||||
|
// .select()
|
||||||
|
// .from(sites)
|
||||||
|
// .where(eq(sites.orgId, updatedSite.orgId));
|
||||||
|
|
||||||
|
// for (const site of orgSites) {
|
||||||
|
// // Don't add the current site to the destinations
|
||||||
|
// if (site.siteId !== currentSiteId && site.subnet && site.endpoint && site.listenPort) {
|
||||||
|
// const [host, portStr] = site.endpoint.split(':');
|
||||||
|
// if (host && portStr) {
|
||||||
|
// destinations.push({
|
||||||
|
// destinationIP: host,
|
||||||
|
// destinationPort: site.listenPort
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
return destinations;
|
||||||
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export async function createOidcIdp(
|
|||||||
autoProvision
|
autoProvision
|
||||||
} = parsedBody.data;
|
} = parsedBody.data;
|
||||||
|
|
||||||
const key = config.getRawConfig().server.secret;
|
const key = config.getRawConfig().server.secret!;
|
||||||
|
|
||||||
const encryptedSecret = encrypt(clientSecret, key);
|
const encryptedSecret = encrypt(clientSecret, key);
|
||||||
const encryptedClientId = encrypt(clientId, key);
|
const encryptedClientId = encrypt(clientId, key);
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export async function generateOidcUrl(
|
|||||||
return scope.length > 0;
|
return scope.length > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const key = config.getRawConfig().server.secret;
|
const key = config.getRawConfig().server.secret!;
|
||||||
|
|
||||||
const decryptedClientId = decrypt(
|
const decryptedClientId = decrypt(
|
||||||
existingIdp.idpOidcConfig.clientId,
|
existingIdp.idpOidcConfig.clientId,
|
||||||
@@ -124,7 +124,7 @@ export async function generateOidcUrl(
|
|||||||
state,
|
state,
|
||||||
codeVerifier
|
codeVerifier
|
||||||
},
|
},
|
||||||
config.getRawConfig().server.secret
|
config.getRawConfig().server.secret!
|
||||||
);
|
);
|
||||||
|
|
||||||
res.cookie("p_oidc_state", stateJwt, {
|
res.cookie("p_oidc_state", stateJwt, {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export async function getIdp(
|
|||||||
return next(createHttpError(HttpCode.NOT_FOUND, "Idp not found"));
|
return next(createHttpError(HttpCode.NOT_FOUND, "Idp not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = config.getRawConfig().server.secret;
|
const key = config.getRawConfig().server.secret!;
|
||||||
|
|
||||||
if (idpRes.idp.type === "oidc") {
|
if (idpRes.idp.type === "oidc") {
|
||||||
const clientSecret = idpRes.idpOidcConfig!.clientSecret;
|
const clientSecret = idpRes.idpOidcConfig!.clientSecret;
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export async function updateOidcIdp(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = config.getRawConfig().server.secret;
|
const key = config.getRawConfig().server.secret!;
|
||||||
const encryptedSecret = clientSecret
|
const encryptedSecret = clientSecret
|
||||||
? encrypt(clientSecret, key)
|
? encrypt(clientSecret, key)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export async function validateOidcCallback(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = config.getRawConfig().server.secret;
|
const key = config.getRawConfig().server.secret!;
|
||||||
|
|
||||||
const decryptedClientId = decrypt(
|
const decryptedClientId = decrypt(
|
||||||
existingIdp.idpOidcConfig.clientId,
|
existingIdp.idpOidcConfig.clientId,
|
||||||
@@ -116,7 +116,7 @@ export async function validateOidcCallback(
|
|||||||
|
|
||||||
const statePayload = jsonwebtoken.verify(
|
const statePayload = jsonwebtoken.verify(
|
||||||
storedState,
|
storedState,
|
||||||
config.getRawConfig().server.secret,
|
config.getRawConfig().server.secret!,
|
||||||
function (err, decoded) {
|
function (err, decoded) {
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.error("Error verifying state JWT", { err });
|
logger.error("Error verifying state JWT", { err });
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import * as client from "./client";
|
|||||||
import * as accessToken from "./accessToken";
|
import * as accessToken from "./accessToken";
|
||||||
import * as apiKeys from "./apiKeys";
|
import * as apiKeys from "./apiKeys";
|
||||||
import * as idp from "./idp";
|
import * as idp from "./idp";
|
||||||
|
import * as siteResource from "./siteResource";
|
||||||
import {
|
import {
|
||||||
verifyApiKey,
|
verifyApiKey,
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyOrgAccess,
|
||||||
@@ -22,7 +23,8 @@ import {
|
|||||||
verifyApiKeyAccessTokenAccess,
|
verifyApiKeyAccessTokenAccess,
|
||||||
verifyApiKeyIsRoot,
|
verifyApiKeyIsRoot,
|
||||||
verifyApiKeyClientAccess,
|
verifyApiKeyClientAccess,
|
||||||
verifyClientsEnabled
|
verifyClientsEnabled,
|
||||||
|
verifyApiKeySiteResourceAccess
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
@@ -128,6 +130,69 @@ authenticated.delete(
|
|||||||
site.deleteSite
|
site.deleteSite
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/user-resources",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
resource.getUserResources
|
||||||
|
);
|
||||||
|
// Site Resource endpoints
|
||||||
|
authenticated.put(
|
||||||
|
"/org/:orgId/site/:siteId/resource",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeySiteAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.createSiteResource),
|
||||||
|
siteResource.createSiteResource
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/site/:siteId/resources",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeySiteAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.listSiteResources),
|
||||||
|
siteResource.listSiteResources
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/site-resources",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.listSiteResources),
|
||||||
|
siteResource.listAllSiteResourcesByOrg
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/site/:siteId/resource/:siteResourceId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeySiteAccess,
|
||||||
|
verifyApiKeySiteResourceAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.getSiteResource),
|
||||||
|
siteResource.getSiteResource
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/site/:siteId/resource/:siteResourceId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeySiteAccess,
|
||||||
|
verifyApiKeySiteResourceAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.updateSiteResource),
|
||||||
|
siteResource.updateSiteResource
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.delete(
|
||||||
|
"/org/:orgId/site/:siteId/resource/:siteResourceId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeySiteAccess,
|
||||||
|
verifyApiKeySiteResourceAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.deleteSiteResource),
|
||||||
|
siteResource.deleteSiteResource
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/org/:orgId/resource",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.createResource),
|
||||||
|
resource.createResource
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.put(
|
authenticated.put(
|
||||||
"/org/:orgId/site/:siteId/resource",
|
"/org/:orgId/site/:siteId/resource",
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyOrgAccess,
|
||||||
@@ -156,6 +221,13 @@ authenticated.get(
|
|||||||
domain.listDomains
|
domain.listDomains
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/invitations",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.listInvitations),
|
||||||
|
user.listInvitations
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/org/:orgId/create-invite",
|
"/org/:orgId/create-invite",
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyOrgAccess,
|
||||||
@@ -526,9 +598,9 @@ authenticated.get(
|
|||||||
);
|
);
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
"/org/:orgId/client/:clientId",
|
"/client/:clientId",
|
||||||
verifyClientsEnabled,
|
verifyClientsEnabled,
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyClientAccess,
|
||||||
verifyApiKeyHasAction(ActionsEnum.getClient),
|
verifyApiKeyHasAction(ActionsEnum.getClient),
|
||||||
client.getClient
|
client.getClient
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import * as auth from "@server/routers/auth";
|
|||||||
import * as supporterKey from "@server/routers/supporterKey";
|
import * as supporterKey from "@server/routers/supporterKey";
|
||||||
import * as license from "@server/routers/license";
|
import * as license from "@server/routers/license";
|
||||||
import * as idp from "@server/routers/idp";
|
import * as idp from "@server/routers/idp";
|
||||||
|
import { proxyToRemote } from "@server/lib/remoteProxy";
|
||||||
|
import config from "@server/lib/config";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import {
|
import {
|
||||||
verifyResourceAccess,
|
verifyResourceAccess,
|
||||||
@@ -49,16 +51,51 @@ internalRouter.get("/idp/:idpId", idp.getIdp);
|
|||||||
const gerbilRouter = Router();
|
const gerbilRouter = Router();
|
||||||
internalRouter.use("/gerbil", gerbilRouter);
|
internalRouter.use("/gerbil", gerbilRouter);
|
||||||
|
|
||||||
|
if (config.isManagedMode()) {
|
||||||
|
// Use proxy router to forward requests to remote cloud server
|
||||||
|
// Proxy endpoints for each gerbil route
|
||||||
|
gerbilRouter.post("/receive-bandwidth", (req, res, next) =>
|
||||||
|
proxyToRemote(req, res, next, "hybrid/gerbil/receive-bandwidth")
|
||||||
|
);
|
||||||
|
|
||||||
|
gerbilRouter.post("/update-hole-punch", (req, res, next) =>
|
||||||
|
proxyToRemote(req, res, next, "hybrid/gerbil/update-hole-punch")
|
||||||
|
);
|
||||||
|
|
||||||
|
gerbilRouter.post("/get-all-relays", (req, res, next) =>
|
||||||
|
proxyToRemote(req, res, next, "hybrid/gerbil/get-all-relays")
|
||||||
|
);
|
||||||
|
|
||||||
|
gerbilRouter.post("/get-resolved-hostname", (req, res, next) =>
|
||||||
|
proxyToRemote(req, res, next, `hybrid/gerbil/get-resolved-hostname`)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET CONFIG IS HANDLED IN THE ORIGINAL HANDLER
|
||||||
|
// SO IT CAN REGISTER THE LOCAL EXIT NODE
|
||||||
|
} else {
|
||||||
|
// Use local gerbil endpoints
|
||||||
|
gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth);
|
||||||
|
gerbilRouter.post("/update-hole-punch", gerbil.updateHolePunch);
|
||||||
|
gerbilRouter.post("/get-all-relays", gerbil.getAllRelays);
|
||||||
|
gerbilRouter.post("/get-resolved-hostname", gerbil.getResolvedHostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WE HANDLE THE PROXY INSIDE OF THIS FUNCTION
|
||||||
|
// SO IT REGISTERS THE EXIT NODE LOCALLY AS WELL
|
||||||
gerbilRouter.post("/get-config", gerbil.getConfig);
|
gerbilRouter.post("/get-config", gerbil.getConfig);
|
||||||
gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth);
|
|
||||||
gerbilRouter.post("/update-hole-punch", gerbil.updateHolePunch);
|
|
||||||
gerbilRouter.post("/get-all-relays", gerbil.getAllRelays);
|
|
||||||
|
|
||||||
// Badger routes
|
// Badger routes
|
||||||
const badgerRouter = Router();
|
const badgerRouter = Router();
|
||||||
internalRouter.use("/badger", badgerRouter);
|
internalRouter.use("/badger", badgerRouter);
|
||||||
|
|
||||||
badgerRouter.post("/verify-session", badger.verifyResourceSession);
|
badgerRouter.post("/verify-session", badger.verifyResourceSession);
|
||||||
badgerRouter.post("/exchange-session", badger.exchangeSession);
|
|
||||||
|
if (config.isManagedMode()) {
|
||||||
|
badgerRouter.post("/exchange-session", (req, res, next) =>
|
||||||
|
proxyToRemote(req, res, next, "hybrid/badger/exchange-session")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
badgerRouter.post("/exchange-session", badger.exchangeSession);
|
||||||
|
}
|
||||||
|
|
||||||
export default internalRouter;
|
export default internalRouter;
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ import {
|
|||||||
ExitNode,
|
ExitNode,
|
||||||
exitNodes,
|
exitNodes,
|
||||||
resources,
|
resources,
|
||||||
|
siteResources,
|
||||||
Target,
|
Target,
|
||||||
targets
|
targets
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { clients, clientSites, Newt, sites } from "@server/db";
|
import { clients, clientSites, Newt, sites } from "@server/db";
|
||||||
import { eq, and, inArray } from "drizzle-orm";
|
import { eq, and, inArray } from "drizzle-orm";
|
||||||
import { updatePeer } from "../olm/peers";
|
import { updatePeer } from "../olm/peers";
|
||||||
import axios from "axios";
|
import { sendToExitNode } from "../../lib/exitNodeComms";
|
||||||
|
|
||||||
const inputSchema = z.object({
|
const inputSchema = z.object({
|
||||||
publicKey: z.string(),
|
publicKey: z.string(),
|
||||||
@@ -102,41 +103,28 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
|
|||||||
.from(exitNodes)
|
.from(exitNodes)
|
||||||
.where(eq(exitNodes.exitNodeId, site.exitNodeId))
|
.where(eq(exitNodes.exitNodeId, site.exitNodeId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
if (exitNode.reachableAt && existingSite.subnet && existingSite.listenPort) {
|
if (
|
||||||
try {
|
exitNode.reachableAt &&
|
||||||
const response = await axios.post(
|
existingSite.subnet &&
|
||||||
`${exitNode.reachableAt}/update-proxy-mapping`,
|
existingSite.listenPort
|
||||||
{
|
) {
|
||||||
oldDestination: {
|
const payload = {
|
||||||
destinationIP: existingSite.subnet?.split("/")[0],
|
oldDestination: {
|
||||||
destinationPort: existingSite.listenPort
|
destinationIP: existingSite.subnet?.split("/")[0],
|
||||||
},
|
destinationPort: existingSite.listenPort
|
||||||
newDestination: {
|
},
|
||||||
destinationIP: site.subnet?.split("/")[0],
|
newDestination: {
|
||||||
destinationPort: site.listenPort
|
destinationIP: site.subnet?.split("/")[0],
|
||||||
}
|
destinationPort: site.listenPort
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info("Destinations updated:", {
|
|
||||||
peer: response.data.status
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error(
|
|
||||||
`Error updating proxy mapping (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.error(
|
|
||||||
`Error updating proxy mapping for exit node at ${exitNode.reachableAt}: ${error}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
await sendToExitNode(exitNode, {
|
||||||
|
remoteType: "remoteExitNode/update-proxy-mapping",
|
||||||
|
localPath: "/update-proxy-mapping",
|
||||||
|
method: "POST",
|
||||||
|
data: payload
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,33 +209,23 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
|
|||||||
const validPeers = peers.filter((peer) => peer !== null);
|
const validPeers = peers.filter((peer) => peer !== null);
|
||||||
|
|
||||||
// Get all enabled targets with their resource protocol information
|
// Get all enabled targets with their resource protocol information
|
||||||
const allTargets = await db
|
const allSiteResources = await db
|
||||||
.select({
|
.select()
|
||||||
resourceId: targets.resourceId,
|
.from(siteResources)
|
||||||
targetId: targets.targetId,
|
.where(eq(siteResources.siteId, siteId));
|
||||||
ip: targets.ip,
|
|
||||||
method: targets.method,
|
|
||||||
port: targets.port,
|
|
||||||
internalPort: targets.internalPort,
|
|
||||||
enabled: targets.enabled,
|
|
||||||
protocol: resources.protocol
|
|
||||||
})
|
|
||||||
.from(targets)
|
|
||||||
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
|
|
||||||
.where(and(eq(targets.siteId, siteId), eq(targets.enabled, true)));
|
|
||||||
|
|
||||||
const { tcpTargets, udpTargets } = allTargets.reduce(
|
const { tcpTargets, udpTargets } = allSiteResources.reduce(
|
||||||
(acc, target) => {
|
(acc, resource) => {
|
||||||
// Filter out invalid targets
|
// Filter out invalid targets
|
||||||
if (!target.internalPort || !target.ip || !target.port) {
|
if (!resource.proxyPort || !resource.destinationIp || !resource.destinationPort) {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format target into string
|
// Format target into string
|
||||||
const formattedTarget = `${target.internalPort}:${target.ip}:${target.port}`;
|
const formattedTarget = `${resource.proxyPort}:${resource.destinationIp}:${resource.destinationPort}`;
|
||||||
|
|
||||||
// Add to the appropriate protocol array
|
// Add to the appropriate protocol array
|
||||||
if (target.protocol === "tcp") {
|
if (resource.protocol === "tcp") {
|
||||||
acc.tcpTargets.push(formattedTarget);
|
acc.tcpTargets.push(formattedTarget);
|
||||||
} else {
|
} else {
|
||||||
acc.udpTargets.push(formattedTarget);
|
acc.udpTargets.push(formattedTarget);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { exitNodes, Newt } from "@server/db";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { ne, eq, or, and, count } from "drizzle-orm";
|
import { ne, eq, or, and, count } from "drizzle-orm";
|
||||||
|
import { listExitNodes } from "@server/lib/exitNodes";
|
||||||
|
|
||||||
export const handleNewtPingRequestMessage: MessageHandler = async (context) => {
|
export const handleNewtPingRequestMessage: MessageHandler = async (context) => {
|
||||||
const { message, client, sendToClient } = context;
|
const { message, client, sendToClient } = context;
|
||||||
@@ -16,12 +17,19 @@ export const handleNewtPingRequestMessage: MessageHandler = async (context) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: pick which nodes to send and ping better than just all of them
|
// Get the newt's orgId through the site relationship
|
||||||
let exitNodesList = await db
|
if (!newt.siteId) {
|
||||||
.select()
|
logger.warn("Newt siteId not found");
|
||||||
.from(exitNodes);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
exitNodesList = exitNodesList.filter((node) => node.maxConnections !== 0);
|
const [site] = await db
|
||||||
|
.select({ orgId: sites.orgId })
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.siteId, newt.siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
const exitNodesList = await listExitNodes(site.orgId, true); // filter for only the online ones
|
||||||
|
|
||||||
let lastExitNodeId = null;
|
let lastExitNodeId = null;
|
||||||
if (newt.siteId) {
|
if (newt.siteId) {
|
||||||
@@ -54,9 +62,9 @@ export const handleNewtPingRequestMessage: MessageHandler = async (context) => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (currentConnections.count >= maxConnections) {
|
if (currentConnections.count >= maxConnections) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
weight =
|
weight =
|
||||||
(maxConnections - currentConnections.count) /
|
(maxConnections - currentConnections.count) /
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
findNextAvailableCidr,
|
findNextAvailableCidr,
|
||||||
getNextAvailableClientSubnet
|
getNextAvailableClientSubnet
|
||||||
} from "@server/lib/ip";
|
} from "@server/lib/ip";
|
||||||
|
import { selectBestExitNode, verifyExitNodeOrgAccess } from "@server/lib/exitNodes";
|
||||||
|
|
||||||
export type ExitNodePingResult = {
|
export type ExitNodePingResult = {
|
||||||
exitNodeId: number;
|
exitNodeId: number;
|
||||||
@@ -24,7 +25,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
|||||||
const { message, client, sendToClient } = context;
|
const { message, client, sendToClient } = context;
|
||||||
const newt = client as Newt;
|
const newt = client as Newt;
|
||||||
|
|
||||||
logger.info("Handling register newt message!");
|
logger.debug("Handling register newt message!");
|
||||||
|
|
||||||
if (!newt) {
|
if (!newt) {
|
||||||
logger.warn("Newt not found");
|
logger.warn("Newt not found");
|
||||||
@@ -64,24 +65,14 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
|||||||
exitNodeId = bestPingResult.exitNodeId;
|
exitNodeId = bestPingResult.exitNodeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newtVersion) {
|
|
||||||
// update the newt version in the database
|
|
||||||
await db
|
|
||||||
.update(newts)
|
|
||||||
.set({
|
|
||||||
version: newtVersion as string
|
|
||||||
})
|
|
||||||
.where(eq(newts.newtId, newt.newtId));
|
|
||||||
}
|
|
||||||
|
|
||||||
const [oldSite] = await db
|
const [oldSite] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(eq(sites.siteId, siteId))
|
.where(eq(sites.siteId, siteId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!oldSite || !oldSite.exitNodeId) {
|
if (!oldSite) {
|
||||||
logger.warn("Site not found or does not have exit node");
|
logger.warn("Site not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +82,18 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
|||||||
// This effectively moves the exit node to the new one
|
// This effectively moves the exit node to the new one
|
||||||
exitNodeIdToQuery = exitNodeId; // Use the provided exitNodeId if it differs from the site's exitNodeId
|
exitNodeIdToQuery = exitNodeId; // Use the provided exitNodeId if it differs from the site's exitNodeId
|
||||||
|
|
||||||
|
const { exitNode, hasAccess } = await verifyExitNodeOrgAccess(exitNodeIdToQuery, oldSite.orgId);
|
||||||
|
|
||||||
|
if (!exitNode) {
|
||||||
|
logger.warn("Exit node not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasAccess) {
|
||||||
|
logger.warn("Not authorized to use this exit node");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const sitesQuery = await db
|
const sitesQuery = await db
|
||||||
.select({
|
.select({
|
||||||
subnet: sites.subnet
|
subnet: sites.subnet
|
||||||
@@ -98,12 +101,6 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
|||||||
.from(sites)
|
.from(sites)
|
||||||
.where(eq(sites.exitNodeId, exitNodeId));
|
.where(eq(sites.exitNodeId, exitNodeId));
|
||||||
|
|
||||||
const [exitNode] = await db
|
|
||||||
.select()
|
|
||||||
.from(exitNodes)
|
|
||||||
.where(eq(exitNodes.exitNodeId, exitNodeIdToQuery))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
const blockSize = config.getRawConfig().gerbil.site_block_size;
|
const blockSize = config.getRawConfig().gerbil.site_block_size;
|
||||||
const subnets = sitesQuery
|
const subnets = sitesQuery
|
||||||
.map((site) => site.subnet)
|
.map((site) => site.subnet)
|
||||||
@@ -140,13 +137,18 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
|||||||
.returning();
|
.returning();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!exitNodeIdToQuery) {
|
||||||
|
logger.warn("No exit node ID to query");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const [exitNode] = await db
|
const [exitNode] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(exitNodes)
|
.from(exitNodes)
|
||||||
.where(eq(exitNodes.exitNodeId, exitNodeIdToQuery))
|
.where(eq(exitNodes.exitNodeId, exitNodeIdToQuery))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (oldSite.pubKey && oldSite.pubKey !== publicKey) {
|
if (oldSite.pubKey && oldSite.pubKey !== publicKey && oldSite.exitNodeId) {
|
||||||
logger.info("Public key mismatch. Deleting old peer...");
|
logger.info("Public key mismatch. Deleting old peer...");
|
||||||
await deletePeer(oldSite.exitNodeId, oldSite.pubKey);
|
await deletePeer(oldSite.exitNodeId, oldSite.pubKey);
|
||||||
}
|
}
|
||||||
@@ -162,6 +164,16 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
|||||||
allowedIps: [siteSubnet]
|
allowedIps: [siteSubnet]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (newtVersion && newtVersion !== newt.version) {
|
||||||
|
// update the newt version in the database
|
||||||
|
await db
|
||||||
|
.update(newts)
|
||||||
|
.set({
|
||||||
|
version: newtVersion as string
|
||||||
|
})
|
||||||
|
.where(eq(newts.newtId, newt.newtId));
|
||||||
|
}
|
||||||
|
|
||||||
// Get all enabled targets with their resource protocol information
|
// Get all enabled targets with their resource protocol information
|
||||||
const allTargets = await db
|
const allTargets = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -218,14 +230,3 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
|||||||
excludeSender: false // Include sender in broadcast
|
excludeSender: false // Include sender in broadcast
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function selectBestExitNode(
|
|
||||||
pingResults: ExitNodePingResult[]
|
|
||||||
): ExitNodePingResult | null {
|
|
||||||
if (!pingResults || pingResults.length === 0) {
|
|
||||||
logger.warn("No ping results provided");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pingResults[0];
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { MessageHandler } from "../ws";
|
import { MessageHandler } from "../ws";
|
||||||
import { clients, Olm } from "@server/db";
|
import { clients, Olm } from "@server/db";
|
||||||
import { eq, lt, isNull } from "drizzle-orm";
|
import { eq, lt, isNull, and, or } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
|
||||||
// Track if the offline checker interval is running
|
// Track if the offline checker interval is running
|
||||||
@@ -13,22 +13,27 @@ const OFFLINE_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes
|
|||||||
* Starts the background interval that checks for clients that haven't pinged recently
|
* Starts the background interval that checks for clients that haven't pinged recently
|
||||||
* and marks them as offline
|
* and marks them as offline
|
||||||
*/
|
*/
|
||||||
export const startOfflineChecker = (): void => {
|
export const startOlmOfflineChecker = (): void => {
|
||||||
if (offlineCheckerInterval) {
|
if (offlineCheckerInterval) {
|
||||||
return; // Already running
|
return; // Already running
|
||||||
}
|
}
|
||||||
|
|
||||||
offlineCheckerInterval = setInterval(async () => {
|
offlineCheckerInterval = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const twoMinutesAgo = new Date(Date.now() - OFFLINE_THRESHOLD_MS);
|
const twoMinutesAgo = Math.floor((Date.now() - OFFLINE_THRESHOLD_MS) / 1000);
|
||||||
|
|
||||||
// Find clients that haven't pinged in the last 2 minutes and mark them as offline
|
// Find clients that haven't pinged in the last 2 minutes and mark them as offline
|
||||||
await db
|
await db
|
||||||
.update(clients)
|
.update(clients)
|
||||||
.set({ online: false })
|
.set({ online: false })
|
||||||
.where(
|
.where(
|
||||||
eq(clients.online, true) &&
|
and(
|
||||||
(lt(clients.lastPing, twoMinutesAgo.getTime() / 1000) || isNull(clients.lastPing))
|
eq(clients.online, true),
|
||||||
|
or(
|
||||||
|
lt(clients.lastPing, twoMinutesAgo),
|
||||||
|
isNull(clients.lastPing)
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -42,7 +47,7 @@ export const startOfflineChecker = (): void => {
|
|||||||
/**
|
/**
|
||||||
* Stops the background interval that checks for offline clients
|
* Stops the background interval that checks for offline clients
|
||||||
*/
|
*/
|
||||||
export const stopOfflineChecker = (): void => {
|
export const stopOlmOfflineChecker = (): void => {
|
||||||
if (offlineCheckerInterval) {
|
if (offlineCheckerInterval) {
|
||||||
clearInterval(offlineCheckerInterval);
|
clearInterval(offlineCheckerInterval);
|
||||||
offlineCheckerInterval = null;
|
offlineCheckerInterval = null;
|
||||||
@@ -72,7 +77,7 @@ export const handleOlmPingMessage: MessageHandler = async (context) => {
|
|||||||
await db
|
await db
|
||||||
.update(clients)
|
.update(clients)
|
||||||
.set({
|
.set({
|
||||||
lastPing: new Date().getTime() / 1000,
|
lastPing: Math.floor(Date.now() / 1000),
|
||||||
online: true,
|
online: true,
|
||||||
})
|
})
|
||||||
.where(eq(clients.clientId, olm.clientId));
|
.where(eq(clients.clientId, olm.clientId));
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user