mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-18 10:56:38 +00:00
Compare commits
346 Commits
1.12.2-s.1
...
clients-us
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3b852ef45 | ||
|
|
53bb4efbb2 | ||
|
|
96dbec9352 | ||
|
|
2d3fbb9704 | ||
|
|
d3be1fbf4c | ||
|
|
89ee57cdf9 | ||
|
|
bdfc7fbcdb | ||
|
|
8726a7f931 | ||
|
|
1cae815be5 | ||
|
|
c5befee134 | ||
|
|
9cf2dbc2cc | ||
|
|
35f9c67cfe | ||
|
|
6707b3c7fe | ||
|
|
dfb85f2c89 | ||
|
|
17dec6cf0b | ||
|
|
8ee4ee7baf | ||
|
|
b1b0702886 | ||
|
|
92aed108cd | ||
|
|
2dcc94cd14 | ||
|
|
a7185ff913 | ||
|
|
04e73515b8 | ||
|
|
2bad9daaea | ||
|
|
54670e150d | ||
|
|
761ed1de9a | ||
|
|
078692c818 | ||
|
|
53ab51691a | ||
|
|
54e2d95b55 | ||
|
|
6e6fa77625 | ||
|
|
5c0c12cabe | ||
|
|
b3ed7c0129 | ||
|
|
10a00ff225 | ||
|
|
ba09479827 | ||
|
|
1c5c36fc12 | ||
|
|
d37ff6e15b | ||
|
|
9288575341 | ||
|
|
0ceed4c812 | ||
|
|
4b61a38501 | ||
|
|
ca9273c9ea | ||
|
|
810704e190 | ||
|
|
f33be1434b | ||
|
|
82a9f2b24f | ||
|
|
7204b5f0de | ||
|
|
9b372780bd | ||
|
|
9065385b87 | ||
|
|
77306e8c97 | ||
|
|
a746ef36a8 | ||
|
|
6e565f1331 | ||
|
|
84c608c2cf | ||
|
|
6da7f58ced | ||
|
|
351097b04d | ||
|
|
bd3d339905 | ||
|
|
c6ad36d78e | ||
|
|
eaeb65e9b4 | ||
|
|
4176bdbc81 | ||
|
|
a2cdd8484c | ||
|
|
23ab76ae08 | ||
|
|
8eec122114 | ||
|
|
79ccbc8e92 | ||
|
|
d70da2aa70 | ||
|
|
c695f50122 | ||
|
|
1b09e5b9f9 | ||
|
|
7efc947e26 | ||
|
|
4b580105cd | ||
|
|
a61c82570a | ||
|
|
6734003d85 | ||
|
|
e49d796b06 | ||
|
|
4ab4029625 | ||
|
|
5afff3c662 | ||
|
|
9be5a01173 | ||
|
|
357f297a3e | ||
|
|
e1edbe6067 | ||
|
|
5a859aad29 | ||
|
|
a28b15a81d | ||
|
|
e62186f395 | ||
|
|
11c1efc19c | ||
|
|
8b0491eb52 | ||
|
|
0032634004 | ||
|
|
4af10c8108 | ||
|
|
56cb685813 | ||
|
|
ccfe1f7d0a | ||
|
|
bf987d867c | ||
|
|
3870ced635 | ||
|
|
cb3861a5c8 | ||
|
|
f5bfddd262 | ||
|
|
f060063f53 | ||
|
|
6eb6b44f41 | ||
|
|
c93ab34021 | ||
|
|
06a31bb716 | ||
|
|
152fb47ca4 | ||
|
|
3d400b2321 | ||
|
|
45a82f3ecc | ||
|
|
342bedc012 | ||
|
|
18db4a11c8 | ||
|
|
a7e32d4013 | ||
|
|
beea28daf3 | ||
|
|
b5e94d44ae | ||
|
|
a623604e96 | ||
|
|
8c62dfa706 | ||
|
|
610e46f2d5 | ||
|
|
92125611e9 | ||
|
|
096da391e5 | ||
|
|
dd6b1d88d3 | ||
|
|
79f0d60533 | ||
|
|
67665864c2 | ||
|
|
c4de617751 | ||
|
|
19e3c5045e | ||
|
|
9f63d8bb5b | ||
|
|
49348c6ab7 | ||
|
|
0961ac1da1 | ||
|
|
6a79436516 | ||
|
|
85b46392e1 | ||
|
|
f721c983aa | ||
|
|
ff0b30fc2e | ||
|
|
18070a37a8 | ||
|
|
5bd31f87f0 | ||
|
|
de83cf9d8c | ||
|
|
ceae787cf5 | ||
|
|
ce6afd0019 | ||
|
|
d977d57b2a | ||
|
|
7bcd6adf01 | ||
|
|
ac68dbd545 | ||
|
|
d450e2c3ab | ||
|
|
9440a4f879 | ||
|
|
73b0411e1c | ||
|
|
6368b9d837 | ||
|
|
1b643fb4b6 | ||
|
|
d118c6b666 | ||
|
|
380e062d25 | ||
|
|
261f0333b8 | ||
|
|
24adca6108 | ||
|
|
3f440f0f7a | ||
|
|
ba6defa87c | ||
|
|
887a0ef574 | ||
|
|
200743747d | ||
|
|
2082c5eed2 | ||
|
|
a42d012788 | ||
|
|
82cc51424b | ||
|
|
7924f195aa | ||
|
|
d41bd3023f | ||
|
|
87a0dd2d12 | ||
|
|
5fd64596eb | ||
|
|
d23f61d995 | ||
|
|
7ac27b3883 | ||
|
|
9420b41e39 | ||
|
|
2cfb0e05cf | ||
|
|
5b9386b18a | ||
|
|
f5c3dff43c | ||
|
|
eeb82c8cfe | ||
|
|
3750c36aa7 | ||
|
|
3801354ae6 | ||
|
|
266fbb1762 | ||
|
|
5d1f81a92c | ||
|
|
d6e8eb5307 | ||
|
|
2bc82f49ed | ||
|
|
487985558d | ||
|
|
dc237b8052 | ||
|
|
4ed4515262 | ||
|
|
cd76fa0139 | ||
|
|
af4b9e83f7 | ||
|
|
fa5facdf33 | ||
|
|
937b36e756 | ||
|
|
e90bdf8f97 | ||
|
|
56491cc17b | ||
|
|
6da531e99b | ||
|
|
01b5158b73 | ||
|
|
8f9b665bef | ||
|
|
806949879a | ||
|
|
e72e2b53aa | ||
|
|
10f42fe2e6 | ||
|
|
51b438117a | ||
|
|
d73825dd24 | ||
|
|
b5c6191c67 | ||
|
|
97c707248e | ||
|
|
02fbc279b5 | ||
|
|
80a68507cd | ||
|
|
dbb1e37033 | ||
|
|
364b84359e | ||
|
|
93d4a40977 | ||
|
|
97312343e4 | ||
|
|
1736ad486a | ||
|
|
a07ad843a2 | ||
|
|
fef9101058 | ||
|
|
2890ff2605 | ||
|
|
026ad2ccb9 | ||
|
|
a82969b778 | ||
|
|
612b04c26f | ||
|
|
2162f5f76f | ||
|
|
710f16ce68 | ||
|
|
61a4f468ba | ||
|
|
b00fea5656 | ||
|
|
269ff630aa | ||
|
|
986f7121bd | ||
|
|
21f0501bc6 | ||
|
|
2b31dd955c | ||
|
|
e7aeb4ff89 | ||
|
|
9dd1192033 | ||
|
|
e61da0958f | ||
|
|
fce588057e | ||
|
|
33331fd3c8 | ||
|
|
1261ad3a00 | ||
|
|
7dcf4d5192 | ||
|
|
dc87df5d38 | ||
|
|
5d2f65daa9 | ||
|
|
58cf471bc4 | ||
|
|
7db99a7dd5 | ||
|
|
000904eb31 | ||
|
|
6d1713b6b9 | ||
|
|
de8262d7b9 | ||
|
|
4f026acad8 | ||
|
|
5b31bbce8d | ||
|
|
e6e80f6fc7 | ||
|
|
bde4492d49 | ||
|
|
7c728c144c | ||
|
|
8ad7bcc0d6 | ||
|
|
e62806d6fb | ||
|
|
4e0a2e441b | ||
|
|
aabe39137b | ||
|
|
d9564ed6fe | ||
|
|
0798a0c6c2 | ||
|
|
c9786946b7 | ||
|
|
9344ab3546 | ||
|
|
1a4078b8a1 | ||
|
|
8674ca931b | ||
|
|
08c82e072e | ||
|
|
23c9827e4c | ||
|
|
864b587b89 | ||
|
|
ca89aa7ce8 | ||
|
|
63a1ecfb86 | ||
|
|
fbce392137 | ||
|
|
c004e969cb | ||
|
|
c6611471b1 | ||
|
|
bdf1625976 | ||
|
|
0a5dc17800 | ||
|
|
fa7aa508ea | ||
|
|
2973b61676 | ||
|
|
2428413442 | ||
|
|
5602d8ee64 | ||
|
|
a70799c8c0 | ||
|
|
693c9fbe0f | ||
|
|
10f8298161 | ||
|
|
f98b4baa73 | ||
|
|
0af51cebbe | ||
|
|
abc5f8ec68 | ||
|
|
ddc14d164e | ||
|
|
aeda85fcfb | ||
|
|
66124f09c4 | ||
|
|
ac5fe1486a | ||
|
|
50ac52d316 | ||
|
|
f85d9f8b6e | ||
|
|
feb0bd58c8 | ||
|
|
32949127d2 | ||
|
|
84d24d9bf5 | ||
|
|
8e1bb6a6fd | ||
|
|
cad4d97fb3 | ||
|
|
de53cfb912 | ||
|
|
55fd276773 | ||
|
|
7125b49024 | ||
|
|
fb9ed8f592 | ||
|
|
020cb2d794 | ||
|
|
9b2c0d0b67 | ||
|
|
3993e5b705 | ||
|
|
47bcadb329 | ||
|
|
00df2c876f | ||
|
|
b4535f3dc4 | ||
|
|
e51fca1f61 | ||
|
|
0e7f5b1aef | ||
|
|
579a4e1021 | ||
|
|
c813202f92 | ||
|
|
94e1c534ca | ||
|
|
41e21acf42 | ||
|
|
b6e98632b5 | ||
|
|
8a5f59cb9f | ||
|
|
0745734273 | ||
|
|
aa3f07f1ba | ||
|
|
2b8204fdc8 | ||
|
|
90e72c6aca | ||
|
|
62e2b7ca9e | ||
|
|
f7e7993fd4 | ||
|
|
18cdf070c7 | ||
|
|
563a5b3e7e | ||
|
|
3756aaecda | ||
|
|
58a13de0ff | ||
|
|
d32505a833 | ||
|
|
42091e88cb | ||
|
|
c2f607bb9a | ||
|
|
3f38080b46 | ||
|
|
9f9aa07c2d | ||
|
|
76d54b2d0f | ||
|
|
bdb564823d | ||
|
|
b3a616c9f3 | ||
|
|
ec1f94791a | ||
|
|
bea1c65076 | ||
|
|
2274a3525b | ||
|
|
999fb2fff1 | ||
|
|
f27ae210ed | ||
|
|
ea744f8d28 | ||
|
|
0b70cbb1a3 | ||
|
|
f928708156 | ||
|
|
fae899a8f1 | ||
|
|
45fb0a4156 | ||
|
|
a62299c387 | ||
|
|
18757d7eb3 | ||
|
|
776c33d79d | ||
|
|
9fd6af3a31 | ||
|
|
4ade878320 | ||
|
|
9e2477587c | ||
|
|
c7787352c8 | ||
|
|
85892c30b2 | ||
|
|
7a2dd31019 | ||
|
|
096ca379ce | ||
|
|
41601010f4 | ||
|
|
64b87e203a | ||
|
|
c64b102aaa | ||
|
|
f371c7df81 | ||
|
|
030f90db2e | ||
|
|
e51b6b545e | ||
|
|
c73f8c88f7 | ||
|
|
2274404324 | ||
|
|
6d349693a7 | ||
|
|
b9ce316574 | ||
|
|
a247ef7564 | ||
|
|
18566c09dc | ||
|
|
1090dca634 | ||
|
|
44f419d4f7 | ||
|
|
162c6d567c | ||
|
|
2f1abfbef8 | ||
|
|
a26a441d56 | ||
|
|
f628a76223 | ||
|
|
8088e30e06 | ||
|
|
801cdec7f3 | ||
|
|
3fd3f9871d | ||
|
|
959a562e7c | ||
|
|
03e0e8d9c2 | ||
|
|
52a311bf36 | ||
|
|
83e0282212 | ||
|
|
dc75d72522 | ||
|
|
6da81b3817 | ||
|
|
847479b639 | ||
|
|
0790f37f5e | ||
|
|
9dd472c59b | ||
|
|
5746d69f98 | ||
|
|
8356c5933f | ||
|
|
2c488baa80 | ||
|
|
d30743a428 | ||
|
|
009d84a3c6 | ||
|
|
e888b76747 |
2
.github/workflows/cicd.yml
vendored
2
.github/workflows/cicd.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
|||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -49,4 +49,5 @@ postgres/
|
|||||||
dynamic/
|
dynamic/
|
||||||
*.mmdb
|
*.mmdb
|
||||||
scratch/
|
scratch/
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
|
hydrateSaas.ts
|
||||||
16
Dockerfile
16
Dockerfile
@@ -1,10 +1,12 @@
|
|||||||
FROM node:22-alpine AS builder
|
FROM node:25-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ARG BUILD=oss
|
ARG BUILD=oss
|
||||||
ARG DATABASE=sqlite
|
ARG DATABASE=sqlite
|
||||||
|
|
||||||
|
RUN apk add --no-cache curl tzdata python3 make g++
|
||||||
|
|
||||||
# COPY package.json package-lock.json ./
|
# COPY package.json package-lock.json ./
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
@@ -12,8 +14,9 @@ RUN npm ci
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts
|
RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts
|
||||||
|
RUN echo "export const driver: \"pg\" | \"sqlite\" = \"$DATABASE\";" >> server/db/index.ts
|
||||||
|
|
||||||
RUN echo "export const build = \"$BUILD\" as any;" > server/build.ts
|
RUN echo "export const build = \"$BUILD\" as \"saas\" | \"enterprise\" | \"oss\";" > server/build.ts
|
||||||
|
|
||||||
# Copy the appropriate TypeScript configuration based on build type
|
# Copy the appropriate TypeScript configuration based on build type
|
||||||
RUN if [ "$BUILD" = "oss" ]; then cp tsconfig.oss.json tsconfig.json; \
|
RUN if [ "$BUILD" = "oss" ]; then cp tsconfig.oss.json tsconfig.json; \
|
||||||
@@ -30,9 +33,9 @@ RUN mkdir -p dist
|
|||||||
RUN npm run next:build
|
RUN npm run next:build
|
||||||
RUN node esbuild.mjs -e server/index.ts -o dist/server.mjs -b $BUILD
|
RUN node esbuild.mjs -e server/index.ts -o dist/server.mjs -b $BUILD
|
||||||
RUN if [ "$DATABASE" = "pg" ]; then \
|
RUN if [ "$DATABASE" = "pg" ]; then \
|
||||||
node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs; \
|
node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs; \
|
||||||
else \
|
else \
|
||||||
node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs; \
|
node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# test to make sure the build output is there and error if not
|
# test to make sure the build output is there and error if not
|
||||||
@@ -40,12 +43,13 @@ RUN test -f dist/server.mjs
|
|||||||
|
|
||||||
RUN npm run build:cli
|
RUN npm run build:cli
|
||||||
|
|
||||||
FROM node:22-alpine AS runner
|
FROM node:25-alpine AS runner
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Curl used for the health checks
|
# Curl used for the health checks
|
||||||
RUN apk add --no-cache curl tzdata
|
# Python and build tools needed for better-sqlite3 native compilation
|
||||||
|
RUN apk add --no-cache curl tzdata python3 make g++
|
||||||
|
|
||||||
# COPY package.json package-lock.json ./
|
# COPY package.json package-lock.json ./
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ proxy-resources:
|
|||||||
# - owen@pangolin.net
|
# - owen@pangolin.net
|
||||||
# whitelist-users:
|
# whitelist-users:
|
||||||
# - owen@pangolin.net
|
# - owen@pangolin.net
|
||||||
|
# auto-login-idp: 1
|
||||||
headers:
|
headers:
|
||||||
- name: X-Example-Header
|
- name: X-Example-Header
|
||||||
value: example-value
|
value: example-value
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
url: http://localhost:4000/api/v1/auth/login
|
url: http://localhost:3000/api/v1/auth/login
|
||||||
body: json
|
body: json
|
||||||
auth: none
|
auth: none
|
||||||
}
|
}
|
||||||
|
|
||||||
body:json {
|
body:json {
|
||||||
{
|
{
|
||||||
"email": "owen@pangolin.net",
|
"email": "admin@fosrl.io",
|
||||||
"password": "Password123!"
|
"password": "Password123!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
bruno/Olm/createOlm.bru
Normal file
15
bruno/Olm/createOlm.bru
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
meta {
|
||||||
|
name: createOlm
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
put {
|
||||||
|
url: http://localhost:3000/api/v1/olm
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
}
|
||||||
8
bruno/Olm/folder.bru
Normal file
8
bruno/Olm/folder.bru
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
meta {
|
||||||
|
name: Olm
|
||||||
|
seq: 15
|
||||||
|
}
|
||||||
|
|
||||||
|
auth {
|
||||||
|
mode: inherit
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "1",
|
"version": "1",
|
||||||
"name": "Pangolin Saas",
|
"name": "Pangolin",
|
||||||
"type": "collection",
|
"type": "collection",
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
|
|||||||
@@ -25,4 +25,3 @@ flags:
|
|||||||
disable_user_create_org: true
|
disable_user_create_org: true
|
||||||
allow_raw_resources: true
|
allow_raw_resources: true
|
||||||
enable_integration_api: true
|
enable_integration_api: true
|
||||||
enable_clients: true
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ services:
|
|||||||
- 80:80 # Port for traefik because of the network_mode
|
- 80:80 # Port for traefik because of the network_mode
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.5
|
image: traefik:v3.6
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: service:gerbil # Ports appear on the gerbil service
|
network_mode: service:gerbil # Ports appear on the gerbil service
|
||||||
@@ -52,4 +52,4 @@ networks:
|
|||||||
default:
|
default:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
name: pangolin
|
name: pangolin
|
||||||
enable_ipv6: true
|
enable_ipv6: true
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ services:
|
|||||||
- 80:80
|
- 80:80
|
||||||
{{end}}
|
{{end}}
|
||||||
traefik:
|
traefik:
|
||||||
image: docker.io/traefik:v3.5
|
image: docker.io/traefik:v3.6
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
{{if .InstallGerbil}}
|
{{if .InstallGerbil}}
|
||||||
@@ -59,4 +59,4 @@ networks:
|
|||||||
default:
|
default:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
name: pangolin
|
name: pangolin
|
||||||
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ module installer
|
|||||||
go 1.24.0
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/term v0.36.0
|
golang.org/x/term v0.37.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/sys v0.37.0 // indirect
|
require golang.org/x/sys v0.38.0 // indirect
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 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=
|
||||||
|
|||||||
@@ -238,7 +238,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("CrowdSec installed successfully!")
|
fmt.Println("CrowdSec installed successfully!")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,15 @@
|
|||||||
"settingsErrorUpdateDescription": "Възникна грешка при обновяване на настройките",
|
"settingsErrorUpdateDescription": "Възникна грешка при обновяване на настройките",
|
||||||
"sidebarCollapse": "Свиване",
|
"sidebarCollapse": "Свиване",
|
||||||
"sidebarExpand": "Разширяване",
|
"sidebarExpand": "Разширяване",
|
||||||
|
"productUpdateMoreInfo": "{noOfUpdates} още актуализации",
|
||||||
|
"productUpdateInfo": "{noOfUpdates} актуализации",
|
||||||
|
"productUpdateWhatsNew": "Какво ново",
|
||||||
|
"productUpdateTitle": "Актуализации на продукта",
|
||||||
|
"productUpdateEmpty": "Няма актуализации",
|
||||||
|
"dismissAll": "Отхвърляне на всички",
|
||||||
|
"pangolinUpdateAvailable": "Налична е нова версия",
|
||||||
|
"pangolinUpdateAvailableInfo": "Версия {version} е готова за инсталиране",
|
||||||
|
"pangolinUpdateAvailableReleaseNotes": "Преглед на бележките за издание",
|
||||||
"newtUpdateAvailable": "Ново обновление",
|
"newtUpdateAvailable": "Ново обновление",
|
||||||
"newtUpdateAvailableInfo": "Нова версия на Newt е налична. Моля, обновете до последната версия за най-добро изживяване.",
|
"newtUpdateAvailableInfo": "Нова версия на Newt е налична. Моля, обновете до последната версия за най-добро изживяване.",
|
||||||
"domainPickerEnterDomain": "Домейн",
|
"domainPickerEnterDomain": "Домейн",
|
||||||
@@ -1525,6 +1534,12 @@
|
|||||||
"resourcesTableTheseResourcesForUseWith": "Тези ресурси са за използване с",
|
"resourcesTableTheseResourcesForUseWith": "Тези ресурси са за използване с",
|
||||||
"resourcesTableClients": "Клиенти",
|
"resourcesTableClients": "Клиенти",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "и са достъпни само вътрешно при свързване с клиент.",
|
"resourcesTableAndOnlyAccessibleInternally": "и са достъпни само вътрешно при свързване с клиент.",
|
||||||
|
"resourcesTableNoTargets": "Без цели",
|
||||||
|
"resourcesTableHealthy": "Здрав",
|
||||||
|
"resourcesTableDegraded": "Влошен",
|
||||||
|
"resourcesTableOffline": "Извън линия",
|
||||||
|
"resourcesTableUnknown": "Неизвестно",
|
||||||
|
"resourcesTableNotMonitored": "Не е наблюдавано",
|
||||||
"editInternalResourceDialogEditClientResource": "Редактиране на клиентски ресурс",
|
"editInternalResourceDialogEditClientResource": "Редактиране на клиентски ресурс",
|
||||||
"editInternalResourceDialogUpdateResourceProperties": "Актуализирайте свойствата на ресурса и конфигурацията на целите за {resourceName}.",
|
"editInternalResourceDialogUpdateResourceProperties": "Актуализирайте свойствата на ресурса и конфигурацията на целите за {resourceName}.",
|
||||||
"editInternalResourceDialogResourceProperties": "Свойствата на ресурса",
|
"editInternalResourceDialogResourceProperties": "Свойствата на ресурса",
|
||||||
@@ -2095,5 +2110,31 @@
|
|||||||
"selectedResources": "Избрани ресурси",
|
"selectedResources": "Избрани ресурси",
|
||||||
"enableSelected": "Разреши избраните",
|
"enableSelected": "Разреши избраните",
|
||||||
"disableSelected": "Забрани избраните",
|
"disableSelected": "Забрани избраните",
|
||||||
"checkSelectedStatus": "Проверете състоянието на избраните"
|
"checkSelectedStatus": "Проверете състоянието на избраните",
|
||||||
}
|
"credentials": "Удостоверения",
|
||||||
|
"savecredentials": "Запазване на удостоверения",
|
||||||
|
"regeneratecredentials": "Прегенериране",
|
||||||
|
"regenerateCredentials": "Прегенериране и запазване на удостоверенията ви",
|
||||||
|
"generatedcredentials": "Прегенерирани удостоверения",
|
||||||
|
"copyandsavethesecredentials": "Копирайте и запазете тези удостоверения",
|
||||||
|
"copyandsavethesecredentialsdescription": "Тези удостоверения няма да бъдат показани отново след като напуснете тази страница. Запазете ги сигурно сега.",
|
||||||
|
"credentialsSaved": "Удостоверенията са запазени",
|
||||||
|
"credentialsSavedDescription": "Удостоверенията бяха прегенерирани и успешно запазени.",
|
||||||
|
"credentialsSaveError": "Грешка при запазването на удостоверенията",
|
||||||
|
"credentialsSaveErrorDescription": "Възникна грешка при прегенерирането и запазването на удостоверенията.",
|
||||||
|
"regenerateCredentialsWarning": "Прегенерирането на удостоверения ще анулира предишните. Уверете се, че актуализирате всички конфигурации, които използват тези удостоверения.",
|
||||||
|
"confirm": "Потвърждаване",
|
||||||
|
"regenerateCredentialsConfirmation": "Сигурни ли сте, че искате да прегенерирате удостоверенията?",
|
||||||
|
"endpoint": "Крайна точка",
|
||||||
|
"Id": "Идентификатор",
|
||||||
|
"SecretKey": "Таен ключ",
|
||||||
|
"featureDisabledTooltip": "Тази функция е налична само в корпоративния пакет и изисква лиценз за използване.",
|
||||||
|
"niceId": "Красив ID",
|
||||||
|
"niceIdUpdated": "Красив ID е обновен",
|
||||||
|
"niceIdUpdatedSuccessfully": "Красив ID е успешно обновен",
|
||||||
|
"niceIdUpdateError": "Грешка при обновяването на Красив ID",
|
||||||
|
"niceIdUpdateErrorDescription": "Възникна грешка при обновяването на Красив ID.",
|
||||||
|
"niceIdCannotBeEmpty": "Красив ID не може да бъде празен",
|
||||||
|
"enterIdentifier": "Въведете идентификатор",
|
||||||
|
"identifier": "Идентификатор"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,15 @@
|
|||||||
"settingsErrorUpdateDescription": "Došlo k chybě při aktualizaci nastavení",
|
"settingsErrorUpdateDescription": "Došlo k chybě při aktualizaci nastavení",
|
||||||
"sidebarCollapse": "Sbalit",
|
"sidebarCollapse": "Sbalit",
|
||||||
"sidebarExpand": "Rozbalit",
|
"sidebarExpand": "Rozbalit",
|
||||||
|
"productUpdateMoreInfo": "{noOfUpdates} další aktualizace",
|
||||||
|
"productUpdateInfo": "Aktualizace {noOfUpdates}",
|
||||||
|
"productUpdateWhatsNew": "Co je nového",
|
||||||
|
"productUpdateTitle": "Aktualizace produktu",
|
||||||
|
"productUpdateEmpty": "Žádné aktualizace",
|
||||||
|
"dismissAll": "Odmítnout vše",
|
||||||
|
"pangolinUpdateAvailable": "K dispozici je nová verze",
|
||||||
|
"pangolinUpdateAvailableInfo": "Verze {version} je připravena k instalaci",
|
||||||
|
"pangolinUpdateAvailableReleaseNotes": "Zobrazit poznámky k vydání",
|
||||||
"newtUpdateAvailable": "Dostupná aktualizace",
|
"newtUpdateAvailable": "Dostupná aktualizace",
|
||||||
"newtUpdateAvailableInfo": "Je k dispozici nová verze Newt. Pro nejlepší zážitek prosím aktualizujte na nejnovější verzi.",
|
"newtUpdateAvailableInfo": "Je k dispozici nová verze Newt. Pro nejlepší zážitek prosím aktualizujte na nejnovější verzi.",
|
||||||
"domainPickerEnterDomain": "Doména",
|
"domainPickerEnterDomain": "Doména",
|
||||||
@@ -1525,6 +1534,12 @@
|
|||||||
"resourcesTableTheseResourcesForUseWith": "Tyto zdroje jsou určeny pro použití s",
|
"resourcesTableTheseResourcesForUseWith": "Tyto zdroje jsou určeny pro použití s",
|
||||||
"resourcesTableClients": "Klienti",
|
"resourcesTableClients": "Klienti",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "a jsou interně přístupné pouze v případě, že jsou propojeni s klientem.",
|
"resourcesTableAndOnlyAccessibleInternally": "a jsou interně přístupné pouze v případě, že jsou propojeni s klientem.",
|
||||||
|
"resourcesTableNoTargets": "Žádné cíle",
|
||||||
|
"resourcesTableHealthy": "Zdravé",
|
||||||
|
"resourcesTableDegraded": "Rozklad",
|
||||||
|
"resourcesTableOffline": "Offline",
|
||||||
|
"resourcesTableUnknown": "Neznámý",
|
||||||
|
"resourcesTableNotMonitored": "Není sledováno",
|
||||||
"editInternalResourceDialogEditClientResource": "Upravit klientský dokument",
|
"editInternalResourceDialogEditClientResource": "Upravit klientský dokument",
|
||||||
"editInternalResourceDialogUpdateResourceProperties": "Aktualizujte vlastnosti zdroje a cílovou konfiguraci pro {resourceName}.",
|
"editInternalResourceDialogUpdateResourceProperties": "Aktualizujte vlastnosti zdroje a cílovou konfiguraci pro {resourceName}.",
|
||||||
"editInternalResourceDialogResourceProperties": "Vlastnosti zdroje",
|
"editInternalResourceDialogResourceProperties": "Vlastnosti zdroje",
|
||||||
@@ -2095,5 +2110,31 @@
|
|||||||
"selectedResources": "Vybrané zdroje",
|
"selectedResources": "Vybrané zdroje",
|
||||||
"enableSelected": "Povolit vybrané",
|
"enableSelected": "Povolit vybrané",
|
||||||
"disableSelected": "Zakázat vybrané",
|
"disableSelected": "Zakázat vybrané",
|
||||||
"checkSelectedStatus": "Zkontrolovat stav vybraného"
|
"checkSelectedStatus": "Zkontrolovat stav vybraného",
|
||||||
}
|
"credentials": "Pověření",
|
||||||
|
"savecredentials": "Uložit přihlašovací údaje",
|
||||||
|
"regeneratecredentials": "Znovu klíče",
|
||||||
|
"regenerateCredentials": "Regenerovat a uložit vaše přihlašovací údaje",
|
||||||
|
"generatedcredentials": "Vygenerovaná pověření",
|
||||||
|
"copyandsavethesecredentials": "Zkopírovat a ukládat tato pověření",
|
||||||
|
"copyandsavethesecredentialsdescription": "Tyto přihlašovací údaje se znovu nezobrazí po opuštění této stránky. Uložte je bezpečně.",
|
||||||
|
"credentialsSaved": "Pověření uloženo",
|
||||||
|
"credentialsSavedDescription": "Pověření byla úspěšně obnovena a uložena.",
|
||||||
|
"credentialsSaveError": "Chyba při ukládání pověření",
|
||||||
|
"credentialsSaveErrorDescription": "Došlo k chybě při obnovování a ukládání přihlašovacích údajů.",
|
||||||
|
"regenerateCredentialsWarning": "Obnovení přihlašovacích údajů zneplatní ty předchozí. Ujistěte se, že aktualizujete všechny konfigurace, které tyto přihlašovací údaje používají.",
|
||||||
|
"confirm": "Potvrdit",
|
||||||
|
"regenerateCredentialsConfirmation": "Jste si jisti, že chcete obnovit přihlašovací údaje?",
|
||||||
|
"endpoint": "Endpoint",
|
||||||
|
"Id": "Id",
|
||||||
|
"SecretKey": "Tajný klíč",
|
||||||
|
"featureDisabledTooltip": "Tato funkce je dostupná pouze v podnikovém plánu a vyžaduje její používání.",
|
||||||
|
"niceId": "Pěkné ID",
|
||||||
|
"niceIdUpdated": "Nice ID aktualizováno",
|
||||||
|
"niceIdUpdatedSuccessfully": "Nice ID úspěšně aktualizováno",
|
||||||
|
"niceIdUpdateError": "Chyba při aktualizaci Nice ID",
|
||||||
|
"niceIdUpdateErrorDescription": "Došlo k chybě při aktualizaci identifikátoru Nice.",
|
||||||
|
"niceIdCannotBeEmpty": "Nice ID nemůže být prázdné",
|
||||||
|
"enterIdentifier": "Zadejte identifikátor",
|
||||||
|
"identifier": "Identifier"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1083,7 +1083,7 @@
|
|||||||
"actionCreateClient": "Kunde erstellen",
|
"actionCreateClient": "Kunde erstellen",
|
||||||
"actionDeleteClient": "Kunde löschen",
|
"actionDeleteClient": "Kunde löschen",
|
||||||
"actionUpdateClient": "Kunde aktualisieren",
|
"actionUpdateClient": "Kunde aktualisieren",
|
||||||
"actionListClients": "Kunden auflisten",
|
"actionListClients": "Clients auflisten",
|
||||||
"actionGetClient": "Kunde holen",
|
"actionGetClient": "Kunde holen",
|
||||||
"actionCreateSiteResource": "Site-Ressource erstellen",
|
"actionCreateSiteResource": "Site-Ressource erstellen",
|
||||||
"actionDeleteSiteResource": "Site-Ressource löschen",
|
"actionDeleteSiteResource": "Site-Ressource löschen",
|
||||||
@@ -1161,7 +1161,7 @@
|
|||||||
"sidebarAllUsers": "Alle Benutzer",
|
"sidebarAllUsers": "Alle Benutzer",
|
||||||
"sidebarIdentityProviders": "Identitätsanbieter",
|
"sidebarIdentityProviders": "Identitätsanbieter",
|
||||||
"sidebarLicense": "Lizenz",
|
"sidebarLicense": "Lizenz",
|
||||||
"sidebarClients": "Kunden",
|
"sidebarClients": "Clients",
|
||||||
"sidebarDomains": "Domänen",
|
"sidebarDomains": "Domänen",
|
||||||
"sidebarBluePrints": "Baupläne",
|
"sidebarBluePrints": "Baupläne",
|
||||||
"blueprints": "Baupläne",
|
"blueprints": "Baupläne",
|
||||||
@@ -1279,6 +1279,15 @@
|
|||||||
"settingsErrorUpdateDescription": "Beim Aktualisieren der Einstellungen ist ein Fehler aufgetreten",
|
"settingsErrorUpdateDescription": "Beim Aktualisieren der Einstellungen ist ein Fehler aufgetreten",
|
||||||
"sidebarCollapse": "Zusammenklappen",
|
"sidebarCollapse": "Zusammenklappen",
|
||||||
"sidebarExpand": "Aufklappen",
|
"sidebarExpand": "Aufklappen",
|
||||||
|
"productUpdateMoreInfo": "{noOfUpdates} weitere Updates",
|
||||||
|
"productUpdateInfo": "{noOfUpdates} Updates",
|
||||||
|
"productUpdateWhatsNew": "Was ist neu",
|
||||||
|
"productUpdateTitle": "Produkt-Updates",
|
||||||
|
"productUpdateEmpty": "Keine Updates",
|
||||||
|
"dismissAll": "Alle verwerfen",
|
||||||
|
"pangolinUpdateAvailable": "Neue Version verfügbar",
|
||||||
|
"pangolinUpdateAvailableInfo": "Version {version} ist bereit zur Installation",
|
||||||
|
"pangolinUpdateAvailableReleaseNotes": "Versionshinweise anzeigen",
|
||||||
"newtUpdateAvailable": "Update verfügbar",
|
"newtUpdateAvailable": "Update verfügbar",
|
||||||
"newtUpdateAvailableInfo": "Eine neue Version von Newt ist verfügbar. Bitte aktualisieren Sie auf die neueste Version für das beste Erlebnis.",
|
"newtUpdateAvailableInfo": "Eine neue Version von Newt ist verfügbar. Bitte aktualisieren Sie auf die neueste Version für das beste Erlebnis.",
|
||||||
"domainPickerEnterDomain": "Domäne",
|
"domainPickerEnterDomain": "Domäne",
|
||||||
@@ -1423,14 +1432,14 @@
|
|||||||
},
|
},
|
||||||
"siteRequired": "Standort ist erforderlich.",
|
"siteRequired": "Standort ist erforderlich.",
|
||||||
"olmTunnel": "Olm-Tunnel",
|
"olmTunnel": "Olm-Tunnel",
|
||||||
"olmTunnelDescription": "Nutzen Sie Olm für die Kundenverbindung",
|
"olmTunnelDescription": "Nutzen Sie Olm für die Client-Verbindung",
|
||||||
"errorCreatingClient": "Fehler beim Erstellen des Clients",
|
"errorCreatingClient": "Fehler beim Erstellen des Clients",
|
||||||
"clientDefaultsNotFound": "Standardeinstellungen des Clients nicht gefunden",
|
"clientDefaultsNotFound": "Standardeinstellungen des Clients nicht gefunden",
|
||||||
"createClient": "Client erstellen",
|
"createClient": "Client erstellen",
|
||||||
"createClientDescription": "Erstellen Sie einen neuen Client für die Verbindung zu Ihren Standorten.",
|
"createClientDescription": "Erstellen Sie einen neuen Client für die Verbindung zu Ihren Standorten.",
|
||||||
"seeAllClients": "Alle Clients anzeigen",
|
"seeAllClients": "Alle Clients anzeigen",
|
||||||
"clientInformation": "Kundeninformationen",
|
"clientInformation": "Client-Informationen",
|
||||||
"clientNamePlaceholder": "Kundenname",
|
"clientNamePlaceholder": "Client-Name",
|
||||||
"address": "Adresse",
|
"address": "Adresse",
|
||||||
"subnetPlaceholder": "Subnetz",
|
"subnetPlaceholder": "Subnetz",
|
||||||
"addressDescription": "Die Adresse, die dieser Client für die Verbindung verwenden wird.",
|
"addressDescription": "Die Adresse, die dieser Client für die Verbindung verwenden wird.",
|
||||||
@@ -1525,6 +1534,12 @@
|
|||||||
"resourcesTableTheseResourcesForUseWith": "Diese Ressourcen sind zur Verwendung mit",
|
"resourcesTableTheseResourcesForUseWith": "Diese Ressourcen sind zur Verwendung mit",
|
||||||
"resourcesTableClients": "Clients",
|
"resourcesTableClients": "Clients",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "und sind nur intern zugänglich, wenn mit einem Client verbunden.",
|
"resourcesTableAndOnlyAccessibleInternally": "und sind nur intern zugänglich, wenn mit einem Client verbunden.",
|
||||||
|
"resourcesTableNoTargets": "Keine Ziele",
|
||||||
|
"resourcesTableHealthy": "Gesund",
|
||||||
|
"resourcesTableDegraded": "Degradiert",
|
||||||
|
"resourcesTableOffline": "Offline",
|
||||||
|
"resourcesTableUnknown": "Unbekannt",
|
||||||
|
"resourcesTableNotMonitored": "Nicht überwacht",
|
||||||
"editInternalResourceDialogEditClientResource": "Client-Ressource bearbeiten",
|
"editInternalResourceDialogEditClientResource": "Client-Ressource bearbeiten",
|
||||||
"editInternalResourceDialogUpdateResourceProperties": "Aktualisieren Sie die Ressourceneigenschaften und die Zielkonfiguration für {resourceName}.",
|
"editInternalResourceDialogUpdateResourceProperties": "Aktualisieren Sie die Ressourceneigenschaften und die Zielkonfiguration für {resourceName}.",
|
||||||
"editInternalResourceDialogResourceProperties": "Ressourceneigenschaften",
|
"editInternalResourceDialogResourceProperties": "Ressourceneigenschaften",
|
||||||
@@ -2049,7 +2064,7 @@
|
|||||||
"orgOrDomainIdMissing": "Organisation oder Domänen-ID fehlt",
|
"orgOrDomainIdMissing": "Organisation oder Domänen-ID fehlt",
|
||||||
"loadingDNSRecords": "Lade DNS-Einträge...",
|
"loadingDNSRecords": "Lade DNS-Einträge...",
|
||||||
"olmUpdateAvailableInfo": "Eine aktualisierte Version von Olm ist verfügbar. Bitte aktualisieren Sie auf die neueste Version für die beste Erfahrung.",
|
"olmUpdateAvailableInfo": "Eine aktualisierte Version von Olm ist verfügbar. Bitte aktualisieren Sie auf die neueste Version für die beste Erfahrung.",
|
||||||
"client": "Kunde",
|
"client": "Client",
|
||||||
"proxyProtocol": "Proxy-Protokoll-Einstellungen",
|
"proxyProtocol": "Proxy-Protokoll-Einstellungen",
|
||||||
"proxyProtocolDescription": "Konfigurieren Sie das Proxy-Protokoll, um die IP-Adressen des Clients für TCP/UDP-Dienste zu erhalten.",
|
"proxyProtocolDescription": "Konfigurieren Sie das Proxy-Protokoll, um die IP-Adressen des Clients für TCP/UDP-Dienste zu erhalten.",
|
||||||
"enableProxyProtocol": "Proxy-Protokoll aktivieren",
|
"enableProxyProtocol": "Proxy-Protokoll aktivieren",
|
||||||
@@ -2095,5 +2110,31 @@
|
|||||||
"selectedResources": "Ausgewählte Ressourcen",
|
"selectedResources": "Ausgewählte Ressourcen",
|
||||||
"enableSelected": "Ausgewählte aktivieren",
|
"enableSelected": "Ausgewählte aktivieren",
|
||||||
"disableSelected": "Ausgewählte deaktivieren",
|
"disableSelected": "Ausgewählte deaktivieren",
|
||||||
|
"credentials": "Zugangsdaten",
|
||||||
|
"savecredentials": "Zugangsdaten speichern",
|
||||||
|
"regeneratecredentials": "Re-Key",
|
||||||
|
"regenerateCredentials": "Regenerieren und speichern Sie Ihre Zugangsdaten",
|
||||||
|
"generatedcredentials": "Generierte Zugangsdaten",
|
||||||
|
"copyandsavethesecredentials": "Diese Zugangsdaten kopieren und speichern",
|
||||||
|
"copyandsavethesecredentialsdescription": "Diese Zugangsdaten werden nach dem Verlassen dieser Seite nicht mehr angezeigt. Speichern Sie sie jetzt sicher.",
|
||||||
|
"credentialsSaved": "Zugangsdaten gespeichert",
|
||||||
|
"credentialsSavedDescription": "Zugangsdaten wurden neu erstellt und erfolgreich gespeichert.",
|
||||||
|
"credentialsSaveError": "Fehler beim Speichern der Zugangsdaten",
|
||||||
|
"credentialsSaveErrorDescription": "Beim Erneuern und Speichern der Zugangsdaten ist ein Fehler aufgetreten.",
|
||||||
|
"regenerateCredentialsWarning": "Das Regenerieren von Zugangsdaten wird die vorhergehenden ungültig machen. Bitte aktualisieren die Konfigurationen, welche diese Zugangsdaten verwenden.",
|
||||||
|
"confirm": "Bestätigen",
|
||||||
|
"regenerateCredentialsConfirmation": "Sind Sie sicher, dass Sie die Zugangsdaten neu generieren möchten?",
|
||||||
|
"endpoint": "Endpunkt",
|
||||||
|
"Id": "ID",
|
||||||
|
"SecretKey": "Geheimer Schlüssel",
|
||||||
|
"featureDisabledTooltip": "Diese Funktion ist nur im Enterprise-Plan verfügbar und erfordert eine Lizenz, um sie zu nutzen.",
|
||||||
|
"niceId": "Schöne ID",
|
||||||
|
"niceIdUpdated": "Schöne ID aktualisiert",
|
||||||
|
"niceIdUpdatedSuccessfully": "Nice ID erfolgreich aktualisiert",
|
||||||
|
"niceIdUpdateError": "Fehler beim Aktualisieren der Nizza-ID",
|
||||||
|
"niceIdUpdateErrorDescription": "Beim Aktualisieren der Nizza-ID ist ein Fehler aufgetreten.",
|
||||||
|
"niceIdCannotBeEmpty": "Nizza-ID darf nicht leer sein",
|
||||||
|
"enterIdentifier": "Identifikator eingeben",
|
||||||
|
"identifier": "Identifier",
|
||||||
"checkSelectedStatus": "Status der Auswahl überprüfen"
|
"checkSelectedStatus": "Status der Auswahl überprüfen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"setupCreate": "Create your organization, site, and resources",
|
"setupCreate": "Create the organization, site, and resources",
|
||||||
"setupNewOrg": "New Organization",
|
"setupNewOrg": "New Organization",
|
||||||
"setupCreateOrg": "Create Organization",
|
"setupCreateOrg": "Create Organization",
|
||||||
"setupCreateResources": "Create Resources",
|
"setupCreateResources": "Create Resources",
|
||||||
"setupOrgName": "Organization Name",
|
"setupOrgName": "Organization Name",
|
||||||
"orgDisplayName": "This is the display name of your organization.",
|
"orgDisplayName": "This is the display name of the organization.",
|
||||||
"orgId": "Organization ID",
|
"orgId": "Organization ID",
|
||||||
"setupIdentifierMessage": "This is the unique identifier for your organization. This is separate from the display name.",
|
"setupIdentifierMessage": "This is the unique identifier for the organization.",
|
||||||
"setupErrorIdentifier": "Organization ID is already taken. Please choose a different one.",
|
"setupErrorIdentifier": "Organization ID is already taken. Please choose a different one.",
|
||||||
"componentsErrorNoMemberCreate": "You are not currently a member of any organizations. Create an organization to get started.",
|
"componentsErrorNoMemberCreate": "You are not currently a member of any organizations. Create an organization to get started.",
|
||||||
"componentsErrorNoMember": "You are not currently a member of any organizations.",
|
"componentsErrorNoMember": "You are not currently a member of any organizations.",
|
||||||
@@ -50,10 +50,10 @@
|
|||||||
"siteMessageRemove": "Once removed the site will no longer be accessible. All targets associated with the site will also be removed.",
|
"siteMessageRemove": "Once removed the site will no longer be accessible. All targets associated with the site will also be removed.",
|
||||||
"siteQuestionRemove": "Are you sure you want to remove the site from the organization?",
|
"siteQuestionRemove": "Are you sure you want to remove the site from the organization?",
|
||||||
"siteManageSites": "Manage Sites",
|
"siteManageSites": "Manage Sites",
|
||||||
"siteDescription": "Allow connectivity to your network through secure tunnels",
|
"siteDescription": "Create and manage sites to enable connectivity to private networks",
|
||||||
"siteCreate": "Create Site",
|
"siteCreate": "Create Site",
|
||||||
"siteCreateDescription2": "Follow the steps below to create and connect a new site",
|
"siteCreateDescription2": "Follow the steps below to create and connect a new site",
|
||||||
"siteCreateDescription": "Create a new site to start connecting your resources",
|
"siteCreateDescription": "Create a new site to start connecting resources",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"siteErrorCreate": "Error creating site",
|
"siteErrorCreate": "Error creating site",
|
||||||
"siteErrorCreateKeyPair": "Key pair or site defaults not found",
|
"siteErrorCreateKeyPair": "Key pair or site defaults not found",
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
"siteInstallNewt": "Install Newt",
|
"siteInstallNewt": "Install Newt",
|
||||||
"siteInstallNewtDescription": "Get Newt running on your system",
|
"siteInstallNewtDescription": "Get Newt running on your system",
|
||||||
"WgConfiguration": "WireGuard Configuration",
|
"WgConfiguration": "WireGuard Configuration",
|
||||||
"WgConfigurationDescription": "Use the following configuration to connect to your network",
|
"WgConfigurationDescription": "Use the following configuration to connect to the network",
|
||||||
"operatingSystem": "Operating System",
|
"operatingSystem": "Operating System",
|
||||||
"commands": "Commands",
|
"commands": "Commands",
|
||||||
"recommended": "Recommended",
|
"recommended": "Recommended",
|
||||||
@@ -87,32 +87,32 @@
|
|||||||
"siteUpdated": "Site updated",
|
"siteUpdated": "Site updated",
|
||||||
"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 the site",
|
||||||
"siteSetting": "{siteName} Settings",
|
"siteSetting": "{siteName} Settings",
|
||||||
"siteNewtTunnel": "Newt Tunnel (Recommended)",
|
"siteNewtTunnel": "Newt Site (Recommended)",
|
||||||
"siteNewtTunnelDescription": "Easiest way to create an entrypoint into your network. No extra setup.",
|
"siteNewtTunnelDescription": "Easiest way to create an entrypoint into any 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.",
|
"siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
|
||||||
"siteLocalDescription": "Local resources only. No tunneling.",
|
"siteLocalDescription": "Local resources only. No tunneling.",
|
||||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||||
"siteSeeAll": "See All Sites",
|
"siteSeeAll": "See All Sites",
|
||||||
"siteTunnelDescription": "Determine how you want to connect to your site",
|
"siteTunnelDescription": "Determine how you want to connect to the site",
|
||||||
"siteNewtCredentials": "Newt Credentials",
|
"siteNewtCredentials": "Credentials",
|
||||||
"siteNewtCredentialsDescription": "This is how Newt will authenticate with the server",
|
"siteNewtCredentialsDescription": "This is how the site will authenticate with the server",
|
||||||
"siteCredentialsSave": "Save Your Credentials",
|
"siteCredentialsSave": "Save the Credentials",
|
||||||
"siteCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
|
"siteCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
|
||||||
"siteInfo": "Site Information",
|
"siteInfo": "Site Information",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"shareTitle": "Manage Share Links",
|
"shareTitle": "Manage Share Links",
|
||||||
"shareDescription": "Create shareable links to grant temporary or permanent access to your resources",
|
"shareDescription": "Create shareable links to grant temporary or permanent access to proxy resources",
|
||||||
"shareSearch": "Search share links...",
|
"shareSearch": "Search share links...",
|
||||||
"shareCreate": "Create Share Link",
|
"shareCreate": "Create Share Link",
|
||||||
"shareErrorDelete": "Failed to delete link",
|
"shareErrorDelete": "Failed to delete link",
|
||||||
"shareErrorDeleteMessage": "An error occurred deleting link",
|
"shareErrorDeleteMessage": "An error occurred deleting link",
|
||||||
"shareDeleted": "Link deleted",
|
"shareDeleted": "Link deleted",
|
||||||
"shareDeletedDescription": "The link has been deleted",
|
"shareDeletedDescription": "The link has been deleted",
|
||||||
"shareTokenDescription": "Your access token can be passed in two ways: as a query parameter or in the request headers. These must be passed from the client on every request for authenticated access.",
|
"shareTokenDescription": "The access token can be passed in two ways: as a query parameter or in the request headers. These must be passed from the client on every request for authenticated access.",
|
||||||
"accessToken": "Access Token",
|
"accessToken": "Access Token",
|
||||||
"usageExamples": "Usage Examples",
|
"usageExamples": "Usage Examples",
|
||||||
"tokenId": "Token ID",
|
"tokenId": "Token ID",
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
"importantNote": "Important Note",
|
"importantNote": "Important Note",
|
||||||
"shareImportantDescription": "For security reasons, using headers is recommended over query parameters when possible, as query parameters may be logged in server logs or browser history.",
|
"shareImportantDescription": "For security reasons, using headers is recommended over query parameters when possible, as query parameters may be logged in server logs or browser history.",
|
||||||
"token": "Token",
|
"token": "Token",
|
||||||
"shareTokenSecurety": "Keep your access token secure. Do not share it in publicly accessible areas or client-side code.",
|
"shareTokenSecurety": "Keep the access token secure. Do not share it in publicly accessible areas or client-side code.",
|
||||||
"shareErrorFetchResource": "Failed to fetch resources",
|
"shareErrorFetchResource": "Failed to fetch resources",
|
||||||
"shareErrorFetchResourceDescription": "An error occurred while fetching the resources",
|
"shareErrorFetchResourceDescription": "An error occurred while fetching the resources",
|
||||||
"shareErrorCreate": "Failed to create share link",
|
"shareErrorCreate": "Failed to create share link",
|
||||||
@@ -144,8 +144,10 @@
|
|||||||
"expires": "Expires",
|
"expires": "Expires",
|
||||||
"never": "Never",
|
"never": "Never",
|
||||||
"shareErrorSelectResource": "Please select a resource",
|
"shareErrorSelectResource": "Please select a resource",
|
||||||
"resourceTitle": "Manage Resources",
|
"proxyResourceTitle": "Manage Proxy Resources",
|
||||||
"resourceDescription": "Create secure proxies to your private applications",
|
"proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser",
|
||||||
|
"clientResourceTitle": "Manage Client Resources",
|
||||||
|
"clientResourceDescription": "Create and manage resources that are only accessible through a connected client",
|
||||||
"resourcesSearch": "Search resources...",
|
"resourcesSearch": "Search resources...",
|
||||||
"resourceAdd": "Add Resource",
|
"resourceAdd": "Add Resource",
|
||||||
"resourceErrorDelte": "Error deleting resource",
|
"resourceErrorDelte": "Error deleting resource",
|
||||||
@@ -155,9 +157,9 @@
|
|||||||
"resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.",
|
"resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.",
|
||||||
"resourceQuestionRemove": "Are you sure you want to remove the resource from the organization?",
|
"resourceQuestionRemove": "Are you sure you want to remove the resource from the organization?",
|
||||||
"resourceHTTP": "HTTPS Resource",
|
"resourceHTTP": "HTTPS Resource",
|
||||||
"resourceHTTPDescription": "Proxy requests to your app over HTTPS using a subdomain or base domain.",
|
"resourceHTTPDescription": "Proxy requests to the app over HTTPS using a subdomain or base domain.",
|
||||||
"resourceRaw": "Raw TCP/UDP Resource",
|
"resourceRaw": "Raw TCP/UDP Resource",
|
||||||
"resourceRawDescription": "Proxy requests to your app over TCP/UDP using a port number. This only works when sites are connected to nodes.",
|
"resourceRawDescription": "Proxy requests to the app over TCP/UDP using a port number. This only works when sites are connected to nodes.",
|
||||||
"resourceCreate": "Create Resource",
|
"resourceCreate": "Create Resource",
|
||||||
"resourceCreateDescription": "Follow the steps below to create a new resource",
|
"resourceCreateDescription": "Follow the steps below to create a new resource",
|
||||||
"resourceSeeAll": "See All Resources",
|
"resourceSeeAll": "See All Resources",
|
||||||
@@ -171,22 +173,22 @@
|
|||||||
"noCountryFound": "No country found.",
|
"noCountryFound": "No country found.",
|
||||||
"siteSelectionDescription": "This site will provide connectivity to the target.",
|
"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 to access the resource",
|
||||||
"resourceHTTPSSettings": "HTTPS Settings",
|
"resourceHTTPSSettings": "HTTPS Settings",
|
||||||
"resourceHTTPSSettingsDescription": "Configure how your resource will be accessed over HTTPS",
|
"resourceHTTPSSettingsDescription": "Configure how the resource will be accessed over HTTPS",
|
||||||
"domainType": "Domain Type",
|
"domainType": "Domain Type",
|
||||||
"subdomain": "Subdomain",
|
"subdomain": "Subdomain",
|
||||||
"baseDomain": "Base Domain",
|
"baseDomain": "Base Domain",
|
||||||
"subdomnainDescription": "The subdomain where your resource will be accessible.",
|
"subdomnainDescription": "The subdomain where the resource will be accessible.",
|
||||||
"resourceRawSettings": "TCP/UDP Settings",
|
"resourceRawSettings": "TCP/UDP Settings",
|
||||||
"resourceRawSettingsDescription": "Configure how your resource will be accessed over TCP/UDP. You map the resource to a port on the host Pangolin server, so you can access the resource from server-public-ip:mapped-port.",
|
"resourceRawSettingsDescription": "Configure how the resource will be accessed over TCP/UDP. You map the resource to a port on the host Pangolin server, so you can access the resource from server-public-ip:mapped-port.",
|
||||||
"protocol": "Protocol",
|
"protocol": "Protocol",
|
||||||
"protocolSelect": "Select a protocol",
|
"protocolSelect": "Select a protocol",
|
||||||
"resourcePortNumber": "Port Number",
|
"resourcePortNumber": "Port Number",
|
||||||
"resourcePortNumberDescription": "The external port number to proxy requests.",
|
"resourcePortNumberDescription": "The external port number to proxy requests.",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"resourceConfig": "Configuration Snippets",
|
"resourceConfig": "Configuration Snippets",
|
||||||
"resourceConfigDescription": "Copy and paste these configuration snippets to set up your TCP/UDP resource",
|
"resourceConfigDescription": "Copy and paste these configuration snippets to set up the TCP/UDP resource",
|
||||||
"resourceAddEntrypoints": "Traefik: Add Entrypoints",
|
"resourceAddEntrypoints": "Traefik: Add Entrypoints",
|
||||||
"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",
|
||||||
@@ -202,14 +204,14 @@
|
|||||||
"proxy": "Proxy",
|
"proxy": "Proxy",
|
||||||
"internal": "Internal",
|
"internal": "Internal",
|
||||||
"rules": "Rules",
|
"rules": "Rules",
|
||||||
"resourceSettingDescription": "Configure the settings on your resource",
|
"resourceSettingDescription": "Configure the settings on the resource",
|
||||||
"resourceSetting": "{resourceName} Settings",
|
"resourceSetting": "{resourceName} Settings",
|
||||||
"alwaysAllow": "Always Allow",
|
"alwaysAllow": "Bypass Auth",
|
||||||
"alwaysDeny": "Always Deny",
|
"alwaysDeny": "Block Access",
|
||||||
"passToAuth": "Pass to Auth",
|
"passToAuth": "Pass to Auth",
|
||||||
"orgSettingsDescription": "Configure your organization's settings",
|
"orgSettingsDescription": "Configure the organization's settings",
|
||||||
"orgGeneralSettings": "Organization Settings",
|
"orgGeneralSettings": "Organization Settings",
|
||||||
"orgGeneralSettingsDescription": "Manage your organization details and configuration",
|
"orgGeneralSettingsDescription": "Manage the organization's details and configuration",
|
||||||
"saveGeneralSettings": "Save General Settings",
|
"saveGeneralSettings": "Save General Settings",
|
||||||
"saveSettings": "Save Settings",
|
"saveSettings": "Save Settings",
|
||||||
"orgDangerZone": "Danger Zone",
|
"orgDangerZone": "Danger Zone",
|
||||||
@@ -232,7 +234,7 @@
|
|||||||
"orgMissing": "Organization ID Missing",
|
"orgMissing": "Organization ID Missing",
|
||||||
"orgMissingMessage": "Unable to regenerate invitation without an organization ID.",
|
"orgMissingMessage": "Unable to regenerate invitation without an organization ID.",
|
||||||
"accessUsersManage": "Manage Users",
|
"accessUsersManage": "Manage Users",
|
||||||
"accessUsersDescription": "Invite users and add them to roles to manage access to your organization",
|
"accessUsersDescription": "Invite and manage users with access to this organization",
|
||||||
"accessUsersSearch": "Search users...",
|
"accessUsersSearch": "Search users...",
|
||||||
"accessUserCreate": "Create User",
|
"accessUserCreate": "Create User",
|
||||||
"accessUserRemove": "Remove User",
|
"accessUserRemove": "Remove User",
|
||||||
@@ -241,13 +243,13 @@
|
|||||||
"role": "Role",
|
"role": "Role",
|
||||||
"nameRequired": "Name is required",
|
"nameRequired": "Name is required",
|
||||||
"accessRolesManage": "Manage Roles",
|
"accessRolesManage": "Manage Roles",
|
||||||
"accessRolesDescription": "Configure roles to manage access to your organization",
|
"accessRolesDescription": "Create and manage roles for users in the organization",
|
||||||
"accessRolesSearch": "Search roles...",
|
"accessRolesSearch": "Search roles...",
|
||||||
"accessRolesAdd": "Add Role",
|
"accessRolesAdd": "Add Role",
|
||||||
"accessRoleDelete": "Delete Role",
|
"accessRoleDelete": "Delete Role",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"inviteTitle": "Open Invitations",
|
"inviteTitle": "Open Invitations",
|
||||||
"inviteDescription": "Manage your invitations to other users",
|
"inviteDescription": "Manage invitations for other users to join the organization",
|
||||||
"inviteSearch": "Search invitations...",
|
"inviteSearch": "Search invitations...",
|
||||||
"minutes": "Minutes",
|
"minutes": "Minutes",
|
||||||
"hours": "Hours",
|
"hours": "Hours",
|
||||||
@@ -261,13 +263,13 @@
|
|||||||
"apiKeysErrorCreate": "Error creating API key",
|
"apiKeysErrorCreate": "Error creating API key",
|
||||||
"apiKeysErrorSetPermission": "Error setting permissions",
|
"apiKeysErrorSetPermission": "Error setting permissions",
|
||||||
"apiKeysCreate": "Generate API Key",
|
"apiKeysCreate": "Generate API Key",
|
||||||
"apiKeysCreateDescription": "Generate a new API key for your organization",
|
"apiKeysCreateDescription": "Generate a new API key for the organization",
|
||||||
"apiKeysGeneralSettings": "Permissions",
|
"apiKeysGeneralSettings": "Permissions",
|
||||||
"apiKeysGeneralSettingsDescription": "Determine what this API key can do",
|
"apiKeysGeneralSettingsDescription": "Determine what this API key can do",
|
||||||
"apiKeysList": "Your API Key",
|
"apiKeysList": "New API Key",
|
||||||
"apiKeysSave": "Save Your API Key",
|
"apiKeysSave": "Save the API Key",
|
||||||
"apiKeysSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
|
"apiKeysSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
|
||||||
"apiKeysInfo": "Your API key is:",
|
"apiKeysInfo": "The API key is:",
|
||||||
"apiKeysConfirmCopy": "I have copied the API key",
|
"apiKeysConfirmCopy": "I have copied the API key",
|
||||||
"generate": "Generate",
|
"generate": "Generate",
|
||||||
"done": "Done",
|
"done": "Done",
|
||||||
@@ -424,7 +426,7 @@
|
|||||||
"userCreated": "User created",
|
"userCreated": "User created",
|
||||||
"userCreatedDescription": "The user has been successfully created.",
|
"userCreatedDescription": "The user has been successfully created.",
|
||||||
"userTypeInternal": "Internal User",
|
"userTypeInternal": "Internal User",
|
||||||
"userTypeInternalDescription": "Invite a user to join your organization directly.",
|
"userTypeInternalDescription": "Invite a user to join the organization directly.",
|
||||||
"userTypeExternal": "External User",
|
"userTypeExternal": "External User",
|
||||||
"userTypeExternalDescription": "Create a user with an external identity provider.",
|
"userTypeExternalDescription": "Create a user with an external identity provider.",
|
||||||
"accessUserCreateDescription": "Follow the steps below to create a new user",
|
"accessUserCreateDescription": "Follow the steps below to create a new user",
|
||||||
@@ -436,6 +438,16 @@
|
|||||||
"inviteEmailSent": "Send invite email to user",
|
"inviteEmailSent": "Send invite email to user",
|
||||||
"inviteValid": "Valid For",
|
"inviteValid": "Valid For",
|
||||||
"selectDuration": "Select duration",
|
"selectDuration": "Select duration",
|
||||||
|
"selectResource": "Select Resource",
|
||||||
|
"filterByResource": "Filter By Resource",
|
||||||
|
"resetFilters": "Reset Filters",
|
||||||
|
"totalBlocked": "Requests Blocked By Pangolin",
|
||||||
|
"totalRequests": "Total Requests",
|
||||||
|
"requestsByCountry": "Requests By Country",
|
||||||
|
"requestsByDay": "Requests By Day",
|
||||||
|
"blocked": "Blocked",
|
||||||
|
"allowed": "Allowed",
|
||||||
|
"topCountries": "Top Countries",
|
||||||
"accessRoleSelect": "Select role",
|
"accessRoleSelect": "Select role",
|
||||||
"inviteEmailSentDescription": "An email has been sent to the user with the access link below. They must access the link to accept the invitation.",
|
"inviteEmailSentDescription": "An email has been sent to the user with the access link below. They must access the link to accept the invitation.",
|
||||||
"inviteSentDescription": "The user has been invited. They must access the link below to accept the invitation.",
|
"inviteSentDescription": "The user has been invited. They must access the link below to accept the invitation.",
|
||||||
@@ -458,13 +470,13 @@
|
|||||||
"accessControlsSubmit": "Save Access Controls",
|
"accessControlsSubmit": "Save Access Controls",
|
||||||
"roles": "Roles",
|
"roles": "Roles",
|
||||||
"accessUsersRoles": "Manage Users & Roles",
|
"accessUsersRoles": "Manage Users & Roles",
|
||||||
"accessUsersRolesDescription": "Invite users and add them to roles to manage access to your organization",
|
"accessUsersRolesDescription": "Invite users and add them to roles to manage access to the organization",
|
||||||
"key": "Key",
|
"key": "Key",
|
||||||
"createdAt": "Created At",
|
"createdAt": "Created At",
|
||||||
"proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.",
|
"proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.",
|
||||||
"proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.",
|
"proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.",
|
||||||
"proxyEnableSSL": "Enable SSL",
|
"proxyEnableSSL": "Enable SSL",
|
||||||
"proxyEnableSSLDescription": "Enable SSL/TLS encryption for secure HTTPS connections to your targets.",
|
"proxyEnableSSLDescription": "Enable SSL/TLS encryption for secure HTTPS connections to the targets.",
|
||||||
"target": "Target",
|
"target": "Target",
|
||||||
"configureTarget": "Configure Targets",
|
"configureTarget": "Configure Targets",
|
||||||
"targetErrorFetch": "Failed to fetch targets",
|
"targetErrorFetch": "Failed to fetch targets",
|
||||||
@@ -480,29 +492,29 @@
|
|||||||
"targetsErrorUpdate": "Failed to update targets",
|
"targetsErrorUpdate": "Failed to update targets",
|
||||||
"targetsErrorUpdateDescription": "An error occurred while updating targets",
|
"targetsErrorUpdateDescription": "An error occurred while updating targets",
|
||||||
"targetTlsUpdate": "TLS settings updated",
|
"targetTlsUpdate": "TLS settings updated",
|
||||||
"targetTlsUpdateDescription": "Your TLS settings have been updated successfully",
|
"targetTlsUpdateDescription": "TLS settings have been updated successfully",
|
||||||
"targetErrorTlsUpdate": "Failed to update TLS settings",
|
"targetErrorTlsUpdate": "Failed to update TLS settings",
|
||||||
"targetErrorTlsUpdateDescription": "An error occurred while updating TLS settings",
|
"targetErrorTlsUpdateDescription": "An error occurred while updating TLS settings",
|
||||||
"proxyUpdated": "Proxy settings updated",
|
"proxyUpdated": "Proxy settings updated",
|
||||||
"proxyUpdatedDescription": "Your proxy settings have been updated successfully",
|
"proxyUpdatedDescription": "Proxy settings have been updated successfully",
|
||||||
"proxyErrorUpdate": "Failed to update proxy settings",
|
"proxyErrorUpdate": "Failed to update proxy settings",
|
||||||
"proxyErrorUpdateDescription": "An error occurred while updating proxy settings",
|
"proxyErrorUpdateDescription": "An error occurred while updating proxy settings",
|
||||||
"targetAddr": "IP / Hostname",
|
"targetAddr": "IP / Hostname",
|
||||||
"targetPort": "Port",
|
"targetPort": "Port",
|
||||||
"targetProtocol": "Protocol",
|
"targetProtocol": "Protocol",
|
||||||
"targetTlsSettings": "Secure Connection Configuration",
|
"targetTlsSettings": "Secure Connection Configuration",
|
||||||
"targetTlsSettingsDescription": "Configure SSL/TLS settings for your resource",
|
"targetTlsSettingsDescription": "Configure SSL/TLS settings for the resource",
|
||||||
"targetTlsSettingsAdvanced": "Advanced TLS Settings",
|
"targetTlsSettingsAdvanced": "Advanced TLS Settings",
|
||||||
"targetTlsSni": "TLS Server Name",
|
"targetTlsSni": "TLS Server Name",
|
||||||
"targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.",
|
"targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.",
|
||||||
"targetTlsSubmit": "Save Settings",
|
"targetTlsSubmit": "Save Settings",
|
||||||
"targets": "Targets Configuration",
|
"targets": "Targets Configuration",
|
||||||
"targetsDescription": "Set up targets to route traffic to your backend services",
|
"targetsDescription": "Set up targets to route traffic to backend services",
|
||||||
"targetStickySessions": "Enable Sticky Sessions",
|
"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",
|
||||||
"targetSubmit": "Add Target",
|
"targetSubmit": "Add Target",
|
||||||
"targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to your backend.",
|
"targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to the backend.",
|
||||||
"targetNoOneDescription": "Adding more than one target above will enable load balancing.",
|
"targetNoOneDescription": "Adding more than one target above will enable load balancing.",
|
||||||
"targetsSubmit": "Save Targets",
|
"targetsSubmit": "Save Targets",
|
||||||
"addTarget": "Add Target",
|
"addTarget": "Add Target",
|
||||||
@@ -516,9 +528,11 @@
|
|||||||
"targetCreatedDescription": "Target has been created successfully",
|
"targetCreatedDescription": "Target has been created successfully",
|
||||||
"targetErrorCreate": "Failed to create target",
|
"targetErrorCreate": "Failed to create target",
|
||||||
"targetErrorCreateDescription": "An error occurred while creating the target",
|
"targetErrorCreateDescription": "An error occurred while creating the target",
|
||||||
|
"tlsServerName": "TLS Server Name",
|
||||||
|
"tlsServerNameDescription": "The TLS server name to use for SNI",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"proxyAdditional": "Additional Proxy Settings",
|
"proxyAdditional": "Additional Proxy Settings",
|
||||||
"proxyAdditionalDescription": "Configure how your resource handles proxy settings",
|
"proxyAdditionalDescription": "Configure how the resource handles proxy settings",
|
||||||
"proxyCustomHeader": "Custom Host Header",
|
"proxyCustomHeader": "Custom Host Header",
|
||||||
"proxyCustomHeaderDescription": "The host header to set when proxying requests. Leave empty to use the default.",
|
"proxyCustomHeaderDescription": "The host header to set when proxying requests. Leave empty to use the default.",
|
||||||
"proxyAdditionalSubmit": "Save Proxy Settings",
|
"proxyAdditionalSubmit": "Save Proxy Settings",
|
||||||
@@ -558,7 +572,7 @@
|
|||||||
"rulesMatchType": "Match Type",
|
"rulesMatchType": "Match Type",
|
||||||
"value": "Value",
|
"value": "Value",
|
||||||
"rulesAbout": "About Rules",
|
"rulesAbout": "About Rules",
|
||||||
"rulesAboutDescription": "Rules allow you to control access to your resource based on a set of criteria. You can create rules to allow or deny access based on IP address or URL path.",
|
"rulesAboutDescription": "Rules allow you to control access to the resource based on a set of criteria. You can create rules to allow or deny access based on IP address or URL path.",
|
||||||
"rulesActions": "Actions",
|
"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",
|
||||||
@@ -570,7 +584,7 @@
|
|||||||
"rulesEnable": "Enable Rules",
|
"rulesEnable": "Enable Rules",
|
||||||
"rulesEnableDescription": "Enable or disable rule evaluation for this resource",
|
"rulesEnableDescription": "Enable or disable rule evaluation for this resource",
|
||||||
"rulesResource": "Resource Rules Configuration",
|
"rulesResource": "Resource Rules Configuration",
|
||||||
"rulesResourceDescription": "Configure rules to control access to your resource",
|
"rulesResourceDescription": "Configure rules to control access to the resource",
|
||||||
"ruleSubmit": "Add Rule",
|
"ruleSubmit": "Add Rule",
|
||||||
"rulesNoOne": "No rules. Add a rule using the form.",
|
"rulesNoOne": "No rules. Add a rule using the form.",
|
||||||
"rulesOrder": "Rules are evaluated by priority in ascending order.",
|
"rulesOrder": "Rules are evaluated by priority in ascending order.",
|
||||||
@@ -586,7 +600,7 @@
|
|||||||
"none": "None",
|
"none": "None",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"resources": "Resources",
|
"resources": "Resources",
|
||||||
"resourcesDescription": "Resources are proxies to applications running on your private network. Create a resource for any HTTP/HTTPS or raw TCP/UDP service on your private network. Each resource must be connected to a site to enable private, secure connectivity through an encrypted WireGuard tunnel.",
|
"resourcesDescription": "Resources are proxies to applications running on the private network. Create a resource for any HTTP/HTTPS or raw TCP/UDP service on your private network. Each resource must be connected to a site to enable private, secure connectivity through an encrypted WireGuard tunnel.",
|
||||||
"resourcesWireGuardConnect": "Secure connectivity with WireGuard encryption",
|
"resourcesWireGuardConnect": "Secure connectivity with WireGuard encryption",
|
||||||
"resourcesMultipleAuthenticationMethods": "Configure multiple authentication methods",
|
"resourcesMultipleAuthenticationMethods": "Configure multiple authentication methods",
|
||||||
"resourcesUsersRolesAccess": "User and role-based access control",
|
"resourcesUsersRolesAccess": "User and role-based access control",
|
||||||
@@ -597,7 +611,7 @@
|
|||||||
"resourceSelect": "Select resource",
|
"resourceSelect": "Select resource",
|
||||||
"shareLinks": "Share Links",
|
"shareLinks": "Share Links",
|
||||||
"share": "Shareable Links",
|
"share": "Shareable Links",
|
||||||
"shareDescription2": "Create shareable links to your resources. Links provide temporary or unlimited access to your resource. You can configure the expiration duration of the link when you create one.",
|
"shareDescription2": "Create shareable links to resources. Links provide temporary or unlimited access to your resource. You can configure the expiration duration of the link when you create one.",
|
||||||
"shareEasyCreate": "Easy to create and share",
|
"shareEasyCreate": "Easy to create and share",
|
||||||
"shareConfigurableExpirationDuration": "Configurable expiration duration",
|
"shareConfigurableExpirationDuration": "Configurable expiration duration",
|
||||||
"shareSecureAndRevocable": "Secure and revocable",
|
"shareSecureAndRevocable": "Secure and revocable",
|
||||||
@@ -607,19 +621,19 @@
|
|||||||
"unknownCommand": "Unknown command",
|
"unknownCommand": "Unknown command",
|
||||||
"newtErrorFetchReleases": "Failed to fetch release info: {err}",
|
"newtErrorFetchReleases": "Failed to fetch release info: {err}",
|
||||||
"newtErrorFetchLatest": "Error fetching latest release: {err}",
|
"newtErrorFetchLatest": "Error fetching latest release: {err}",
|
||||||
"newtEndpoint": "Newt Endpoint",
|
"newtEndpoint": "Endpoint",
|
||||||
"newtId": "Newt ID",
|
"newtId": "ID",
|
||||||
"newtSecretKey": "Newt Secret Key",
|
"newtSecretKey": "Secret",
|
||||||
"architecture": "Architecture",
|
"architecture": "Architecture",
|
||||||
"sites": "Sites",
|
"sites": "Sites",
|
||||||
"siteWgAnyClients": "Use any WireGuard client to connect. You will have to address your internal resources using the peer IP.",
|
"siteWgAnyClients": "Use any WireGuard client to connect. You will have to address internal resources using the peer IP.",
|
||||||
"siteWgCompatibleAllClients": "Compatible with all WireGuard clients",
|
"siteWgCompatibleAllClients": "Compatible with all WireGuard clients",
|
||||||
"siteWgManualConfigurationRequired": "Manual configuration required",
|
"siteWgManualConfigurationRequired": "Manual configuration required",
|
||||||
"userErrorNotAdminOrOwner": "User is not an admin or owner",
|
"userErrorNotAdminOrOwner": "User is not an admin or owner",
|
||||||
"pangolinSettings": "Settings - Pangolin",
|
"pangolinSettings": "Settings - Pangolin",
|
||||||
"accessRoleYour": "Your role:",
|
"accessRoleYour": "Your role:",
|
||||||
"accessRoleSelect2": "Select a role",
|
"accessRoleSelect2": "Select roles",
|
||||||
"accessUserSelect": "Select a user",
|
"accessUserSelect": "Select users",
|
||||||
"otpEmailEnter": "Enter an email",
|
"otpEmailEnter": "Enter an email",
|
||||||
"otpEmailEnterDescription": "Press enter to add an email after typing it in the input field.",
|
"otpEmailEnterDescription": "Press enter to add an email after typing it in the input field.",
|
||||||
"otpEmailErrorInvalid": "Invalid email address. Wildcard (*) must be the entire local part.",
|
"otpEmailErrorInvalid": "Invalid email address. Wildcard (*) must be the entire local part.",
|
||||||
@@ -671,7 +685,7 @@
|
|||||||
"resourcePincodeSetupTitle": "Set Pincode",
|
"resourcePincodeSetupTitle": "Set Pincode",
|
||||||
"resourcePincodeSetupTitleDescription": "Set a pincode to protect this resource",
|
"resourcePincodeSetupTitleDescription": "Set a pincode to protect this resource",
|
||||||
"resourceRoleDescription": "Admins can always access this resource.",
|
"resourceRoleDescription": "Admins can always access this resource.",
|
||||||
"resourceUsersRoles": "Users & Roles",
|
"resourceUsersRoles": "Access Controls",
|
||||||
"resourceUsersRolesDescription": "Configure which users and roles can visit this resource",
|
"resourceUsersRolesDescription": "Configure which users and roles can visit this resource",
|
||||||
"resourceUsersRolesSubmit": "Save Users & Roles",
|
"resourceUsersRolesSubmit": "Save Users & Roles",
|
||||||
"resourceWhitelistSave": "Saved successfully",
|
"resourceWhitelistSave": "Saved successfully",
|
||||||
@@ -702,6 +716,7 @@
|
|||||||
"resourceTransferSubmit": "Transfer Resource",
|
"resourceTransferSubmit": "Transfer Resource",
|
||||||
"siteDestination": "Destination Site",
|
"siteDestination": "Destination Site",
|
||||||
"searchSites": "Search sites",
|
"searchSites": "Search sites",
|
||||||
|
"countries": "Countries",
|
||||||
"accessRoleCreate": "Create Role",
|
"accessRoleCreate": "Create Role",
|
||||||
"accessRoleCreateDescription": "Create a new role to group users and manage their permissions.",
|
"accessRoleCreateDescription": "Create a new role to group users and manage their permissions.",
|
||||||
"accessRoleCreateSubmit": "Create Role",
|
"accessRoleCreateSubmit": "Create Role",
|
||||||
@@ -766,15 +781,15 @@
|
|||||||
"idpOidcConfigure": "OAuth2/OIDC Configuration",
|
"idpOidcConfigure": "OAuth2/OIDC Configuration",
|
||||||
"idpOidcConfigureDescription": "Configure the OAuth2/OIDC provider endpoints and credentials",
|
"idpOidcConfigureDescription": "Configure the OAuth2/OIDC provider endpoints and credentials",
|
||||||
"idpClientId": "Client ID",
|
"idpClientId": "Client ID",
|
||||||
"idpClientIdDescription": "The OAuth2 client ID from your identity provider",
|
"idpClientIdDescription": "The OAuth2 client ID from the identity provider",
|
||||||
"idpClientSecret": "Client Secret",
|
"idpClientSecret": "Client Secret",
|
||||||
"idpClientSecretDescription": "The OAuth2 client secret from your identity provider",
|
"idpClientSecretDescription": "The OAuth2 client secret from the identity provider",
|
||||||
"idpAuthUrl": "Authorization URL",
|
"idpAuthUrl": "Authorization URL",
|
||||||
"idpAuthUrlDescription": "The OAuth2 authorization endpoint URL",
|
"idpAuthUrlDescription": "The OAuth2 authorization endpoint URL",
|
||||||
"idpTokenUrl": "Token URL",
|
"idpTokenUrl": "Token URL",
|
||||||
"idpTokenUrlDescription": "The OAuth2 token endpoint URL",
|
"idpTokenUrlDescription": "The OAuth2 token endpoint URL",
|
||||||
"idpOidcConfigureAlert": "Important Information",
|
"idpOidcConfigureAlert": "Important Information",
|
||||||
"idpOidcConfigureAlertDescription": "After creating the identity provider, you will need to configure the callback URL in your identity provider's settings. The callback URL will be provided after successful creation.",
|
"idpOidcConfigureAlertDescription": "After creating the identity provider, you will need to configure the callback URL in the identity provider's settings. The callback URL will be provided after successful creation.",
|
||||||
"idpToken": "Token Configuration",
|
"idpToken": "Token Configuration",
|
||||||
"idpTokenDescription": "Configure how to extract user information from the ID token",
|
"idpTokenDescription": "Configure how to extract user information from the ID token",
|
||||||
"idpJmespathAbout": "About JMESPath",
|
"idpJmespathAbout": "About JMESPath",
|
||||||
@@ -791,7 +806,7 @@
|
|||||||
"idpSubmit": "Create Identity Provider",
|
"idpSubmit": "Create Identity Provider",
|
||||||
"orgPolicies": "Organization Policies",
|
"orgPolicies": "Organization Policies",
|
||||||
"idpSettings": "{idpName} Settings",
|
"idpSettings": "{idpName} Settings",
|
||||||
"idpCreateSettingsDescription": "Configure the settings for your identity provider",
|
"idpCreateSettingsDescription": "Configure the settings for the identity provider",
|
||||||
"roleMapping": "Role Mapping",
|
"roleMapping": "Role Mapping",
|
||||||
"orgMapping": "Organization Mapping",
|
"orgMapping": "Organization Mapping",
|
||||||
"orgPoliciesSearch": "Search organization policies...",
|
"orgPoliciesSearch": "Search organization policies...",
|
||||||
@@ -826,7 +841,7 @@
|
|||||||
"idpUpdatedDescription": "Identity provider updated successfully",
|
"idpUpdatedDescription": "Identity provider updated successfully",
|
||||||
"redirectUrl": "Redirect URL",
|
"redirectUrl": "Redirect URL",
|
||||||
"redirectUrlAbout": "About Redirect URL",
|
"redirectUrlAbout": "About Redirect URL",
|
||||||
"redirectUrlAboutDescription": "This is the URL to which users will be redirected after authentication. You need to configure this URL in your identity provider settings.",
|
"redirectUrlAboutDescription": "This is the URL to which users will be redirected after authentication. You need to configure this URL in the identity provider's settings.",
|
||||||
"pangolinAuth": "Auth - Pangolin",
|
"pangolinAuth": "Auth - Pangolin",
|
||||||
"verificationCodeLengthRequirements": "Your verification code must be 8 characters.",
|
"verificationCodeLengthRequirements": "Your verification code must be 8 characters.",
|
||||||
"errorOccurred": "An error occurred",
|
"errorOccurred": "An error occurred",
|
||||||
@@ -1091,12 +1106,15 @@
|
|||||||
"actionListSiteResources": "List Site Resources",
|
"actionListSiteResources": "List Site Resources",
|
||||||
"actionUpdateSiteResource": "Update Site Resource",
|
"actionUpdateSiteResource": "Update Site Resource",
|
||||||
"actionListInvitations": "List Invitations",
|
"actionListInvitations": "List Invitations",
|
||||||
|
"actionExportLogs": "Export Logs",
|
||||||
|
"actionViewLogs": "View Logs",
|
||||||
"noneSelected": "None selected",
|
"noneSelected": "None selected",
|
||||||
"orgNotFound2": "No organizations found.",
|
"orgNotFound2": "No organizations found.",
|
||||||
"searchProgress": "Search...",
|
"searchProgress": "Search...",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"orgs": "Organizations",
|
"orgs": "Organizations",
|
||||||
"loginError": "An error occurred while logging in",
|
"loginError": "An error occurred while logging in",
|
||||||
|
"loginRequiredForDevice": "Login is required to authenticate your device.",
|
||||||
"passwordForgot": "Forgot your password?",
|
"passwordForgot": "Forgot your password?",
|
||||||
"otpAuth": "Two-Factor Authentication",
|
"otpAuth": "Two-Factor Authentication",
|
||||||
"otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.",
|
"otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.",
|
||||||
@@ -1151,19 +1169,29 @@
|
|||||||
"sidebarHome": "Home",
|
"sidebarHome": "Home",
|
||||||
"sidebarSites": "Sites",
|
"sidebarSites": "Sites",
|
||||||
"sidebarResources": "Resources",
|
"sidebarResources": "Resources",
|
||||||
|
"sidebarProxyResources": "Proxy Resources",
|
||||||
|
"sidebarClientResources": "Client Resources",
|
||||||
"sidebarAccessControl": "Access Control",
|
"sidebarAccessControl": "Access Control",
|
||||||
|
"sidebarLogsAndAnalytics": "Logs & Analytics",
|
||||||
"sidebarUsers": "Users",
|
"sidebarUsers": "Users",
|
||||||
|
"sidebarAdmin": "Admin",
|
||||||
"sidebarInvitations": "Invitations",
|
"sidebarInvitations": "Invitations",
|
||||||
"sidebarRoles": "Roles",
|
"sidebarRoles": "Roles",
|
||||||
"sidebarShareableLinks": "Shareable Links",
|
"sidebarShareableLinks": "Links",
|
||||||
"sidebarApiKeys": "API Keys",
|
"sidebarApiKeys": "API Keys",
|
||||||
"sidebarSettings": "Settings",
|
"sidebarSettings": "Settings",
|
||||||
"sidebarAllUsers": "All Users",
|
"sidebarAllUsers": "All Users",
|
||||||
"sidebarIdentityProviders": "Identity Providers",
|
"sidebarIdentityProviders": "Identity Providers",
|
||||||
"sidebarLicense": "License",
|
"sidebarLicense": "License",
|
||||||
"sidebarClients": "Clients",
|
"sidebarClients": "Clients",
|
||||||
|
"sidebarUserDevices": "User Devices",
|
||||||
|
"sidebarMachineClients": "Machine Clients",
|
||||||
"sidebarDomains": "Domains",
|
"sidebarDomains": "Domains",
|
||||||
|
"sidebarGeneral": "General",
|
||||||
|
"sidebarLogAndAnalytics": "Log & Analytics",
|
||||||
"sidebarBluePrints": "Blueprints",
|
"sidebarBluePrints": "Blueprints",
|
||||||
|
"sidebarOrganization": "Organization",
|
||||||
|
"sidebarLogsAnalytics": "Request Analytics",
|
||||||
"blueprints": "Blueprints",
|
"blueprints": "Blueprints",
|
||||||
"blueprintsDescription": "Apply declarative configurations and view previous runs",
|
"blueprintsDescription": "Apply declarative configurations and view previous runs",
|
||||||
"blueprintAdd": "Add Blueprint",
|
"blueprintAdd": "Add Blueprint",
|
||||||
@@ -1174,7 +1202,7 @@
|
|||||||
"blueprintDetailsDescription": "See the result of the applied blueprint and any errors that occurred",
|
"blueprintDetailsDescription": "See the result of the applied blueprint and any errors that occurred",
|
||||||
"blueprintInfo": "Blueprint Information",
|
"blueprintInfo": "Blueprint Information",
|
||||||
"message": "Message",
|
"message": "Message",
|
||||||
"blueprintContentsDescription": "Define the YAML content describing your infrastructure",
|
"blueprintContentsDescription": "Define the YAML content describing the infrastructure",
|
||||||
"blueprintErrorCreateDescription": "An error occurred when applying the blueprint",
|
"blueprintErrorCreateDescription": "An error occurred when applying the blueprint",
|
||||||
"blueprintErrorCreate": "Error creating blueprint",
|
"blueprintErrorCreate": "Error creating blueprint",
|
||||||
"searchBlueprintProgress": "Search blueprints...",
|
"searchBlueprintProgress": "Search blueprints...",
|
||||||
@@ -1230,15 +1258,15 @@
|
|||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
"restart": "Restart",
|
"restart": "Restart",
|
||||||
"domains": "Domains",
|
"domains": "Domains",
|
||||||
"domainsDescription": "Manage domains for your organization",
|
"domainsDescription": "Create and manage domains available in the organization",
|
||||||
"domainsSearch": "Search domains...",
|
"domainsSearch": "Search domains...",
|
||||||
"domainAdd": "Add Domain",
|
"domainAdd": "Add Domain",
|
||||||
"domainAddDescription": "Register a new domain with your organization",
|
"domainAddDescription": "Register a new domain with to the organization",
|
||||||
"domainCreate": "Create Domain",
|
"domainCreate": "Create Domain",
|
||||||
"domainCreatedDescription": "Domain created successfully",
|
"domainCreatedDescription": "Domain created successfully",
|
||||||
"domainDeletedDescription": "Domain deleted successfully",
|
"domainDeletedDescription": "Domain deleted successfully",
|
||||||
"domainQuestionRemove": "Are you sure you want to remove the domain from your account?",
|
"domainQuestionRemove": "Are you sure you want to remove the domain?",
|
||||||
"domainMessageRemove": "Once removed, the domain will no longer be associated with your account.",
|
"domainMessageRemove": "Once removed, the domain will no longer be associated with the organization.",
|
||||||
"domainConfirmDelete": "Confirm Delete Domain",
|
"domainConfirmDelete": "Confirm Delete Domain",
|
||||||
"domainDelete": "Delete Domain",
|
"domainDelete": "Delete Domain",
|
||||||
"domain": "Domain",
|
"domain": "Domain",
|
||||||
@@ -1257,7 +1285,7 @@
|
|||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
"sidebarBilling": "Billing",
|
"sidebarBilling": "Billing",
|
||||||
"billing": "Billing",
|
"billing": "Billing",
|
||||||
"orgBillingDescription": "Manage your billing information and subscriptions",
|
"orgBillingDescription": "Manage billing information and subscriptions",
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
"pangolinHosted": "Pangolin Hosted",
|
"pangolinHosted": "Pangolin Hosted",
|
||||||
"fossorial": "Fossorial",
|
"fossorial": "Fossorial",
|
||||||
@@ -1279,6 +1307,15 @@
|
|||||||
"settingsErrorUpdateDescription": "An error occurred while updating settings",
|
"settingsErrorUpdateDescription": "An error occurred while updating settings",
|
||||||
"sidebarCollapse": "Collapse",
|
"sidebarCollapse": "Collapse",
|
||||||
"sidebarExpand": "Expand",
|
"sidebarExpand": "Expand",
|
||||||
|
"productUpdateMoreInfo": "{noOfUpdates} more updates",
|
||||||
|
"productUpdateInfo": "{noOfUpdates} updates",
|
||||||
|
"productUpdateWhatsNew": "What's New",
|
||||||
|
"productUpdateTitle": "Product Updates",
|
||||||
|
"productUpdateEmpty": "No updates",
|
||||||
|
"dismissAll": "Dismiss all",
|
||||||
|
"pangolinUpdateAvailable": "New version available",
|
||||||
|
"pangolinUpdateAvailableInfo": "Version {version} is ready to install",
|
||||||
|
"pangolinUpdateAvailableReleaseNotes": "View release notes",
|
||||||
"newtUpdateAvailable": "Update Available",
|
"newtUpdateAvailable": "Update Available",
|
||||||
"newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.",
|
"newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.",
|
||||||
"domainPickerEnterDomain": "Domain",
|
"domainPickerEnterDomain": "Domain",
|
||||||
@@ -1291,7 +1328,7 @@
|
|||||||
"domainPickerSortAsc": "A-Z",
|
"domainPickerSortAsc": "A-Z",
|
||||||
"domainPickerSortDesc": "Z-A",
|
"domainPickerSortDesc": "Z-A",
|
||||||
"domainPickerCheckingAvailability": "Checking availability...",
|
"domainPickerCheckingAvailability": "Checking availability...",
|
||||||
"domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check your organization's domain settings.",
|
"domainPickerNoMatchingDomains": "No matching domains found. Try a different domain or check the organization's domain settings.",
|
||||||
"domainPickerOrganizationDomains": "Organization Domains",
|
"domainPickerOrganizationDomains": "Organization Domains",
|
||||||
"domainPickerProvidedDomains": "Provided Domains",
|
"domainPickerProvidedDomains": "Provided Domains",
|
||||||
"domainPickerSubdomain": "Subdomain: {subdomain}",
|
"domainPickerSubdomain": "Subdomain: {subdomain}",
|
||||||
@@ -1325,7 +1362,7 @@
|
|||||||
"billingModifySubscription": "Modify Subscription",
|
"billingModifySubscription": "Modify Subscription",
|
||||||
"billingStartSubscription": "Start Subscription",
|
"billingStartSubscription": "Start Subscription",
|
||||||
"billingRecurringCharge": "Recurring Charge",
|
"billingRecurringCharge": "Recurring Charge",
|
||||||
"billingManageSubscriptionSettings": "Manage your subscription settings and preferences",
|
"billingManageSubscriptionSettings": "Manage subscription settings and preferences",
|
||||||
"billingNoActiveSubscription": "You don't have an active subscription. Start your subscription to increase usage limits.",
|
"billingNoActiveSubscription": "You don't have an active subscription. Start your subscription to increase usage limits.",
|
||||||
"billingFailedToLoadSubscription": "Failed to load subscription",
|
"billingFailedToLoadSubscription": "Failed to load subscription",
|
||||||
"billingFailedToLoadUsage": "Failed to load usage",
|
"billingFailedToLoadUsage": "Failed to load usage",
|
||||||
@@ -1336,9 +1373,9 @@
|
|||||||
"billingPortalError": "Portal Error",
|
"billingPortalError": "Portal Error",
|
||||||
"billingDataUsageInfo": "You're charged for all data transferred through your secure tunnels when connected to the cloud. This includes both incoming and outgoing traffic across all your sites. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Data is not charged when using nodes.",
|
"billingDataUsageInfo": "You're charged for all data transferred through your secure tunnels when connected to the cloud. This includes both incoming and outgoing traffic across all your sites. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Data is not charged when using nodes.",
|
||||||
"billingOnlineTimeInfo": "You're charged based on how long your sites stay connected to the cloud. For example, 44,640 minutes equals one site running 24/7 for a full month. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Time is not charged when using nodes.",
|
"billingOnlineTimeInfo": "You're charged based on how long your sites stay connected to the cloud. For example, 44,640 minutes equals one site running 24/7 for a full month. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Time is not charged when using nodes.",
|
||||||
"billingUsersInfo": "You're charged for each user in your organization. Billing is calculated daily based on the number of active user accounts in your org.",
|
"billingUsersInfo": "You're charged for each user in the organization. Billing is calculated daily based on the number of active user accounts in your org.",
|
||||||
"billingDomainInfo": "You're charged for each domain in your organization. Billing is calculated daily based on the number of active domain accounts in your org.",
|
"billingDomainInfo": "You're charged for each domain in the organization. Billing is calculated daily based on the number of active domain accounts in your org.",
|
||||||
"billingRemoteExitNodesInfo": "You're charged for each managed Node in your organization. Billing is calculated daily based on the number of active managed Nodes in your org.",
|
"billingRemoteExitNodesInfo": "You're charged for each managed Node in the organization. Billing is calculated daily based on the number of active managed Nodes in your org.",
|
||||||
"domainNotFound": "Domain Not Found",
|
"domainNotFound": "Domain Not Found",
|
||||||
"domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.",
|
"domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.",
|
||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
@@ -1421,29 +1458,32 @@
|
|||||||
"and": "and",
|
"and": "and",
|
||||||
"privacyPolicy": "privacy policy"
|
"privacyPolicy": "privacy policy"
|
||||||
},
|
},
|
||||||
|
"signUpMarketing": {
|
||||||
|
"keepMeInTheLoop": "Keep me in the loop with news, updates, and new features by email."
|
||||||
|
},
|
||||||
"siteRequired": "Site is required.",
|
"siteRequired": "Site is required.",
|
||||||
"olmTunnel": "Olm Tunnel",
|
"olmTunnel": "Olm Tunnel",
|
||||||
"olmTunnelDescription": "Use Olm for client connectivity",
|
"olmTunnelDescription": "Use Olm for client connectivity",
|
||||||
"errorCreatingClient": "Error creating client",
|
"errorCreatingClient": "Error creating client",
|
||||||
"clientDefaultsNotFound": "Client defaults not found",
|
"clientDefaultsNotFound": "Client defaults not found",
|
||||||
"createClient": "Create Client",
|
"createClient": "Create Client",
|
||||||
"createClientDescription": "Create a new client for connecting to your sites",
|
"createClientDescription": "Create a new client to access private resources",
|
||||||
"seeAllClients": "See All Clients",
|
"seeAllClients": "See All Clients",
|
||||||
"clientInformation": "Client Information",
|
"clientInformation": "Client Information",
|
||||||
"clientNamePlaceholder": "Client name",
|
"clientNamePlaceholder": "Client name",
|
||||||
"address": "Address",
|
"address": "Address",
|
||||||
"subnetPlaceholder": "Subnet",
|
"subnetPlaceholder": "Subnet",
|
||||||
"addressDescription": "The address that this client will use for connectivity",
|
"addressDescription": "The internal address of the client. Must fall within the organization's subnet.",
|
||||||
"selectSites": "Select sites",
|
"selectSites": "Select sites",
|
||||||
"sitesDescription": "The client will have connectivity to the selected sites",
|
"sitesDescription": "The client will have connectivity to the selected sites",
|
||||||
"clientInstallOlm": "Install Olm",
|
"clientInstallOlm": "Install Olm",
|
||||||
"clientInstallOlmDescription": "Get Olm running on your system",
|
"clientInstallOlmDescription": "Get Olm running on your system",
|
||||||
"clientOlmCredentials": "Olm Credentials",
|
"clientOlmCredentials": "Credentials",
|
||||||
"clientOlmCredentialsDescription": "This is how Olm will authenticate with the server",
|
"clientOlmCredentialsDescription": "This is how the client will authenticate with the server",
|
||||||
"olmEndpoint": "Olm Endpoint",
|
"olmEndpoint": "Endpoint",
|
||||||
"olmId": "Olm ID",
|
"olmId": "ID",
|
||||||
"olmSecretKey": "Olm Secret Key",
|
"olmSecretKey": "Secret",
|
||||||
"clientCredentialsSave": "Save Your Credentials",
|
"clientCredentialsSave": "Save the Credentials",
|
||||||
"clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
|
"clientCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
|
||||||
"generalSettingsDescription": "Configure the general settings for this client",
|
"generalSettingsDescription": "Configure the general settings for this client",
|
||||||
"clientUpdated": "Client updated",
|
"clientUpdated": "Client updated",
|
||||||
@@ -1454,9 +1494,7 @@
|
|||||||
"sitesFetchError": "An error occurred while fetching sites.",
|
"sitesFetchError": "An error occurred while fetching sites.",
|
||||||
"olmErrorFetchReleases": "An error occurred while fetching Olm releases.",
|
"olmErrorFetchReleases": "An error occurred while fetching Olm releases.",
|
||||||
"olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.",
|
"olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.",
|
||||||
"remoteSubnets": "Remote Subnets",
|
|
||||||
"enterCidrRange": "Enter CIDR range",
|
"enterCidrRange": "Enter CIDR range",
|
||||||
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
|
||||||
"resourceEnableProxy": "Enable Public Proxy",
|
"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",
|
||||||
@@ -1474,14 +1512,15 @@
|
|||||||
"enableHealthChecksDescription": "Monitor the health of this target. You can monitor a different endpoint than the target if required.",
|
"enableHealthChecksDescription": "Monitor the health of this target. You can monitor a different endpoint than the target if required.",
|
||||||
"healthScheme": "Method",
|
"healthScheme": "Method",
|
||||||
"healthSelectScheme": "Select Method",
|
"healthSelectScheme": "Select Method",
|
||||||
|
"healthCheckPortInvalid": "Health check port must be between 1 and 65535",
|
||||||
"healthCheckPath": "Path",
|
"healthCheckPath": "Path",
|
||||||
"healthHostname": "IP / Host",
|
"healthHostname": "IP / Host",
|
||||||
"healthPort": "Port",
|
"healthPort": "Port",
|
||||||
"healthCheckPathDescription": "The path to check for health status.",
|
"healthCheckPathDescription": "The path to check for health status.",
|
||||||
"healthyIntervalSeconds": "Healthy Interval",
|
"healthyIntervalSeconds": "Healthy Interval (sec)",
|
||||||
"unhealthyIntervalSeconds": "Unhealthy Interval",
|
"unhealthyIntervalSeconds": "Unhealthy Interval (sec)",
|
||||||
"IntervalSeconds": "Healthy Interval",
|
"IntervalSeconds": "Healthy Interval",
|
||||||
"timeoutSeconds": "Timeout",
|
"timeoutSeconds": "Timeout (sec)",
|
||||||
"timeIsInSeconds": "Time is in seconds",
|
"timeIsInSeconds": "Time is in seconds",
|
||||||
"retryAttempts": "Retry Attempts",
|
"retryAttempts": "Retry Attempts",
|
||||||
"expectedResponseCodes": "Expected Response Codes",
|
"expectedResponseCodes": "Expected Response Codes",
|
||||||
@@ -1517,16 +1556,22 @@
|
|||||||
"resourceEditDomain": "Edit Domain",
|
"resourceEditDomain": "Edit Domain",
|
||||||
"siteName": "Site Name",
|
"siteName": "Site Name",
|
||||||
"proxyPort": "Port",
|
"proxyPort": "Port",
|
||||||
"resourcesTableProxyResources": "Proxy Resources",
|
"resourcesTableProxyResources": "Public",
|
||||||
"resourcesTableClientResources": "Client Resources",
|
"resourcesTableClientResources": "Private",
|
||||||
"resourcesTableNoProxyResourcesFound": "No proxy resources found.",
|
"resourcesTableNoProxyResourcesFound": "No proxy resources found.",
|
||||||
"resourcesTableNoInternalResourcesFound": "No internal resources found.",
|
"resourcesTableNoInternalResourcesFound": "No internal resources found.",
|
||||||
"resourcesTableDestination": "Destination",
|
"resourcesTableDestination": "Destination",
|
||||||
"resourcesTableTheseResourcesForUseWith": "These resources are for use with",
|
"resourcesTableAlias": "Alias",
|
||||||
"resourcesTableClients": "Clients",
|
"resourcesTableClients": "Clients",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.",
|
"resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.",
|
||||||
|
"resourcesTableNoTargets": "No targets",
|
||||||
|
"resourcesTableHealthy": "Healthy",
|
||||||
|
"resourcesTableDegraded": "Degraded",
|
||||||
|
"resourcesTableOffline": "Offline",
|
||||||
|
"resourcesTableUnknown": "Unknown",
|
||||||
|
"resourcesTableNotMonitored": "Not monitored",
|
||||||
"editInternalResourceDialogEditClientResource": "Edit Client Resource",
|
"editInternalResourceDialogEditClientResource": "Edit Client Resource",
|
||||||
"editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.",
|
"editInternalResourceDialogUpdateResourceProperties": "Update the resource configuration and access controls for {resourceName}",
|
||||||
"editInternalResourceDialogResourceProperties": "Resource Properties",
|
"editInternalResourceDialogResourceProperties": "Resource Properties",
|
||||||
"editInternalResourceDialogName": "Name",
|
"editInternalResourceDialogName": "Name",
|
||||||
"editInternalResourceDialogProtocol": "Protocol",
|
"editInternalResourceDialogProtocol": "Protocol",
|
||||||
@@ -1545,11 +1590,22 @@
|
|||||||
"editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format",
|
"editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format",
|
||||||
"editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1",
|
"editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1",
|
||||||
"editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536",
|
"editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536",
|
||||||
|
"editInternalResourceDialogPortModeRequired": "Protocol, proxy port, and destination port are required for port mode",
|
||||||
|
"editInternalResourceDialogMode": "Mode",
|
||||||
|
"editInternalResourceDialogModePort": "Port",
|
||||||
|
"editInternalResourceDialogModeHost": "Host",
|
||||||
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
|
"editInternalResourceDialogDestination": "Destination",
|
||||||
|
"editInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.",
|
||||||
|
"editInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.",
|
||||||
|
"editInternalResourceDialogDestinationCidrDescription": "The CIDR range of the resource on the site's network.",
|
||||||
|
"editInternalResourceDialogAlias": "Alias",
|
||||||
|
"editInternalResourceDialogAliasDescription": "An optional internal DNS alias for this resource.",
|
||||||
"createInternalResourceDialogNoSitesAvailable": "No Sites Available",
|
"createInternalResourceDialogNoSitesAvailable": "No Sites Available",
|
||||||
"createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.",
|
"createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.",
|
||||||
"createInternalResourceDialogClose": "Close",
|
"createInternalResourceDialogClose": "Close",
|
||||||
"createInternalResourceDialogCreateClientResource": "Create Client Resource",
|
"createInternalResourceDialogCreateClientResource": "Create Client Resource",
|
||||||
"createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.",
|
"createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will only be accessible to clients connected to the organization",
|
||||||
"createInternalResourceDialogResourceProperties": "Resource Properties",
|
"createInternalResourceDialogResourceProperties": "Resource Properties",
|
||||||
"createInternalResourceDialogName": "Name",
|
"createInternalResourceDialogName": "Name",
|
||||||
"createInternalResourceDialogSite": "Site",
|
"createInternalResourceDialogSite": "Site",
|
||||||
@@ -1578,11 +1634,22 @@
|
|||||||
"createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format",
|
"createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format",
|
||||||
"createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1",
|
"createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1",
|
||||||
"createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536",
|
"createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536",
|
||||||
|
"createInternalResourceDialogPortModeRequired": "Protocol, proxy port, and destination port are required for port mode",
|
||||||
|
"createInternalResourceDialogMode": "Mode",
|
||||||
|
"createInternalResourceDialogModePort": "Port",
|
||||||
|
"createInternalResourceDialogModeHost": "Host",
|
||||||
|
"createInternalResourceDialogModeCidr": "CIDR",
|
||||||
|
"createInternalResourceDialogDestination": "Destination",
|
||||||
|
"createInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.",
|
||||||
|
"createInternalResourceDialogDestinationCidrDescription": "The CIDR range of the resource on the site's network.",
|
||||||
|
"createInternalResourceDialogAlias": "Alias",
|
||||||
|
"createInternalResourceDialogAliasDescription": "An optional internal DNS alias for this resource.",
|
||||||
"siteConfiguration": "Configuration",
|
"siteConfiguration": "Configuration",
|
||||||
"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 user devices and clients to access resources on this site. This can be changed later.",
|
||||||
"siteAddress": "Site Address",
|
"siteAddress": "Site Address (Advanced)",
|
||||||
"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.",
|
"siteAddressDescription": "The internal address of the site. Must fall within the organization's subnet.",
|
||||||
|
"siteNameDescription": "The display name of the site that can be changed later.",
|
||||||
"autoLoginExternalIdp": "Auto Login with External IDP",
|
"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",
|
||||||
@@ -1596,7 +1663,7 @@
|
|||||||
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
||||||
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
|
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "Remote Nodes",
|
"remoteExitNodeManageRemoteExitNodes": "Remote Nodes",
|
||||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
|
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend network connectivity and reduce reliance on the cloud",
|
||||||
"remoteExitNodes": "Nodes",
|
"remoteExitNodes": "Nodes",
|
||||||
"searchRemoteExitNodes": "Search nodes...",
|
"searchRemoteExitNodes": "Search nodes...",
|
||||||
"remoteExitNodeAdd": "Add Node",
|
"remoteExitNodeAdd": "Add Node",
|
||||||
@@ -1608,11 +1675,11 @@
|
|||||||
"sidebarRemoteExitNodes": "Remote Nodes",
|
"sidebarRemoteExitNodes": "Remote Nodes",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "Create Node",
|
"title": "Create Node",
|
||||||
"description": "Create a new node to extend your network connectivity",
|
"description": "Create a new node to extend network connectivity",
|
||||||
"viewAllButton": "View All Nodes",
|
"viewAllButton": "View All Nodes",
|
||||||
"strategy": {
|
"strategy": {
|
||||||
"title": "Creation Strategy",
|
"title": "Creation Strategy",
|
||||||
"description": "Choose this to manually configure your node or generate new credentials.",
|
"description": "Choose this to manually configure the node or generate new credentials.",
|
||||||
"adopt": {
|
"adopt": {
|
||||||
"title": "Adopt Node",
|
"title": "Adopt Node",
|
||||||
"description": "Choose this if you already have the credentials for the node."
|
"description": "Choose this if you already have the credentials for the node."
|
||||||
@@ -1633,7 +1700,7 @@
|
|||||||
},
|
},
|
||||||
"generate": {
|
"generate": {
|
||||||
"title": "Generated Credentials",
|
"title": "Generated Credentials",
|
||||||
"description": "Use these generated credentials to configure your node",
|
"description": "Use these generated credentials to configure the node",
|
||||||
"nodeIdTitle": "Node ID",
|
"nodeIdTitle": "Node ID",
|
||||||
"secretTitle": "Secret",
|
"secretTitle": "Secret",
|
||||||
"saveCredentialsTitle": "Add Credentials to Config",
|
"saveCredentialsTitle": "Add Credentials to Config",
|
||||||
@@ -1709,16 +1776,16 @@
|
|||||||
"idpTypeLabel": "Identity Provider Type",
|
"idpTypeLabel": "Identity Provider Type",
|
||||||
"roleMappingExpressionPlaceholder": "e.g., contains(groups, 'admin') && 'Admin' || 'Member'",
|
"roleMappingExpressionPlaceholder": "e.g., contains(groups, 'admin') && 'Admin' || 'Member'",
|
||||||
"idpGoogleConfiguration": "Google Configuration",
|
"idpGoogleConfiguration": "Google Configuration",
|
||||||
"idpGoogleConfigurationDescription": "Configure your Google OAuth2 credentials",
|
"idpGoogleConfigurationDescription": "Configure the Google OAuth2 credentials",
|
||||||
"idpGoogleClientIdDescription": "Your Google OAuth2 Client ID",
|
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
|
||||||
"idpGoogleClientSecretDescription": "Your Google OAuth2 Client Secret",
|
"idpGoogleClientSecretDescription": "Google OAuth2 Client Secret",
|
||||||
"idpAzureConfiguration": "Azure Entra ID Configuration",
|
"idpAzureConfiguration": "Azure Entra ID Configuration",
|
||||||
"idpAzureConfigurationDescription": "Configure your Azure Entra ID OAuth2 credentials",
|
"idpAzureConfigurationDescription": "Configure Azure Entra ID OAuth2 credentials",
|
||||||
"idpTenantId": "Tenant ID",
|
"idpTenantId": "Tenant ID",
|
||||||
"idpTenantIdPlaceholder": "your-tenant-id",
|
"idpTenantIdPlaceholder": "tenant-id",
|
||||||
"idpAzureTenantIdDescription": "Your Azure tenant ID (found in Azure Active Directory overview)",
|
"idpAzureTenantIdDescription": "Azure tenant ID (found in Azure Active Directory overview)",
|
||||||
"idpAzureClientIdDescription": "Your Azure App Registration Client ID",
|
"idpAzureClientIdDescription": "Azure App Registration Client ID",
|
||||||
"idpAzureClientSecretDescription": "Your Azure App Registration Client Secret",
|
"idpAzureClientSecretDescription": "Azure App Registration Client Secret",
|
||||||
"idpGoogleTitle": "Google",
|
"idpGoogleTitle": "Google",
|
||||||
"idpGoogleAlt": "Google",
|
"idpGoogleAlt": "Google",
|
||||||
"idpAzureTitle": "Azure Entra ID",
|
"idpAzureTitle": "Azure Entra ID",
|
||||||
@@ -1726,14 +1793,14 @@
|
|||||||
"idpGoogleConfigurationTitle": "Google Configuration",
|
"idpGoogleConfigurationTitle": "Google Configuration",
|
||||||
"idpAzureConfigurationTitle": "Azure Entra ID Configuration",
|
"idpAzureConfigurationTitle": "Azure Entra ID Configuration",
|
||||||
"idpTenantIdLabel": "Tenant ID",
|
"idpTenantIdLabel": "Tenant ID",
|
||||||
"idpAzureClientIdDescription2": "Your Azure App Registration Client ID",
|
"idpAzureClientIdDescription2": "Azure App Registration Client ID",
|
||||||
"idpAzureClientSecretDescription2": "Your Azure App Registration Client Secret",
|
"idpAzureClientSecretDescription2": "Azure App Registration Client Secret",
|
||||||
"idpGoogleDescription": "Google OAuth2/OIDC provider",
|
"idpGoogleDescription": "Google OAuth2/OIDC provider",
|
||||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||||
"subnet": "Subnet",
|
"subnet": "Subnet",
|
||||||
"subnetDescription": "The subnet for this organization's network configuration.",
|
"subnetDescription": "The subnet for this organization's network configuration.",
|
||||||
"authPage": "Auth Page",
|
"authPage": "Auth Page",
|
||||||
"authPageDescription": "Configure the auth page for your organization",
|
"authPageDescription": "Configure the auth page for the organization",
|
||||||
"authPageDomain": "Auth Page Domain",
|
"authPageDomain": "Auth Page Domain",
|
||||||
"noDomainSet": "No domain set",
|
"noDomainSet": "No domain set",
|
||||||
"changeDomain": "Change Domain",
|
"changeDomain": "Change Domain",
|
||||||
@@ -1743,7 +1810,7 @@
|
|||||||
"setAuthPageDomain": "Set Auth Page Domain",
|
"setAuthPageDomain": "Set Auth Page Domain",
|
||||||
"failedToFetchCertificate": "Failed to fetch certificate",
|
"failedToFetchCertificate": "Failed to fetch certificate",
|
||||||
"failedToRestartCertificate": "Failed to restart certificate",
|
"failedToRestartCertificate": "Failed to restart certificate",
|
||||||
"addDomainToEnableCustomAuthPages": "Add a domain to enable custom authentication pages for your organization",
|
"addDomainToEnableCustomAuthPages": "Add a domain to enable custom authentication pages for the organization",
|
||||||
"selectDomainForOrgAuthPage": "Select a domain for the organization's authentication page",
|
"selectDomainForOrgAuthPage": "Select a domain for the organization's authentication page",
|
||||||
"domainPickerProvidedDomain": "Provided Domain",
|
"domainPickerProvidedDomain": "Provided Domain",
|
||||||
"domainPickerFreeProvidedDomain": "Free Provided Domain",
|
"domainPickerFreeProvidedDomain": "Free Provided Domain",
|
||||||
@@ -1758,7 +1825,7 @@
|
|||||||
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" could not be made valid for {domain}.",
|
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" could not be made valid for {domain}.",
|
||||||
"domainPickerSubdomainSanitized": "Subdomain sanitized",
|
"domainPickerSubdomainSanitized": "Subdomain sanitized",
|
||||||
"domainPickerSubdomainCorrected": "\"{sub}\" was corrected to \"{sanitized}\"",
|
"domainPickerSubdomainCorrected": "\"{sub}\" was corrected to \"{sanitized}\"",
|
||||||
"orgAuthSignInTitle": "Sign in to your organization",
|
"orgAuthSignInTitle": "Sign in to the organization",
|
||||||
"orgAuthChooseIdpDescription": "Choose your identity provider to continue",
|
"orgAuthChooseIdpDescription": "Choose your identity provider to continue",
|
||||||
"orgAuthNoIdpConfigured": "This organization doesn't have any identity providers configured. You can log in with your Pangolin identity instead.",
|
"orgAuthNoIdpConfigured": "This organization doesn't have any identity providers configured. You can log in with your Pangolin identity instead.",
|
||||||
"orgAuthSignInWithPangolin": "Sign in with Pangolin",
|
"orgAuthSignInWithPangolin": "Sign in with Pangolin",
|
||||||
@@ -1776,7 +1843,7 @@
|
|||||||
"enableTwoFactorAuthentication": "Enable two-factor authentication",
|
"enableTwoFactorAuthentication": "Enable two-factor authentication",
|
||||||
"completeSecuritySteps": "Complete Security Steps",
|
"completeSecuritySteps": "Complete Security Steps",
|
||||||
"securitySettings": "Security Settings",
|
"securitySettings": "Security Settings",
|
||||||
"securitySettingsDescription": "Configure security policies for your organization",
|
"securitySettingsDescription": "Configure security policies for the organization",
|
||||||
"requireTwoFactorForAllUsers": "Require Two-Factor Authentication for All Users",
|
"requireTwoFactorForAllUsers": "Require Two-Factor Authentication for All Users",
|
||||||
"requireTwoFactorDescription": "When enabled, all internal users in this organization must have two-factor authentication enabled to access the organization.",
|
"requireTwoFactorDescription": "When enabled, all internal users in this organization must have two-factor authentication enabled to access the organization.",
|
||||||
"requireTwoFactorDisabledDescription": "This feature requires a valid license (Enterprise) or active subscription (SaaS)",
|
"requireTwoFactorDisabledDescription": "This feature requires a valid license (Enterprise) or active subscription (SaaS)",
|
||||||
@@ -1839,8 +1906,12 @@
|
|||||||
"enterpriseEdition": "Enterprise Edition",
|
"enterpriseEdition": "Enterprise Edition",
|
||||||
"unlicensed": "Unlicensed",
|
"unlicensed": "Unlicensed",
|
||||||
"beta": "Beta",
|
"beta": "Beta",
|
||||||
"manageClients": "Manage Clients",
|
"manageUserDevices": "User Devices",
|
||||||
"manageClientsDescription": "Clients are devices that can connect to your sites",
|
"manageUserDevicesDescription": "View and manage devices that users use to privately connect to resources",
|
||||||
|
"manageMachineClients": "Manage Machine Clients",
|
||||||
|
"manageMachineClientsDescription": "Create and manage clients that servers and systems use to privately connect to resources",
|
||||||
|
"clientsTableUserClients": "User",
|
||||||
|
"clientsTableMachineClients": "Machine",
|
||||||
"licenseTableValidUntil": "Valid Until",
|
"licenseTableValidUntil": "Valid Until",
|
||||||
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
|
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
|
||||||
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
|
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
|
||||||
@@ -1980,6 +2051,7 @@
|
|||||||
"clientMessageRemove": "Once removed, the client will no longer be able to connect to the site.",
|
"clientMessageRemove": "Once removed, the client will no longer be able to connect to the site.",
|
||||||
"sidebarLogs": "Logs",
|
"sidebarLogs": "Logs",
|
||||||
"request": "Request",
|
"request": "Request",
|
||||||
|
"requests": "Requests",
|
||||||
"logs": "Logs",
|
"logs": "Logs",
|
||||||
"logsSettingsDescription": "Monitor logs collected from this orginization",
|
"logsSettingsDescription": "Monitor logs collected from this orginization",
|
||||||
"searchLogs": "Search logs...",
|
"searchLogs": "Search logs...",
|
||||||
@@ -2005,6 +2077,7 @@
|
|||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Reason",
|
"reason": "Reason",
|
||||||
"requestLogs": "Request Logs",
|
"requestLogs": "Request Logs",
|
||||||
|
"requestAnalytics": "Request Analytics",
|
||||||
"host": "Host",
|
"host": "Host",
|
||||||
"location": "Location",
|
"location": "Location",
|
||||||
"actionLogs": "Action Logs",
|
"actionLogs": "Action Logs",
|
||||||
@@ -2014,6 +2087,7 @@
|
|||||||
"logRetention": "Log Retention",
|
"logRetention": "Log Retention",
|
||||||
"logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them",
|
"logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them",
|
||||||
"requestLogsDescription": "View detailed request logs for resources in this organization",
|
"requestLogsDescription": "View detailed request logs for resources in this organization",
|
||||||
|
"requestAnalyticsDescription": "View detailed request analytics for resources in this organization",
|
||||||
"logRetentionRequestLabel": "Request Log Retention",
|
"logRetentionRequestLabel": "Request Log Retention",
|
||||||
"logRetentionRequestDescription": "How long to retain request logs",
|
"logRetentionRequestDescription": "How long to retain request logs",
|
||||||
"logRetentionAccessLabel": "Access Log Retention",
|
"logRetentionAccessLabel": "Access Log Retention",
|
||||||
@@ -2037,7 +2111,7 @@
|
|||||||
"preferWildcardCert": "Prefer Wildcard Certificate",
|
"preferWildcardCert": "Prefer Wildcard Certificate",
|
||||||
"unverified": "Unverified",
|
"unverified": "Unverified",
|
||||||
"domainSetting": "Domain Settings",
|
"domainSetting": "Domain Settings",
|
||||||
"domainSettingDescription": "Configure settings for your domain",
|
"domainSettingDescription": "Configure settings for the domain",
|
||||||
"preferWildcardCertDescription": "Attempt to generate a wildcard certificate (require a properly configured certificate resolver).",
|
"preferWildcardCertDescription": "Attempt to generate a wildcard certificate (require a properly configured certificate resolver).",
|
||||||
"recordName": "Record Name",
|
"recordName": "Record Name",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
@@ -2051,15 +2125,15 @@
|
|||||||
"olmUpdateAvailableInfo": "An updated version of Olm is available. Please update to the latest version for the best experience.",
|
"olmUpdateAvailableInfo": "An updated version of Olm is available. Please update to the latest version for the best experience.",
|
||||||
"client": "Client",
|
"client": "Client",
|
||||||
"proxyProtocol": "Proxy Protocol Settings",
|
"proxyProtocol": "Proxy Protocol Settings",
|
||||||
"proxyProtocolDescription": "Configure Proxy Protocol to preserve client IP addresses for TCP/UDP services.",
|
"proxyProtocolDescription": "Configure Proxy Protocol to preserve client IP addresses for TCP services.",
|
||||||
"enableProxyProtocol": "Enable Proxy Protocol",
|
"enableProxyProtocol": "Enable Proxy Protocol",
|
||||||
"proxyProtocolInfo": "Preserve client IP addresses for TCP/UDP backends",
|
"proxyProtocolInfo": "Preserve client IP addresses for TCP backends",
|
||||||
"proxyProtocolVersion": "Proxy Protocol Version",
|
"proxyProtocolVersion": "Proxy Protocol Version",
|
||||||
"version1": " Version 1 (Recommended)",
|
"version1": " Version 1 (Recommended)",
|
||||||
"version2": "Version 2",
|
"version2": "Version 2",
|
||||||
"versionDescription": "Version 1 is text-based and widely supported. Version 2 is binary and more efficient but less compatible. Make sure servers transport is added to dynamic config.",
|
"versionDescription": "Version 1 is text-based and widely supported. Version 2 is binary and more efficient but less compatible. Make sure servers transport is added to dynamic config.",
|
||||||
"warning": "Warning",
|
"warning": "Warning",
|
||||||
"proxyProtocolWarning": "Your backend application must be configured to accept Proxy Protocol connections. If your backend doesn't support Proxy Protocol, enabling this will break all connections so only enable this if you know what you're doing. Make sure to configure your backend to trust Proxy Protocol headers from Traefik.",
|
"proxyProtocolWarning": "The backend application must be configured to accept Proxy Protocol connections. If your backend doesn't support Proxy Protocol, enabling this will break all connections so only enable this if you know what you're doing. Make sure to configure your backend to trust Proxy Protocol headers from Traefik.",
|
||||||
"restarting": "Restarting...",
|
"restarting": "Restarting...",
|
||||||
"manual": "Manual",
|
"manual": "Manual",
|
||||||
"messageSupport": "Message Support",
|
"messageSupport": "Message Support",
|
||||||
@@ -2082,6 +2156,43 @@
|
|||||||
"supportMessageSent": "Message Sent!",
|
"supportMessageSent": "Message Sent!",
|
||||||
"supportWillContact": "We'll be in touch shortly!",
|
"supportWillContact": "We'll be in touch shortly!",
|
||||||
"selectLogRetention": "Select log retention",
|
"selectLogRetention": "Select log retention",
|
||||||
|
"terms": "Terms",
|
||||||
|
"privacy": "Privacy",
|
||||||
|
"security": "Security",
|
||||||
|
"docs": "Docs",
|
||||||
|
"deviceActivation": "Device activation",
|
||||||
|
"deviceCodeInvalidFormat": "Code must be 9 characters (e.g., A1AJ-N5JD)",
|
||||||
|
"deviceCodeInvalidOrExpired": "Invalid or expired code",
|
||||||
|
"deviceCodeVerifyFailed": "Failed to verify device code",
|
||||||
|
"signedInAs": "Signed in as",
|
||||||
|
"deviceCodeEnterPrompt": "Enter the code displayed on the device",
|
||||||
|
"continue": "Continue",
|
||||||
|
"deviceUnknownLocation": "Unknown location",
|
||||||
|
"deviceAuthorizationRequested": "This authorization was requested from {location} on {date}. Make sure you trust this device as it will get access to the account.",
|
||||||
|
"deviceLabel": "Device: {deviceName}",
|
||||||
|
"deviceWantsAccess": "wants to access your account",
|
||||||
|
"deviceExistingAccess": "Existing access:",
|
||||||
|
"deviceFullAccess": "Full access to your account",
|
||||||
|
"deviceOrganizationsAccess": "Access to all organizations your account has access to",
|
||||||
|
"deviceAuthorize": "Authorize {applicationName}",
|
||||||
|
"deviceConnected": "Device Connected!",
|
||||||
|
"deviceAuthorizedMessage": "Device is authorized to access your account.",
|
||||||
|
"pangolinCloud": "Pangolin Cloud",
|
||||||
|
"viewDevices": "View Devices",
|
||||||
|
"viewDevicesDescription": "Manage your connected devices",
|
||||||
|
"noDevices": "No devices found",
|
||||||
|
"dateCreated": "Date Created",
|
||||||
|
"unnamedDevice": "Unnamed Device",
|
||||||
|
"deviceQuestionRemove": "Are you sure you want to delete this device?",
|
||||||
|
"deviceMessageRemove": "This action cannot be undone.",
|
||||||
|
"deviceDeleteConfirm": "Delete Device",
|
||||||
|
"deleteDevice": "Delete Device",
|
||||||
|
"errorLoadingDevices": "Error loading devices",
|
||||||
|
"failedToLoadDevices": "Failed to load devices",
|
||||||
|
"deviceDeleted": "Device deleted",
|
||||||
|
"deviceDeletedDescription": "The device has been successfully deleted.",
|
||||||
|
"errorDeletingDevice": "Error deleting device",
|
||||||
|
"failedToDeleteDevice": "Failed to delete device",
|
||||||
"showColumns": "Show Columns",
|
"showColumns": "Show Columns",
|
||||||
"hideColumns": "Hide Columns",
|
"hideColumns": "Hide Columns",
|
||||||
"columnVisibility": "Column Visibility",
|
"columnVisibility": "Column Visibility",
|
||||||
@@ -2095,5 +2206,46 @@
|
|||||||
"selectedResources": "Selected Resources",
|
"selectedResources": "Selected Resources",
|
||||||
"enableSelected": "Enable Selected",
|
"enableSelected": "Enable Selected",
|
||||||
"disableSelected": "Disable Selected",
|
"disableSelected": "Disable Selected",
|
||||||
"checkSelectedStatus": "Check Status of Selected"
|
"checkSelectedStatus": "Check Status of Selected",
|
||||||
}
|
"clients": "Clients",
|
||||||
|
"accessClientSelect": "Select machine clients",
|
||||||
|
"resourceClientDescription": "Machine clients that can access this resource",
|
||||||
|
"regenerate": "Regenerate",
|
||||||
|
"credentials": "Credentials",
|
||||||
|
"savecredentials": "Save Credentials",
|
||||||
|
"regeneratecredentials": "Re-key",
|
||||||
|
"regenerateCredentials": "Regenerate and save your credentials",
|
||||||
|
"generatedcredentials": "Generated Credentials",
|
||||||
|
"copyandsavethesecredentials": "Copy and save these credentials",
|
||||||
|
"copyandsavethesecredentialsdescription": "These credentials will not be shown again after you leave this page. Save them securely now.",
|
||||||
|
"credentialsSaved": "Credentials Saved",
|
||||||
|
"credentialsSavedDescription": "Credentials have been regenerated and saved successfully.",
|
||||||
|
"credentialsSaveError": "Credentials Save Error",
|
||||||
|
"credentialsSaveErrorDescription": "An error occurred while regenerating and saving the credentials.",
|
||||||
|
"regenerateCredentialsWarning": "Regenerating credentials will invalidate the previous ones. Make sure to update any configurations that use these credentials.",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"regenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials?",
|
||||||
|
"endpoint": "Endpoint",
|
||||||
|
"Id": "Id",
|
||||||
|
"SecretKey": "Secret Key",
|
||||||
|
"featureDisabledTooltip": "This feature is only available in the enterprise plan and require a license to use it.",
|
||||||
|
"niceId": "Nice ID",
|
||||||
|
"niceIdUpdated": "Nice ID Updated",
|
||||||
|
"niceIdUpdatedSuccessfully": "Nice ID Updated Successfully",
|
||||||
|
"niceIdUpdateError": "Error updating Nice ID",
|
||||||
|
"niceIdUpdateErrorDescription": "An error occurred while updating the Nice ID.",
|
||||||
|
"niceIdCannotBeEmpty": "Nice ID cannot be empty",
|
||||||
|
"enterIdentifier": "Enter identifier",
|
||||||
|
"identifier": "Identifier",
|
||||||
|
"deviceLoginUseDifferentAccount": "Not you? Use a different account.",
|
||||||
|
"deviceLoginDeviceRequestingAccessToAccount": "A device is requesting access to this account.",
|
||||||
|
"noData": "No Data",
|
||||||
|
"machineClients": "Machine Clients",
|
||||||
|
"install": "Install",
|
||||||
|
"run": "Run",
|
||||||
|
"clientNameDescription": "The display name of the client that can be changed later.",
|
||||||
|
"clientAddress": "Client Address (Advanced)",
|
||||||
|
"setupFailedToFetchSubnet": "Failed to fetch default subnet",
|
||||||
|
"setupSubnetAdvanced": "Subnet (Advanced)",
|
||||||
|
"setupSubnetDescription": "The subnet for this organization's internal network."
|
||||||
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,15 @@
|
|||||||
"settingsErrorUpdateDescription": "Ocurrió un error al actualizar ajustes",
|
"settingsErrorUpdateDescription": "Ocurrió un error al actualizar ajustes",
|
||||||
"sidebarCollapse": "Colapsar",
|
"sidebarCollapse": "Colapsar",
|
||||||
"sidebarExpand": "Expandir",
|
"sidebarExpand": "Expandir",
|
||||||
|
"productUpdateMoreInfo": "{noOfUpdates} actualizaciones más",
|
||||||
|
"productUpdateInfo": "{noOfUpdates} actualizaciones",
|
||||||
|
"productUpdateWhatsNew": "Novedades",
|
||||||
|
"productUpdateTitle": "Actualizaciones de producto",
|
||||||
|
"productUpdateEmpty": "Sin actualizaciones",
|
||||||
|
"dismissAll": "Descartar todo",
|
||||||
|
"pangolinUpdateAvailable": "Nueva versión disponible",
|
||||||
|
"pangolinUpdateAvailableInfo": "La versión {version} está lista para instalar",
|
||||||
|
"pangolinUpdateAvailableReleaseNotes": "Ver notas del lanzamiento",
|
||||||
"newtUpdateAvailable": "Nueva actualización disponible",
|
"newtUpdateAvailable": "Nueva actualización disponible",
|
||||||
"newtUpdateAvailableInfo": "Hay una nueva versión de Newt disponible. Actualice a la última versión para la mejor experiencia.",
|
"newtUpdateAvailableInfo": "Hay una nueva versión de Newt disponible. Actualice a la última versión para la mejor experiencia.",
|
||||||
"domainPickerEnterDomain": "Dominio",
|
"domainPickerEnterDomain": "Dominio",
|
||||||
@@ -1525,6 +1534,12 @@
|
|||||||
"resourcesTableTheseResourcesForUseWith": "Estos recursos son para uso con",
|
"resourcesTableTheseResourcesForUseWith": "Estos recursos son para uso con",
|
||||||
"resourcesTableClients": "Clientes",
|
"resourcesTableClients": "Clientes",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "y solo son accesibles internamente cuando se conectan con un cliente.",
|
"resourcesTableAndOnlyAccessibleInternally": "y solo son accesibles internamente cuando se conectan con un cliente.",
|
||||||
|
"resourcesTableNoTargets": "Sin objetivos",
|
||||||
|
"resourcesTableHealthy": "Saludable",
|
||||||
|
"resourcesTableDegraded": "Degrado",
|
||||||
|
"resourcesTableOffline": "Desconectado",
|
||||||
|
"resourcesTableUnknown": "Desconocido",
|
||||||
|
"resourcesTableNotMonitored": "No supervisado",
|
||||||
"editInternalResourceDialogEditClientResource": "Editar recurso del cliente",
|
"editInternalResourceDialogEditClientResource": "Editar recurso del cliente",
|
||||||
"editInternalResourceDialogUpdateResourceProperties": "Actualizar las propiedades del recurso y la configuración del objetivo para {resourceName}.",
|
"editInternalResourceDialogUpdateResourceProperties": "Actualizar las propiedades del recurso y la configuración del objetivo para {resourceName}.",
|
||||||
"editInternalResourceDialogResourceProperties": "Propiedades del recurso",
|
"editInternalResourceDialogResourceProperties": "Propiedades del recurso",
|
||||||
@@ -2095,5 +2110,31 @@
|
|||||||
"selectedResources": "Recursos seleccionados",
|
"selectedResources": "Recursos seleccionados",
|
||||||
"enableSelected": "Habilitar seleccionados",
|
"enableSelected": "Habilitar seleccionados",
|
||||||
"disableSelected": "Desactivar Seleccionado",
|
"disableSelected": "Desactivar Seleccionado",
|
||||||
"checkSelectedStatus": "Comprobar el estado de selección"
|
"checkSelectedStatus": "Comprobar el estado de selección",
|
||||||
}
|
"credentials": "Credenciales",
|
||||||
|
"savecredentials": "Guardar credenciales",
|
||||||
|
"regeneratecredentials": "Re-clave",
|
||||||
|
"regenerateCredentials": "Regenerar y guardar tus credenciales",
|
||||||
|
"generatedcredentials": "Credenciales generadas",
|
||||||
|
"copyandsavethesecredentials": "Copiar y guardar estas credenciales",
|
||||||
|
"copyandsavethesecredentialsdescription": "Estas credenciales no se mostrarán de nuevo después de salir de esta página. Guárdelas de forma segura ahora.",
|
||||||
|
"credentialsSaved": "Credenciales guardadas",
|
||||||
|
"credentialsSavedDescription": "Las credenciales se han regenerado y guardado correctamente.",
|
||||||
|
"credentialsSaveError": "Error al guardar las credenciales",
|
||||||
|
"credentialsSaveErrorDescription": "Se ha producido un error al regenerar y guardar las credenciales.",
|
||||||
|
"regenerateCredentialsWarning": "Regenerar las credenciales invalidará las anteriores. Asegúrese de actualizar cualquier configuración que use estas credenciales.",
|
||||||
|
"confirm": "Confirmar",
|
||||||
|
"regenerateCredentialsConfirmation": "¿Está seguro que desea regenerar las credenciales?",
|
||||||
|
"endpoint": "Endpoint",
|
||||||
|
"Id": "Id",
|
||||||
|
"SecretKey": "Clave secreta",
|
||||||
|
"featureDisabledTooltip": "Esta característica sólo está disponible en el plan empresarial y requiere una licencia para usarla.",
|
||||||
|
"niceId": "ID bonita",
|
||||||
|
"niceIdUpdated": "Bonito ID actualizado",
|
||||||
|
"niceIdUpdatedSuccessfully": "Bonito ID actualizado correctamente",
|
||||||
|
"niceIdUpdateError": "Error al actualizar Nice ID",
|
||||||
|
"niceIdUpdateErrorDescription": "Se ha producido un error al actualizar el ID de Niza.",
|
||||||
|
"niceIdCannotBeEmpty": "El ID de Niza no puede estar vacío",
|
||||||
|
"enterIdentifier": "Introducir identificador",
|
||||||
|
"identifier": "Identifier"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
{
|
{
|
||||||
"setupCreate": "Créez votre organisation, votre site et vos ressources",
|
"setupCreate": "Créez votre organisation, vos nœuds et vos ressources",
|
||||||
"setupNewOrg": "Nouvelle organisation",
|
"setupNewOrg": "Nouvelle organisation",
|
||||||
"setupCreateOrg": "Créer une organisation",
|
"setupCreateOrg": "Créer une organisation",
|
||||||
"setupCreateResources": "Créer des ressources",
|
"setupCreateResources": "Créer des ressources",
|
||||||
"setupOrgName": "Nom de l'organisation",
|
"setupOrgName": "Nom de l'organisation",
|
||||||
"orgDisplayName": "Ceci est le nom d'affichage de votre organisation.",
|
"orgDisplayName": "Ceci est le nom affiché de votre organisation.",
|
||||||
"orgId": "ID de l'organisation",
|
"orgId": "ID de l'organisation",
|
||||||
"setupIdentifierMessage": "Ceci est l'identifiant unique pour votre organisation. Il est séparé du nom affiché.",
|
"setupIdentifierMessage": "Ceci est l'identifiant unique de votre organisation. Il est différent du nom.",
|
||||||
"setupErrorIdentifier": "L'ID de l'organisation est déjà pris. Veuillez en choisir un autre.",
|
"setupErrorIdentifier": "Cet ID est déjà utilisé. Veuillez en choisir un autre.",
|
||||||
"componentsErrorNoMemberCreate": "Vous n'êtes actuellement membre d'aucune organisation. Créez une organisation pour commencer.",
|
"componentsErrorNoMemberCreate": "Vous n'êtes actuellement membre d'aucune organisation. Créez une organisation pour commencer.",
|
||||||
"componentsErrorNoMember": "Vous n'êtes actuellement membre d'aucune organisation.",
|
"componentsErrorNoMember": "Vous n'êtes actuellement membre d'aucune organisation.",
|
||||||
"welcome": "Bienvenue sur Pangolin !",
|
"welcome": "Bienvenue sur Pangolin !",
|
||||||
"welcomeTo": "Bienvenue chez",
|
"welcomeTo": "Bienvenue chez",
|
||||||
"componentsCreateOrg": "Créer une organisation",
|
"componentsCreateOrg": "Créer une organisation",
|
||||||
"componentsMember": "Vous êtes membre de {count, plural, =0 {aucune organisation} one {une organisation} other {# organisations}}.",
|
"componentsMember": "Vous {count, plural, =0 {n'} other {} }êtes membre {count, plural, =0 {d'aucune organisation} one {d'une organisation} other {de # organisations}}.",
|
||||||
"componentsInvalidKey": "Clés de licence invalides ou expirées détectées. Veuillez respecter les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
|
"componentsInvalidKey": "Clés de licence invalides ou expirées détectées. Veuillez respecter les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
|
||||||
"dismiss": "Rejeter",
|
"dismiss": "Rejeter",
|
||||||
"componentsLicenseViolation": "Violation de licence : ce serveur utilise {usedSites} sites, ce qui dépasse la limite autorisée de {maxSites} sites. Respectez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
|
"componentsLicenseViolation": "Violation de licence : ce serveur utilise {usedSites} nœuds, ce qui dépasse la limite autorisée de {maxSites} nœuds. Respectez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
|
||||||
"componentsSupporterMessage": "Merci de soutenir Pangolin en tant que {tier}!",
|
"componentsSupporterMessage": "Merci de soutenir Pangolin en tant que {tier}!",
|
||||||
"inviteErrorNotValid": "Nous sommes désolés, mais il semble que l'invitation à laquelle vous essayez d'accéder n'ait pas été acceptée ou ne soit plus valide.",
|
"inviteErrorNotValid": "Nous sommes désolés, mais il semble que l'invitation à laquelle vous essayez d'accéder n'ait pas été acceptée ou ne soit plus valide.",
|
||||||
"inviteErrorUser": "Nous sommes désolés, mais il semble que l'invitation à laquelle vous essayez d'accéder ne soit pas pour cet utilisateur.",
|
"inviteErrorUser": "Nous sommes désolés, mais il semble que l'invitation à laquelle vous essayez d'accéder ne soit pas pour cet utilisateur.",
|
||||||
"inviteLoginUser": "Veuillez vous assurer que vous êtes connecté avec le bon utilisateur.",
|
"inviteLoginUser": "Veuillez vous assurer que vous êtes connecté avec le bon compte.",
|
||||||
"inviteErrorNoUser": "Nous sommes désolés, mais il semble que l'invitation à laquelle vous essayez d'accéder ne concerne pas un utilisateur existant.",
|
"inviteErrorNoUser": "Nous sommes désolés, mais il semble que l'invitation à laquelle vous essayez d'accéder ne concerne pas un utilisateur existant.",
|
||||||
"inviteCreateUser": "Veuillez d'abord créer un compte.",
|
"inviteCreateUser": "Veuillez d'abord créer un compte.",
|
||||||
"goHome": "Retour à l'accueil",
|
"goHome": "Retour à l'accueil",
|
||||||
@@ -29,35 +29,35 @@
|
|||||||
"inviteNotAccepted": "Invitation non acceptée",
|
"inviteNotAccepted": "Invitation non acceptée",
|
||||||
"authCreateAccount": "Créez un compte pour commencer",
|
"authCreateAccount": "Créez un compte pour commencer",
|
||||||
"authNoAccount": "Vous n'avez pas de compte ?",
|
"authNoAccount": "Vous n'avez pas de compte ?",
|
||||||
"email": "Courriel",
|
"email": "Adresse mail",
|
||||||
"password": "Mot de passe",
|
"password": "Mot de passe",
|
||||||
"confirmPassword": "Confirmer le mot de passe",
|
"confirmPassword": "Confirmer le mot de passe",
|
||||||
"createAccount": "Créer un compte",
|
"createAccount": "Créer un compte",
|
||||||
"viewSettings": "Afficher les paramètres",
|
"viewSettings": "Afficher les paramètres",
|
||||||
"delete": "Supprimez",
|
"delete": "Supprimer",
|
||||||
"name": "Nom",
|
"name": "Nom",
|
||||||
"online": "En ligne",
|
"online": "En ligne",
|
||||||
"offline": "Hors ligne",
|
"offline": "Hors ligne",
|
||||||
"site": "Site",
|
"site": "Nœud",
|
||||||
"dataIn": "Données entrantes",
|
"dataIn": "Données reçues",
|
||||||
"dataOut": "Données sortantes",
|
"dataOut": "Données émises",
|
||||||
"connectionType": "Type de connexion",
|
"connectionType": "Type de connexion",
|
||||||
"tunnelType": "Type de tunnel",
|
"tunnelType": "Type de tunnel",
|
||||||
"local": "Locale",
|
"local": "Locale",
|
||||||
"edit": "Éditer",
|
"edit": "Modifier",
|
||||||
"siteConfirmDelete": "Confirmer la suppression du site",
|
"siteConfirmDelete": "Confirmer la suppression du nœud",
|
||||||
"siteDelete": "Supprimer le site",
|
"siteDelete": "Supprimer le nœud",
|
||||||
"siteMessageRemove": "Une fois supprimé, le site ne sera plus accessible. Toutes les cibles associées au site seront également supprimées.",
|
"siteMessageRemove": "Une fois supprimé, le nœud ne sera plus accessible. Toutes les cibles associées au nœud seront également supprimées.",
|
||||||
"siteQuestionRemove": "Êtes-vous sûr de vouloir supprimer le site de l'organisation ?",
|
"siteQuestionRemove": "Êtes-vous sûr de vouloir supprimer ce nœud de l'organisation ?",
|
||||||
"siteManageSites": "Gérer les sites",
|
"siteManageSites": "Gérer les nœuds",
|
||||||
"siteDescription": "Autoriser la connectivité à votre réseau via des tunnels sécurisés",
|
"siteDescription": "Autoriser la connexion à votre réseau via des tunnels sécurisés",
|
||||||
"siteCreate": "Créer un site",
|
"siteCreate": "Créer un nœud",
|
||||||
"siteCreateDescription2": "Suivez les étapes ci-dessous pour créer et connecter un nouveau site",
|
"siteCreateDescription2": "Suivez les étapes ci-dessous pour créer et connecter un nouveau nœud",
|
||||||
"siteCreateDescription": "Créez un nouveau site pour commencer à connecter vos ressources",
|
"siteCreateDescription": "Créez un nouveau nœud pour commencer à connecter vos ressources",
|
||||||
"close": "Fermer",
|
"close": "Fermer",
|
||||||
"siteErrorCreate": "Erreur lors de la création du site",
|
"siteErrorCreate": "Erreur lors de la création du nœud",
|
||||||
"siteErrorCreateKeyPair": "Paire de clés ou site par défaut introuvable",
|
"siteErrorCreateKeyPair": "Clés ou nœud par défaut introuvable",
|
||||||
"siteErrorCreateDefaults": "Les valeurs par défaut du site sont introuvables",
|
"siteErrorCreateDefaults": "Les valeurs par défaut du nœud sont introuvables",
|
||||||
"method": "Méthode",
|
"method": "Méthode",
|
||||||
"siteMethodDescription": "C'est ainsi que vous exposerez les connexions.",
|
"siteMethodDescription": "C'est ainsi que vous exposerez les connexions.",
|
||||||
"siteLearnNewt": "Apprenez à installer Newt sur votre système",
|
"siteLearnNewt": "Apprenez à installer Newt sur votre système",
|
||||||
@@ -65,12 +65,12 @@
|
|||||||
"siteLoadWGConfig": "Chargement de la configuration WireGuard...",
|
"siteLoadWGConfig": "Chargement de la configuration WireGuard...",
|
||||||
"siteDocker": "Développer pour obtenir plus de détails sur le déploiement Docker",
|
"siteDocker": "Développer pour obtenir plus de détails sur le déploiement Docker",
|
||||||
"toggle": "Activer/désactiver",
|
"toggle": "Activer/désactiver",
|
||||||
"dockerCompose": "Composition Docker",
|
"dockerCompose": "Docker Compose",
|
||||||
"dockerRun": "Exécution Docker",
|
"dockerRun": "Docker Run",
|
||||||
"siteLearnLocal": "Les sites locaux ne font pas de tunnel, en savoir plus",
|
"siteLearnLocal": "Les nœuds locaux ne font pas de tunnel, en savoir plus",
|
||||||
"siteConfirmCopy": "J'ai copié la configuration",
|
"siteConfirmCopy": "J'ai copié la configuration",
|
||||||
"searchSitesProgress": "Rechercher des sites...",
|
"searchSitesProgress": "Rechercher des nœuds...",
|
||||||
"siteAdd": "Ajouter un site",
|
"siteAdd": "Ajouter un nœud",
|
||||||
"siteInstallNewt": "Installer Newt",
|
"siteInstallNewt": "Installer Newt",
|
||||||
"siteInstallNewtDescription": "Faites fonctionner Newt sur votre système",
|
"siteInstallNewtDescription": "Faites fonctionner Newt sur votre système",
|
||||||
"WgConfiguration": "Configuration WireGuard",
|
"WgConfiguration": "Configuration WireGuard",
|
||||||
@@ -78,41 +78,41 @@
|
|||||||
"operatingSystem": "Système d'exploitation",
|
"operatingSystem": "Système d'exploitation",
|
||||||
"commands": "Commandes",
|
"commands": "Commandes",
|
||||||
"recommended": "Recommandé",
|
"recommended": "Recommandé",
|
||||||
"siteNewtDescription": "Pour une meilleure expérience d'utilisateur, utilisez Newt. Il utilise WireGuard sous le capot et vous permet de vous connecter à vos ressources privées par leur adresse LAN sur votre réseau privé à partir du tableau de bord Pangolin.",
|
"siteNewtDescription": "Pour une meilleure expérience d'utilisateur, utilisez Newt. Newt se base sur le protocole WireGuard et vous permet de vous connecter à vos ressources privées, par leur adresse LAN sur votre réseau privé, à partir de Pangolin.",
|
||||||
"siteRunsInDocker": "Exécute dans Docker",
|
"siteRunsInDocker": "Exécute dans Docker",
|
||||||
"siteRunsInShell": "Exécute en shell sur macOS, Linux et Windows",
|
"siteRunsInShell": "Fonctionne depuis le shell sur macOS, Linux et Windows",
|
||||||
"siteErrorDelete": "Erreur lors de la suppression du site",
|
"siteErrorDelete": "Erreur lors de la suppression du nœud",
|
||||||
"siteErrorUpdate": "Impossible de mettre à jour le site",
|
"siteErrorUpdate": "Impossible de mettre à jour le nœud",
|
||||||
"siteErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour du site.",
|
"siteErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour du nœud.",
|
||||||
"siteUpdated": "Site mis à jour",
|
"siteUpdated": "Nœud mis à jour",
|
||||||
"siteUpdatedDescription": "Le site a été mis à jour.",
|
"siteUpdatedDescription": "Le nœud a été mis à jour.",
|
||||||
"siteGeneralDescription": "Configurer les paramètres généraux de ce site",
|
"siteGeneralDescription": "Configurer les paramètres par défaut de ce nœud",
|
||||||
"siteSettingDescription": "Configurer les paramètres de votre site",
|
"siteSettingDescription": "Configurer les paramètres de votre nœud",
|
||||||
"siteSetting": "Réglages {siteName}",
|
"siteSetting": "Paramètres de {siteName}",
|
||||||
"siteNewtTunnel": "Tunnel Newt (Recommandé)",
|
"siteNewtTunnel": "Tunnel Newt (Recommandé)",
|
||||||
"siteNewtTunnelDescription": "La façon la plus simple de créer un point d'entrée dans votre réseau. Pas de configuration supplémentaire.",
|
"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.",
|
"siteWgDescriptionSaas": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise.",
|
||||||
"siteLocalDescription": "Ressources locales seulement. Pas de tunneling.",
|
"siteLocalDescription": "Ressources locales seulement. Pas de tunneling.",
|
||||||
"siteLocalDescriptionSaas": "Ressources locales uniquement. Pas de tunneling. Disponible uniquement sur les nœuds distants.",
|
"siteLocalDescriptionSaas": "Ressources locales uniquement. Pas de tunneling. Disponible uniquement sur les nœuds distants.",
|
||||||
"siteSeeAll": "Voir tous les sites",
|
"siteSeeAll": "Voir tous les nœuds",
|
||||||
"siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre site",
|
"siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre nœud",
|
||||||
"siteNewtCredentials": "Identifiants Newt",
|
"siteNewtCredentials": "Identifiants Newt",
|
||||||
"siteNewtCredentialsDescription": "C'est ainsi que Newt s'authentifiera avec le serveur",
|
"siteNewtCredentialsDescription": "C'est comme ça que Newt s'authentifiera avec le serveur",
|
||||||
"siteCredentialsSave": "Enregistrez vos identifiants",
|
"siteCredentialsSave": "Enregistrez vos identifiants",
|
||||||
"siteCredentialsSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Assurez-vous de le copier dans un endroit sécurisé.",
|
"siteCredentialsSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Assurez-vous de l'enregistrer dans un endroit sécurisé.",
|
||||||
"siteInfo": "Informations sur le site",
|
"siteInfo": "Informations du nœud",
|
||||||
"status": "Statut",
|
"status": "Statut",
|
||||||
"shareTitle": "Gérer les liens de partage",
|
"shareTitle": "Gérer les liens partageables",
|
||||||
"shareDescription": "Créez des liens partageables pour accorder un accès temporaire ou permanent à vos ressources",
|
"shareDescription": "Créez des liens partageables pour accorder un accès temporaire ou permanent à vos ressources",
|
||||||
"shareSearch": "Rechercher des liens de partage...",
|
"shareSearch": "Rechercher des liens partageables...",
|
||||||
"shareCreate": "Créer un lien de partage",
|
"shareCreate": "Créer un lien partageable",
|
||||||
"shareErrorDelete": "Impossible de supprimer le lien",
|
"shareErrorDelete": "Impossible de supprimer le lien",
|
||||||
"shareErrorDeleteMessage": "Une erreur s'est produite lors de la suppression du lien",
|
"shareErrorDeleteMessage": "Une erreur s'est produite lors de la suppression du lien",
|
||||||
"shareDeleted": "Lien supprimé",
|
"shareDeleted": "Lien supprimé",
|
||||||
"shareDeletedDescription": "Le lien a été supprimé",
|
"shareDeletedDescription": "Le lien a été supprimé",
|
||||||
"shareTokenDescription": "Votre jeton d'accès peut être passé de deux façons : en tant que paramètre de requête ou dans les en-têtes de la requête. Elles doivent être transmises par le client à chaque demande d'accès authentifié.",
|
"shareTokenDescription": "Votre jeton d'accès peut être fourni de deux façons : en tant que paramètre de requête ou dans les en-têtes de la requête. Il doit être transmis par le client à chaque demande d'accès authentifié.",
|
||||||
"accessToken": "Jeton d'accès",
|
"accessToken": "Jeton d'accès",
|
||||||
"usageExamples": "Exemples d'utilisation",
|
"usageExamples": "Exemples d'utilisation",
|
||||||
"tokenId": "ID du jeton",
|
"tokenId": "ID du jeton",
|
||||||
@@ -124,16 +124,16 @@
|
|||||||
"shareTokenSecurety": "Gardez votre jeton d'accès sécurisé. Ne le partagez pas dans des zones accessibles au public ou dans du code côté client.",
|
"shareTokenSecurety": "Gardez votre jeton d'accès sécurisé. Ne le partagez pas dans des zones accessibles au public ou dans du code côté client.",
|
||||||
"shareErrorFetchResource": "Impossible de récupérer les ressources",
|
"shareErrorFetchResource": "Impossible de récupérer les ressources",
|
||||||
"shareErrorFetchResourceDescription": "Une erreur est survenue lors de la récupération des ressources",
|
"shareErrorFetchResourceDescription": "Une erreur est survenue lors de la récupération des ressources",
|
||||||
"shareErrorCreate": "Impossible de créer le lien de partage",
|
"shareErrorCreate": "Impossible de créer le lien partageable",
|
||||||
"shareErrorCreateDescription": "Une erreur s'est produite lors de la création du lien de partage",
|
"shareErrorCreateDescription": "Une erreur s'est produite lors de la création du lien partageable",
|
||||||
"shareCreateDescription": "N'importe qui avec ce lien peut accéder à la ressource",
|
"shareCreateDescription": "N'importe qui avec ce lien peut accéder à la ressource",
|
||||||
"shareTitleOptional": "Titre (facultatif)",
|
"shareTitleOptional": "Titre (facultatif)",
|
||||||
"expireIn": "Expire dans",
|
"expireIn": "Expire dans",
|
||||||
"neverExpire": "N'expire jamais",
|
"neverExpire": "N'expire jamais",
|
||||||
"shareExpireDescription": "La durée d'expiration correspond à la période pendant laquelle le lien sera utilisable et permettra d'accéder à la ressource. Passé ce délai, le lien ne fonctionnera plus et les utilisateurs qui l'ont utilisé perdront l'accès à la ressource.",
|
"shareExpireDescription": "Le délai d'expiration correspond à la période pendant laquelle le lien sera utilisable et permettra d'accéder à la ressource. Passé ce délai, le lien ne fonctionnera plus et les utilisateurs qui l'ont utilisé perdront l'accès à la ressource.",
|
||||||
"shareSeeOnce": "Vous ne pourrez voir ce lien qu'une seule fois. Assurez-vous de le copier.",
|
"shareSeeOnce": "Vous ne pourrez voir ce lien qu'une seule fois. N'oubliez pas de le copier.",
|
||||||
"shareAccessHint": "N'importe qui avec ce lien peut accéder à la ressource. Partagez-le avec précaution.",
|
"shareAccessHint": "N'importe qui avec ce lien peut accéder à la ressource. Partagez-le avec précaution.",
|
||||||
"shareTokenUsage": "Voir Utilisation du jeton d'accès",
|
"shareTokenUsage": "Voir l'utilisation du jeton d'accès",
|
||||||
"createLink": "Créer un lien",
|
"createLink": "Créer un lien",
|
||||||
"resourcesNotFound": "Aucune ressource trouvée",
|
"resourcesNotFound": "Aucune ressource trouvée",
|
||||||
"resourceSearch": "Rechercher des ressources",
|
"resourceSearch": "Rechercher des ressources",
|
||||||
@@ -145,43 +145,43 @@
|
|||||||
"never": "Jamais",
|
"never": "Jamais",
|
||||||
"shareErrorSelectResource": "Veuillez sélectionner une ressource",
|
"shareErrorSelectResource": "Veuillez sélectionner une ressource",
|
||||||
"resourceTitle": "Gérer les ressources",
|
"resourceTitle": "Gérer les ressources",
|
||||||
"resourceDescription": "Créez des proxy sécurisés pour vos applications privées",
|
"resourceDescription": "Créez des proxys sécurisés pour vos applications privées",
|
||||||
"resourcesSearch": "Rechercher des ressources...",
|
"resourcesSearch": "Chercher des ressources...",
|
||||||
"resourceAdd": "Ajouter une ressource",
|
"resourceAdd": "Ajouter une ressource",
|
||||||
"resourceErrorDelte": "Erreur de suppression de la ressource",
|
"resourceErrorDelte": "Erreur lors de la de suppression de la ressource",
|
||||||
"authentication": "Authentification",
|
"authentication": "Authentification",
|
||||||
"protected": "Protégé",
|
"protected": "Protégé",
|
||||||
"notProtected": "Non Protégé",
|
"notProtected": "Non Protégé",
|
||||||
"resourceMessageRemove": "Une fois supprimée, la ressource ne sera plus accessible. Toutes les cibles associées à la ressource seront également supprimées.",
|
"resourceMessageRemove": "Une fois supprimée, la ressource ne sera plus accessible. Toutes les cibles associées à la ressource seront également supprimées.",
|
||||||
"resourceQuestionRemove": "Êtes-vous sûr de vouloir supprimer la ressource de l'organisation ?",
|
"resourceQuestionRemove": "Êtes-vous sûr de vouloir retirer la ressource de l'organisation ?",
|
||||||
"resourceHTTP": "Ressource HTTPS",
|
"resourceHTTP": "Ressource HTTPS",
|
||||||
"resourceHTTPDescription": "Requêtes de proxy vers votre application via HTTPS en utilisant un sous-domaine ou un domaine de base.",
|
"resourceHTTPDescription": "Requêtes de proxy vers votre application via HTTPS en utilisant un sous-domaine ou un domaine racine.",
|
||||||
"resourceRaw": "Ressource TCP/UDP brute",
|
"resourceRaw": "Ressource TCP/UDP brute",
|
||||||
"resourceRawDescription": "Demandes de proxy vers votre application via TCP/UDP en utilisant un numéro de port.",
|
"resourceRawDescription": "Demandes de proxy vers votre application via TCP/UDP en utilisant un port.",
|
||||||
"resourceCreate": "Créer une ressource",
|
"resourceCreate": "Créer une ressource",
|
||||||
"resourceCreateDescription": "Suivez les étapes ci-dessous pour créer une nouvelle ressource",
|
"resourceCreateDescription": "Suivez les étapes ci-dessous pour créer une nouvelle ressource",
|
||||||
"resourceSeeAll": "Voir toutes les ressources",
|
"resourceSeeAll": "Voir toutes les ressources",
|
||||||
"resourceInfo": "Informations sur la ressource",
|
"resourceInfo": "Informations sur la ressource",
|
||||||
"resourceNameDescription": "Ceci est le nom d'affichage de la ressource.",
|
"resourceNameDescription": "Ceci est le nom d'affichage de la ressource.",
|
||||||
"siteSelect": "Sélectionner un site",
|
"siteSelect": "Sélectionnez un nœud",
|
||||||
"siteSearch": "Chercher un site",
|
"siteSearch": "Chercher un nœud",
|
||||||
"siteNotFound": "Aucun site trouvé.",
|
"siteNotFound": "Aucun nœud trouvé.",
|
||||||
"selectCountry": "Sélectionnez un pays",
|
"selectCountry": "Sélectionnez un pays",
|
||||||
"searchCountries": "Recherchez des pays...",
|
"searchCountries": "Recherchez des pays...",
|
||||||
"noCountryFound": "Aucun pays trouvé.",
|
"noCountryFound": "Aucun pays trouvé.",
|
||||||
"siteSelectionDescription": "Ce site fournira la connectivité à la cible.",
|
"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étermine comment vous voulez accéder à votre ressource",
|
||||||
"resourceHTTPSSettings": "Paramètres HTTPS",
|
"resourceHTTPSSettings": "Paramètres HTTPS",
|
||||||
"resourceHTTPSSettingsDescription": "Configurer comment votre ressource sera accédée via HTTPS",
|
"resourceHTTPSSettingsDescription": "Configure comment votre ressource sera accédée via HTTPS",
|
||||||
"domainType": "Type de domaine",
|
"domainType": "Type de domaine",
|
||||||
"subdomain": "Sous-domaine",
|
"subdomain": "Sous-domaine",
|
||||||
"baseDomain": "Domaine de base",
|
"baseDomain": "Domaine racine",
|
||||||
"subdomnainDescription": "Le sous-domaine où votre ressource sera accessible.",
|
"subdomnainDescription": "Le sous-domaine depuis lequel cette ressource sera accessible.",
|
||||||
"resourceRawSettings": "Paramètres TCP/UDP",
|
"resourceRawSettings": "Paramètres TCP/UDP",
|
||||||
"resourceRawSettingsDescription": "Configurer comment votre ressource sera accédée via TCP/UDP. Vous mappez la ressource à un port sur le serveur Pangolin, de sorte que vous puissiez accéder à la ressource depuis server-public-ip:mapped-port.",
|
"resourceRawSettingsDescription": "Configurer comment votre ressource sera accédée via TCP/UDP. Vous mappez la ressource à un port sur le serveur Pangolin, de sorte que vous puissiez accéder à la ressource depuis ip-publique-du-serveur:port-mappé.",
|
||||||
"protocol": "Protocole",
|
"protocol": "Protocole",
|
||||||
"protocolSelect": "Sélectionner un protocole",
|
"protocolSelect": "Choisir un protocole",
|
||||||
"resourcePortNumber": "Numéro de port",
|
"resourcePortNumber": "Numéro de port",
|
||||||
"resourcePortNumberDescription": "Le numéro de port externe pour les requêtes de proxy.",
|
"resourcePortNumberDescription": "Le numéro de port externe pour les requêtes de proxy.",
|
||||||
"cancel": "Abandonner",
|
"cancel": "Abandonner",
|
||||||
@@ -203,17 +203,17 @@
|
|||||||
"internal": "Interne",
|
"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 de {resourceName}",
|
||||||
"alwaysAllow": "Toujours autoriser",
|
"alwaysAllow": "Toujours autoriser",
|
||||||
"alwaysDeny": "Toujours refuser",
|
"alwaysDeny": "Toujours refuser",
|
||||||
"passToAuth": "Passer à l'authentification",
|
"passToAuth": "Passer à l'authentification",
|
||||||
"orgSettingsDescription": "Configurer les paramètres généraux de votre organisation",
|
"orgSettingsDescription": "Configurer les paramètres 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",
|
||||||
"saveGeneralSettings": "Enregistrer les paramètres généraux",
|
"saveGeneralSettings": "Enregistrer les paramètres généraux",
|
||||||
"saveSettings": "Enregistrer les paramètres",
|
"saveSettings": "Enregistrer les paramètres",
|
||||||
"orgDangerZone": "Zone de danger",
|
"orgDangerZone": "Zone dangereuse",
|
||||||
"orgDangerZoneDescription": "Une fois que vous supprimez cette organisation, il n'y a pas de retour en arrière. Soyez certain.",
|
"orgDangerZoneDescription": "Une fois cette organisation supprimée, elle ne pourra plus être restaurée. Faites attention.",
|
||||||
"orgDelete": "Supprimer l'organisation",
|
"orgDelete": "Supprimer l'organisation",
|
||||||
"orgDeleteConfirm": "Confirmer la suppression de l'organisation",
|
"orgDeleteConfirm": "Confirmer la suppression de l'organisation",
|
||||||
"orgMessageRemove": "Cette action est irréversible et supprimera toutes les données associées.",
|
"orgMessageRemove": "Cette action est irréversible et supprimera toutes les données associées.",
|
||||||
@@ -224,7 +224,7 @@
|
|||||||
"orgErrorUpdate": "Échec de la mise à jour de l'organisation",
|
"orgErrorUpdate": "Échec de la mise à jour de l'organisation",
|
||||||
"orgErrorUpdateMessage": "Une erreur s'est produite lors de la mise à jour de l'organisation.",
|
"orgErrorUpdateMessage": "Une erreur s'est produite lors de la mise à jour de l'organisation.",
|
||||||
"orgErrorFetch": "Impossible de récupérer les organisations",
|
"orgErrorFetch": "Impossible de récupérer les organisations",
|
||||||
"orgErrorFetchMessage": "Une erreur s'est produite lors de la liste de vos organisations",
|
"orgErrorFetchMessage": "Une erreur s'est produite lors de la récupération des organisations",
|
||||||
"orgErrorDelete": "Échec de la suppression de l'organisation",
|
"orgErrorDelete": "Échec de la suppression de l'organisation",
|
||||||
"orgErrorDeleteMessage": "Une erreur s'est produite lors de la suppression de l'organisation.",
|
"orgErrorDeleteMessage": "Une erreur s'est produite lors de la suppression de l'organisation.",
|
||||||
"orgDeleted": "Organisation supprimée",
|
"orgDeleted": "Organisation supprimée",
|
||||||
@@ -233,21 +233,21 @@
|
|||||||
"orgMissingMessage": "Impossible de régénérer l'invitation sans un ID d'organisation.",
|
"orgMissingMessage": "Impossible de régénérer l'invitation sans un ID d'organisation.",
|
||||||
"accessUsersManage": "Gérer les utilisateurs",
|
"accessUsersManage": "Gérer les utilisateurs",
|
||||||
"accessUsersDescription": "Invitez des utilisateurs et ajoutez-les aux rôles pour gérer l'accès à votre organisation",
|
"accessUsersDescription": "Invitez des utilisateurs et ajoutez-les aux rôles pour gérer l'accès à votre organisation",
|
||||||
"accessUsersSearch": "Rechercher des utilisateurs...",
|
"accessUsersSearch": "Chercher des utilisateurs...",
|
||||||
"accessUserCreate": "Créer un utilisateur",
|
"accessUserCreate": "Créer un utilisateur",
|
||||||
"accessUserRemove": "Supprimer l'utilisateur",
|
"accessUserRemove": "Supprimer un utilisateur",
|
||||||
"username": "Nom d'utilisateur",
|
"username": "Nom d'utilisateur",
|
||||||
"identityProvider": "Fournisseur d'identité",
|
"identityProvider": "Fournisseur d'identité",
|
||||||
"role": "Rôle",
|
"role": "Rôle",
|
||||||
"nameRequired": "Le nom est requis",
|
"nameRequired": "Le nom est requis",
|
||||||
"accessRolesManage": "Gérer les rôles",
|
"accessRolesManage": "Gérer les rôles",
|
||||||
"accessRolesDescription": "Configurer les rôles pour gérer l'accès à votre organisation",
|
"accessRolesDescription": "Configurer les rôles pour gérer l'accès à votre organisation",
|
||||||
"accessRolesSearch": "Rechercher des rôles...",
|
"accessRolesSearch": "Chercher des rôles...",
|
||||||
"accessRolesAdd": "Ajouter un rôle",
|
"accessRolesAdd": "Ajouter un rôle",
|
||||||
"accessRoleDelete": "Supprimer le rôle",
|
"accessRoleDelete": "Supprimer le rôle",
|
||||||
"description": "Libellé",
|
"description": "Libellé",
|
||||||
"inviteTitle": "Invitations ouvertes",
|
"inviteTitle": "Invitations actives",
|
||||||
"inviteDescription": "Gérer vos invitations à d'autres utilisateurs",
|
"inviteDescription": "Gérez les invitations des autres utilisateurs",
|
||||||
"inviteSearch": "Rechercher des invitations...",
|
"inviteSearch": "Rechercher des invitations...",
|
||||||
"minutes": "Minutes",
|
"minutes": "Minutes",
|
||||||
"hours": "Heures",
|
"hours": "Heures",
|
||||||
@@ -256,41 +256,41 @@
|
|||||||
"months": "Mois",
|
"months": "Mois",
|
||||||
"years": "Années",
|
"years": "Années",
|
||||||
"day": "{count, plural, one {# jour} other {# jours}}",
|
"day": "{count, plural, one {# jour} other {# jours}}",
|
||||||
"apiKeysTitle": "Informations sur la clé API",
|
"apiKeysTitle": "Informations sur la clé d'API",
|
||||||
"apiKeysConfirmCopy2": "Vous devez confirmer que vous avez copié la clé API.",
|
"apiKeysConfirmCopy2": "Vous devez confirmer que vous avez copié la clé d'API.",
|
||||||
"apiKeysErrorCreate": "Erreur lors de la création de la clé API",
|
"apiKeysErrorCreate": "Erreur lors de la création de la clé API",
|
||||||
"apiKeysErrorSetPermission": "Erreur lors de la définition des permissions",
|
"apiKeysErrorSetPermission": "Erreur lors de la définition des permissions",
|
||||||
"apiKeysCreate": "Générer une clé API",
|
"apiKeysCreate": "Générer une clé d'API",
|
||||||
"apiKeysCreateDescription": "Générer une nouvelle clé API pour votre organisation",
|
"apiKeysCreateDescription": "Générer une nouvelle clé d'API pour votre organisation",
|
||||||
"apiKeysGeneralSettings": "Permissions",
|
"apiKeysGeneralSettings": "Permissions",
|
||||||
"apiKeysGeneralSettingsDescription": "Déterminez ce que cette clé API peut faire",
|
"apiKeysGeneralSettingsDescription": "Déterminez ce que cette clé d\"API peut faire",
|
||||||
"apiKeysList": "Votre clé API",
|
"apiKeysList": "Votre clé d\"API",
|
||||||
"apiKeysSave": "Enregistrer votre clé API",
|
"apiKeysSave": "Enregistrer votre clé API",
|
||||||
"apiKeysSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Assurez-vous de la copier dans un endroit sécurisé.",
|
"apiKeysSaveDescription": "Vous ne pourrez la voir qu'une seule fois. Assurez-vous de la copier dans un endroit sécurisé.",
|
||||||
"apiKeysInfo": "Votre clé API est :",
|
"apiKeysInfo": "Votre clé d'API est :",
|
||||||
"apiKeysConfirmCopy": "J'ai copié la clé API",
|
"apiKeysConfirmCopy": "J'ai copié la clé d\"API",
|
||||||
"generate": "Générer",
|
"generate": "Générer",
|
||||||
"done": "Terminé",
|
"done": "Terminé",
|
||||||
"apiKeysSeeAll": "Voir toutes les clés API",
|
"apiKeysSeeAll": "Voir toutes les clés d\"API",
|
||||||
"apiKeysPermissionsErrorLoadingActions": "Erreur lors du chargement des actions de la clé API",
|
"apiKeysPermissionsErrorLoadingActions": "Erreur lors du chargement des actions de la clé d\"API",
|
||||||
"apiKeysPermissionsErrorUpdate": "Erreur lors de la définition des permissions",
|
"apiKeysPermissionsErrorUpdate": "Erreur lors de la définition des permissions",
|
||||||
"apiKeysPermissionsUpdated": "Permissions mises à jour",
|
"apiKeysPermissionsUpdated": "Permissions mises à jour",
|
||||||
"apiKeysPermissionsUpdatedDescription": "Les permissions ont été mises à jour.",
|
"apiKeysPermissionsUpdatedDescription": "Les permissions ont été mises à jour.",
|
||||||
"apiKeysPermissionsGeneralSettings": "Permissions",
|
"apiKeysPermissionsGeneralSettings": "Permissions",
|
||||||
"apiKeysPermissionsGeneralSettingsDescription": "Déterminez ce que cette clé API peut faire",
|
"apiKeysPermissionsGeneralSettingsDescription": "Déterminez ce que cette clé d'API peut faire",
|
||||||
"apiKeysPermissionsSave": "Enregistrer les permissions",
|
"apiKeysPermissionsSave": "Enregistrer les permissions",
|
||||||
"apiKeysPermissionsTitle": "Permissions",
|
"apiKeysPermissionsTitle": "Permissions",
|
||||||
"apiKeys": "Clés API",
|
"apiKeys": "Clés d'API",
|
||||||
"searchApiKeys": "Rechercher des clés API...",
|
"searchApiKeys": "Rechercher des clés d'API...",
|
||||||
"apiKeysAdd": "Générer une clé API",
|
"apiKeysAdd": "Générer une clé d'API",
|
||||||
"apiKeysErrorDelete": "Erreur lors de la suppression de la clé API",
|
"apiKeysErrorDelete": "Erreur lors de la suppression de la clé d'API",
|
||||||
"apiKeysErrorDeleteMessage": "Erreur lors de la suppression de la clé API",
|
"apiKeysErrorDeleteMessage": "Erreur lors de la suppression de la clé d'API",
|
||||||
"apiKeysQuestionRemove": "Êtes-vous sûr de vouloir supprimer la clé API de l'organisation ?",
|
"apiKeysQuestionRemove": "Êtes-vous sûr de vouloir supprimer la clé d'API de l'organisation ?",
|
||||||
"apiKeysMessageRemove": "Une fois supprimée, la clé API ne pourra plus être utilisée.",
|
"apiKeysMessageRemove": "Une fois supprimée, la clé d'API ne pourra plus être utilisée.",
|
||||||
"apiKeysDeleteConfirm": "Confirmer la suppression de la clé API",
|
"apiKeysDeleteConfirm": "Confirmer la suppression de la clé d'API",
|
||||||
"apiKeysDelete": "Supprimer la clé API",
|
"apiKeysDelete": "Supprimer la clé d'API",
|
||||||
"apiKeysManage": "Gérer les clés API",
|
"apiKeysManage": "Gérer les clés d'API",
|
||||||
"apiKeysDescription": "Les clés API sont utilisées pour s'authentifier avec l'API d'intégration",
|
"apiKeysDescription": "Les clés d'API sont utilisées pour s'authentifier avec l'API d'intégration",
|
||||||
"apiKeysSettings": "Paramètres de {apiKeyName}",
|
"apiKeysSettings": "Paramètres de {apiKeyName}",
|
||||||
"userTitle": "Gérer tous les utilisateurs",
|
"userTitle": "Gérer tous les utilisateurs",
|
||||||
"userDescription": "Voir et gérer tous les utilisateurs du système",
|
"userDescription": "Voir et gérer tous les utilisateurs du système",
|
||||||
@@ -305,10 +305,10 @@
|
|||||||
"userQuestionRemove": "Êtes-vous sûr de vouloir supprimer définitivement l'utilisateur du serveur?",
|
"userQuestionRemove": "Êtes-vous sûr de vouloir supprimer définitivement l'utilisateur du serveur?",
|
||||||
"licenseKey": "Clé de licence",
|
"licenseKey": "Clé de licence",
|
||||||
"valid": "Valide",
|
"valid": "Valide",
|
||||||
"numberOfSites": "Nombre de sites",
|
"numberOfSites": "Nombre de nœuds",
|
||||||
"licenseKeySearch": "Rechercher des clés de licence...",
|
"licenseKeySearch": "Rechercher des clés de licence...",
|
||||||
"licenseKeyAdd": "Ajouter une clé de licence",
|
"licenseKeyAdd": "Ajouter une clé de licence",
|
||||||
"type": "Type de texte",
|
"type": "Type",
|
||||||
"licenseKeyRequired": "La clé de licence est requise",
|
"licenseKeyRequired": "La clé de licence est requise",
|
||||||
"licenseTermsAgree": "Vous devez accepter les conditions de licence",
|
"licenseTermsAgree": "Vous devez accepter les conditions de licence",
|
||||||
"licenseErrorKeyLoad": "Impossible de charger les clés de licence",
|
"licenseErrorKeyLoad": "Impossible de charger les clés de licence",
|
||||||
@@ -611,7 +611,7 @@
|
|||||||
"newtId": "ID Newt",
|
"newtId": "ID Newt",
|
||||||
"newtSecretKey": "Clé secrète Newt",
|
"newtSecretKey": "Clé secrète Newt",
|
||||||
"architecture": "Architecture",
|
"architecture": "Architecture",
|
||||||
"sites": "Espaces",
|
"sites": "Nœuds",
|
||||||
"siteWgAnyClients": "Utilisez n'importe quel client WireGuard pour vous connecter. Vous devrez adresser vos ressources internes en utilisant l'IP du pair.",
|
"siteWgAnyClients": "Utilisez n'importe quel client WireGuard pour vous connecter. Vous devrez adresser vos ressources internes en utilisant l'IP du pair.",
|
||||||
"siteWgCompatibleAllClients": "Compatible avec tous les clients WireGuard",
|
"siteWgCompatibleAllClients": "Compatible avec tous les clients WireGuard",
|
||||||
"siteWgManualConfigurationRequired": "Configuration manuelle requise",
|
"siteWgManualConfigurationRequired": "Configuration manuelle requise",
|
||||||
@@ -1021,7 +1021,7 @@
|
|||||||
"actionDeleteSite": "Supprimer un site",
|
"actionDeleteSite": "Supprimer un site",
|
||||||
"actionGetSite": "Obtenir un site",
|
"actionGetSite": "Obtenir un site",
|
||||||
"actionListSites": "Lister les sites",
|
"actionListSites": "Lister les sites",
|
||||||
"actionApplyBlueprint": "Appliquer le Plan",
|
"actionApplyBlueprint": "Appliquer la Config",
|
||||||
"setupToken": "Jeton de configuration",
|
"setupToken": "Jeton de configuration",
|
||||||
"setupTokenDescription": "Entrez le jeton de configuration depuis la console du serveur.",
|
"setupTokenDescription": "Entrez le jeton de configuration depuis la console du serveur.",
|
||||||
"setupTokenRequired": "Le jeton de configuration est requis.",
|
"setupTokenRequired": "Le jeton de configuration est requis.",
|
||||||
@@ -1149,7 +1149,7 @@
|
|||||||
"apiKeysErrorNoUpdate": "Pas de clé API à mettre à jour",
|
"apiKeysErrorNoUpdate": "Pas de clé API à mettre à jour",
|
||||||
"sidebarOverview": "Aperçu",
|
"sidebarOverview": "Aperçu",
|
||||||
"sidebarHome": "Domicile",
|
"sidebarHome": "Domicile",
|
||||||
"sidebarSites": "Espaces",
|
"sidebarSites": "Nœuds",
|
||||||
"sidebarResources": "Ressource",
|
"sidebarResources": "Ressource",
|
||||||
"sidebarAccessControl": "Contrôle d'accès",
|
"sidebarAccessControl": "Contrôle d'accès",
|
||||||
"sidebarUsers": "Utilisateurs",
|
"sidebarUsers": "Utilisateurs",
|
||||||
@@ -1163,26 +1163,26 @@
|
|||||||
"sidebarLicense": "Licence",
|
"sidebarLicense": "Licence",
|
||||||
"sidebarClients": "Clients",
|
"sidebarClients": "Clients",
|
||||||
"sidebarDomains": "Domaines",
|
"sidebarDomains": "Domaines",
|
||||||
"sidebarBluePrints": "Plans",
|
"sidebarBluePrints": "Configs",
|
||||||
"blueprints": "Plans",
|
"blueprints": "Configs",
|
||||||
"blueprintsDescription": "Appliquer les configurations déclaratives et afficher les exécutions précédentes",
|
"blueprintsDescription": "Appliquer les configurations déclaratives et afficher les exécutions précédentes",
|
||||||
"blueprintAdd": "Ajouter un Plan",
|
"blueprintAdd": "Ajouter une Config",
|
||||||
"blueprintGoBack": "Voir tous les plans",
|
"blueprintGoBack": "Voir toutes les Configs",
|
||||||
"blueprintCreate": "Créer un Plan",
|
"blueprintCreate": "Créer une Config",
|
||||||
"blueprintCreateDescription2": "Suivez les étapes ci-dessous pour créer et appliquer un nouveau plan",
|
"blueprintCreateDescription2": "Suivez les étapes ci-dessous pour créer et appliquer une nouvelle config",
|
||||||
"blueprintDetails": "Détails du Plan",
|
"blueprintDetails": "Détails de la Config",
|
||||||
"blueprintDetailsDescription": "Voir le résultat du plan appliqué et les erreurs qui se sont produites",
|
"blueprintDetailsDescription": "Voir le résultat du plan appliqué et les erreurs qui se sont produites",
|
||||||
"blueprintInfo": "Informations sur le Plan",
|
"blueprintInfo": "Informations sur la Config",
|
||||||
"message": "Message",
|
"message": "Message",
|
||||||
"blueprintContentsDescription": "Définissez le contenu YAML décrivant votre infrastructure",
|
"blueprintContentsDescription": "Définissez le contenu YAML décrivant votre infrastructure",
|
||||||
"blueprintErrorCreateDescription": "Une erreur s'est produite lors de l'application du plan",
|
"blueprintErrorCreateDescription": "Une erreur s'est produite lors de l'application de la config",
|
||||||
"blueprintErrorCreate": "Erreur lors de la création du plan",
|
"blueprintErrorCreate": "Erreur lors de la création de la config",
|
||||||
"searchBlueprintProgress": "Rechercher des plans...",
|
"searchBlueprintProgress": "Rechercher des configs...",
|
||||||
"appliedAt": "Appliqué à",
|
"appliedAt": "Appliqué à",
|
||||||
"source": "Source",
|
"source": "Source",
|
||||||
"contents": "Contenus",
|
"contents": "Contenus",
|
||||||
"parsedContents": "Contenu analysé (lecture seule)",
|
"parsedContents": "Contenu analysé (lecture seule)",
|
||||||
"enableDockerSocket": "Activer le Plan Docker",
|
"enableDockerSocket": "Activer la Config Docker",
|
||||||
"enableDockerSocketDescription": "Activer le ramassage d'étiquettes de socket Docker pour les étiquettes de plan. Le chemin de socket doit être fourni à Newt.",
|
"enableDockerSocketDescription": "Activer le ramassage d'étiquettes de socket Docker pour les étiquettes de plan. Le chemin de socket doit être fourni à Newt.",
|
||||||
"enableDockerSocketLink": "En savoir plus",
|
"enableDockerSocketLink": "En savoir plus",
|
||||||
"viewDockerContainers": "Voir les conteneurs Docker",
|
"viewDockerContainers": "Voir les conteneurs Docker",
|
||||||
@@ -1279,6 +1279,15 @@
|
|||||||
"settingsErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour des paramètres",
|
"settingsErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour des paramètres",
|
||||||
"sidebarCollapse": "Réduire",
|
"sidebarCollapse": "Réduire",
|
||||||
"sidebarExpand": "Développer",
|
"sidebarExpand": "Développer",
|
||||||
|
"productUpdateMoreInfo": "{noOfUpdates} mises à jour de plus",
|
||||||
|
"productUpdateInfo": "{noOfUpdates} mises à jour",
|
||||||
|
"productUpdateWhatsNew": "Quoi de neuf",
|
||||||
|
"productUpdateTitle": "Mises à jour",
|
||||||
|
"productUpdateEmpty": "Aucune mise à jour",
|
||||||
|
"dismissAll": "Tout cacher",
|
||||||
|
"pangolinUpdateAvailable": "Mise à jour disponible",
|
||||||
|
"pangolinUpdateAvailableInfo": "La version {version} est prête à être installée",
|
||||||
|
"pangolinUpdateAvailableReleaseNotes": "Voir les notes de version",
|
||||||
"newtUpdateAvailable": "Mise à jour disponible",
|
"newtUpdateAvailable": "Mise à jour disponible",
|
||||||
"newtUpdateAvailableInfo": "Une nouvelle version de Newt est disponible. Veuillez mettre à jour vers la dernière version pour une meilleure expérience.",
|
"newtUpdateAvailableInfo": "Une nouvelle version de Newt est disponible. Veuillez mettre à jour vers la dernière version pour une meilleure expérience.",
|
||||||
"domainPickerEnterDomain": "Domaine",
|
"domainPickerEnterDomain": "Domaine",
|
||||||
@@ -1525,6 +1534,12 @@
|
|||||||
"resourcesTableTheseResourcesForUseWith": "Ces ressources sont à utiliser avec",
|
"resourcesTableTheseResourcesForUseWith": "Ces ressources sont à utiliser avec",
|
||||||
"resourcesTableClients": "Clients",
|
"resourcesTableClients": "Clients",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "et sont uniquement accessibles en interne lorsqu'elles sont connectées avec un client.",
|
"resourcesTableAndOnlyAccessibleInternally": "et sont uniquement accessibles en interne lorsqu'elles sont connectées avec un client.",
|
||||||
|
"resourcesTableNoTargets": "Aucune cible",
|
||||||
|
"resourcesTableHealthy": "Sain",
|
||||||
|
"resourcesTableDegraded": "Dégradé",
|
||||||
|
"resourcesTableOffline": "Hors ligne",
|
||||||
|
"resourcesTableUnknown": "Inconnu",
|
||||||
|
"resourcesTableNotMonitored": "Non-monitoré",
|
||||||
"editInternalResourceDialogEditClientResource": "Modifier la ressource client",
|
"editInternalResourceDialogEditClientResource": "Modifier la ressource client",
|
||||||
"editInternalResourceDialogUpdateResourceProperties": "Mettez à jour les propriétés de la ressource et la configuration de la cible pour {resourceName}.",
|
"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",
|
"editInternalResourceDialogResourceProperties": "Propriétés de la ressource",
|
||||||
@@ -2041,7 +2056,7 @@
|
|||||||
"preferWildcardCertDescription": "Tentative de génération d'un certificat générique (nécessite un résolveur de certificat correctement configuré).",
|
"preferWildcardCertDescription": "Tentative de génération d'un certificat générique (nécessite un résolveur de certificat correctement configuré).",
|
||||||
"recordName": "Nom de l'enregistrement",
|
"recordName": "Nom de l'enregistrement",
|
||||||
"auto": "Automatique",
|
"auto": "Automatique",
|
||||||
"TTL": "TTC",
|
"TTL": "TTL",
|
||||||
"howToAddRecords": "Comment ajouter des enregistrements",
|
"howToAddRecords": "Comment ajouter des enregistrements",
|
||||||
"dnsRecord": "Enregistrements DNS",
|
"dnsRecord": "Enregistrements DNS",
|
||||||
"required": "Requis",
|
"required": "Requis",
|
||||||
@@ -2061,7 +2076,7 @@
|
|||||||
"warning": "Avertissement",
|
"warning": "Avertissement",
|
||||||
"proxyProtocolWarning": "Votre application backend doit être configurée pour accepter les connexions Proxy Protocol. Si votre backend ne prend pas en charge le protocole Proxy, activer ceci va casser toutes les connexions. Assurez-vous de configurer votre backend pour faire confiance aux en-têtes du protocole Proxy de Traefik.",
|
"proxyProtocolWarning": "Votre application backend doit être configurée pour accepter les connexions Proxy Protocol. Si votre backend ne prend pas en charge le protocole Proxy, activer ceci va casser toutes les connexions. Assurez-vous de configurer votre backend pour faire confiance aux en-têtes du protocole Proxy de Traefik.",
|
||||||
"restarting": "Redémarrage...",
|
"restarting": "Redémarrage...",
|
||||||
"manual": "Manuelle",
|
"manual": "Manuel",
|
||||||
"messageSupport": "Soutien aux messages",
|
"messageSupport": "Soutien aux messages",
|
||||||
"supportNotAvailableTitle": "Support non disponible",
|
"supportNotAvailableTitle": "Support non disponible",
|
||||||
"supportNotAvailableDescription": "L'assistance n'est pas disponible pour le moment. Vous pouvez envoyer un e-mail à support@pangolin.net.",
|
"supportNotAvailableDescription": "L'assistance n'est pas disponible pour le moment. Vous pouvez envoyer un e-mail à support@pangolin.net.",
|
||||||
@@ -2081,19 +2096,45 @@
|
|||||||
"supportSend": "Envoyer",
|
"supportSend": "Envoyer",
|
||||||
"supportMessageSent": "Message envoyé !",
|
"supportMessageSent": "Message envoyé !",
|
||||||
"supportWillContact": "Nous vous contacterons sous peu!",
|
"supportWillContact": "Nous vous contacterons sous peu!",
|
||||||
"selectLogRetention": "Sélectionner la durée de rétention du journal",
|
"selectLogRetention": "Sélectionner la durée de rétention des logs",
|
||||||
"showColumns": "Afficher les colonnes",
|
"showColumns": "Afficher les colonnes",
|
||||||
"hideColumns": "Cacher les colonnes",
|
"hideColumns": "Cacher les colonnes",
|
||||||
"columnVisibility": "Visibilité des colonnes",
|
"columnVisibility": "Visibilité des colonnes",
|
||||||
"toggleColumn": "Activer/désactiver la colonne {columnName}",
|
"toggleColumn": "Activer/désactiver la colonne {columnName}",
|
||||||
"allColumns": "Toutes les colonnes",
|
"allColumns": "Toutes les colonnes",
|
||||||
"defaultColumns": "Colonnes par défaut",
|
"defaultColumns": "Colonnes par défaut",
|
||||||
"customizeView": "Personnaliser la vue",
|
"customizeView": "Personnaliser l'apparence",
|
||||||
"viewOptions": "Voir les options",
|
"viewOptions": "Voir les options",
|
||||||
"selectAll": "Tout sélectionner",
|
"selectAll": "Tout sélectionner",
|
||||||
"selectNone": "Ne rien sélectionner",
|
"selectNone": "Ne rien sélectionner",
|
||||||
"selectedResources": "Ressources sélectionnées",
|
"selectedResources": "Ressources sélectionnées",
|
||||||
"enableSelected": "Activer la sélection",
|
"enableSelected": "Activer la sélection",
|
||||||
"disableSelected": "Désactiver la sélection",
|
"disableSelected": "Désactiver la sélection",
|
||||||
"checkSelectedStatus": "Vérifier le statut de la sélection"
|
"checkSelectedStatus": "Vérifier le statut de la sélection",
|
||||||
}
|
"credentials": "Identifiants",
|
||||||
|
"savecredentials": "Enregistrer les identifiants",
|
||||||
|
"regeneratecredentials": "Re-claver",
|
||||||
|
"regenerateCredentials": "Régénérer et enregistrer les identifiants",
|
||||||
|
"generatedcredentials": "Identifiants générés",
|
||||||
|
"copyandsavethesecredentials": "Copier et enregistrer ces identifiants",
|
||||||
|
"copyandsavethesecredentialsdescription": "Ces identifiants ne seront pas affichés à nouveaux une fois cette page fermée. Enregistrez-les maintenant.",
|
||||||
|
"credentialsSaved": "Identifiants enregistrés",
|
||||||
|
"credentialsSavedDescription": "Les identifiants ont été régénérés et enregistrés avec succès.",
|
||||||
|
"credentialsSaveError": "Erreur lors de l'enregistrement des identifiants",
|
||||||
|
"credentialsSaveErrorDescription": "Une erreur s'est produite lors de la régénération et l'enregistrement des identifiants.",
|
||||||
|
"regenerateCredentialsWarning": "La régénération de ces identifiants invalidera ceux actuellement utilisés. Assurez-vous de mettre à jour toutes les configurations qui les utilisent.",
|
||||||
|
"confirm": "Confirmer",
|
||||||
|
"regenerateCredentialsConfirmation": "Voulez-vous vraiment régénérer les identifiants ?",
|
||||||
|
"endpoint": "Endpoint",
|
||||||
|
"Id": "Id",
|
||||||
|
"SecretKey": "Clé privée",
|
||||||
|
"featureDisabledTooltip": "Cette fonctionnalité n'est disponible que dans la version entreprise et nécessite une licence pour être utilisée.",
|
||||||
|
"niceId": "Joli ID",
|
||||||
|
"niceIdUpdated": "Joli ID mis à jour",
|
||||||
|
"niceIdUpdatedSuccessfully": "Joli ID mis à jour avec succès",
|
||||||
|
"niceIdUpdateError": "Erreur lors de la mise à jour du joli ID",
|
||||||
|
"niceIdUpdateErrorDescription": "Erreur lors de la mise à jour du joli ID.",
|
||||||
|
"niceIdCannotBeEmpty": "Merci de renseigner un joli ID",
|
||||||
|
"enterIdentifier": "Entrez l'identifiant",
|
||||||
|
"identifier": "Identifiant"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,15 @@
|
|||||||
"settingsErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento delle impostazioni",
|
"settingsErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento delle impostazioni",
|
||||||
"sidebarCollapse": "Comprimi",
|
"sidebarCollapse": "Comprimi",
|
||||||
"sidebarExpand": "Espandi",
|
"sidebarExpand": "Espandi",
|
||||||
|
"productUpdateMoreInfo": "{noOfUpdates} altri aggiornamenti",
|
||||||
|
"productUpdateInfo": "{noOfUpdates} aggiornamenti",
|
||||||
|
"productUpdateWhatsNew": "Novità",
|
||||||
|
"productUpdateTitle": "Aggiornamenti Prodotto",
|
||||||
|
"productUpdateEmpty": "Nessun aggiornamento",
|
||||||
|
"dismissAll": "Ignora tutto",
|
||||||
|
"pangolinUpdateAvailable": "Nuova versione disponibile",
|
||||||
|
"pangolinUpdateAvailableInfo": "La versione {version} è pronta per l'installazione",
|
||||||
|
"pangolinUpdateAvailableReleaseNotes": "Visualizza note di rilascio",
|
||||||
"newtUpdateAvailable": "Aggiornamento Disponibile",
|
"newtUpdateAvailable": "Aggiornamento Disponibile",
|
||||||
"newtUpdateAvailableInfo": "È disponibile una nuova versione di Newt. Si prega di aggiornare all'ultima versione per la migliore esperienza.",
|
"newtUpdateAvailableInfo": "È disponibile una nuova versione di Newt. Si prega di aggiornare all'ultima versione per la migliore esperienza.",
|
||||||
"domainPickerEnterDomain": "Dominio",
|
"domainPickerEnterDomain": "Dominio",
|
||||||
@@ -1525,6 +1534,12 @@
|
|||||||
"resourcesTableTheseResourcesForUseWith": "Queste risorse sono per uso con",
|
"resourcesTableTheseResourcesForUseWith": "Queste risorse sono per uso con",
|
||||||
"resourcesTableClients": "Client",
|
"resourcesTableClients": "Client",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "e sono accessibili solo internamente quando connessi con un client.",
|
"resourcesTableAndOnlyAccessibleInternally": "e sono accessibili solo internamente quando connessi con un client.",
|
||||||
|
"resourcesTableNoTargets": "Nessun obiettivo",
|
||||||
|
"resourcesTableHealthy": "Sano",
|
||||||
|
"resourcesTableDegraded": "Degraded",
|
||||||
|
"resourcesTableOffline": "Offline",
|
||||||
|
"resourcesTableUnknown": "Sconosciuto",
|
||||||
|
"resourcesTableNotMonitored": "Non monitorato",
|
||||||
"editInternalResourceDialogEditClientResource": "Modifica Risorsa Client",
|
"editInternalResourceDialogEditClientResource": "Modifica Risorsa Client",
|
||||||
"editInternalResourceDialogUpdateResourceProperties": "Aggiorna le proprietà della risorsa e la configurazione del target per {resourceName}.",
|
"editInternalResourceDialogUpdateResourceProperties": "Aggiorna le proprietà della risorsa e la configurazione del target per {resourceName}.",
|
||||||
"editInternalResourceDialogResourceProperties": "Proprietà della Risorsa",
|
"editInternalResourceDialogResourceProperties": "Proprietà della Risorsa",
|
||||||
@@ -2095,5 +2110,31 @@
|
|||||||
"selectedResources": "Risorse Selezionate",
|
"selectedResources": "Risorse Selezionate",
|
||||||
"enableSelected": "Abilita Selezionati",
|
"enableSelected": "Abilita Selezionati",
|
||||||
"disableSelected": "Disabilita Selezionati",
|
"disableSelected": "Disabilita Selezionati",
|
||||||
"checkSelectedStatus": "Controlla lo stato dei selezionati"
|
"checkSelectedStatus": "Controlla lo stato dei selezionati",
|
||||||
}
|
"credentials": "Credenziali",
|
||||||
|
"savecredentials": "Salva Credenziali",
|
||||||
|
"regeneratecredentials": "Ri-chiave",
|
||||||
|
"regenerateCredentials": "Rigenera e salva le tue credenziali",
|
||||||
|
"generatedcredentials": "Credenziali Generate",
|
||||||
|
"copyandsavethesecredentials": "Copia e salva queste credenziali",
|
||||||
|
"copyandsavethesecredentialsdescription": "Queste credenziali non verranno mostrate di nuovo dopo aver lasciato questa pagina. Salvarle in modo sicuro ora.",
|
||||||
|
"credentialsSaved": "Credenziali Salvate",
|
||||||
|
"credentialsSavedDescription": "Le credenziali sono state rigenerate e salvate con successo.",
|
||||||
|
"credentialsSaveError": "Errore Di Salvataggio Credenziali",
|
||||||
|
"credentialsSaveErrorDescription": "Errore durante la rigenerazione e il salvataggio delle credenziali.",
|
||||||
|
"regenerateCredentialsWarning": "Rigenerare le credenziali invaliderà quelle precedenti. Assicurarsi di aggiornare le configurazioni che utilizzano queste credenziali.",
|
||||||
|
"confirm": "Conferma",
|
||||||
|
"regenerateCredentialsConfirmation": "Sei sicuro di voler rigenerare le credenziali?",
|
||||||
|
"endpoint": "Endpoint",
|
||||||
|
"Id": "Id",
|
||||||
|
"SecretKey": "Chiave Segreta",
|
||||||
|
"featureDisabledTooltip": "Questa funzione è disponibile solo nel piano aziendale e richiede una licenza per utilizzarla.",
|
||||||
|
"niceId": "Simpatico ID",
|
||||||
|
"niceIdUpdated": "Nice ID Aggiornato",
|
||||||
|
"niceIdUpdatedSuccessfully": "Nizza Id Aggiornato Con Successo",
|
||||||
|
"niceIdUpdateError": "Errore nell'aggiornare Nice ID",
|
||||||
|
"niceIdUpdateErrorDescription": "Si è verificato un errore durante l'aggiornamento del Nice ID.",
|
||||||
|
"niceIdCannotBeEmpty": "Il Nice ID non può essere vuoto",
|
||||||
|
"enterIdentifier": "Inserisci identificatore",
|
||||||
|
"identifier": "Identifier"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,15 @@
|
|||||||
"settingsErrorUpdateDescription": "설정을 업데이트하는 동안 오류가 발생했습니다",
|
"settingsErrorUpdateDescription": "설정을 업데이트하는 동안 오류가 발생했습니다",
|
||||||
"sidebarCollapse": "줄이기",
|
"sidebarCollapse": "줄이기",
|
||||||
"sidebarExpand": "확장하기",
|
"sidebarExpand": "확장하기",
|
||||||
|
"productUpdateMoreInfo": "{noOfUpdates}개의 더 많은 업데이트",
|
||||||
|
"productUpdateInfo": "{noOfUpdates}개 업데이트",
|
||||||
|
"productUpdateWhatsNew": "새로운 기능",
|
||||||
|
"productUpdateTitle": "제품 업데이트",
|
||||||
|
"productUpdateEmpty": "업데이트 없음",
|
||||||
|
"dismissAll": "모두 해제",
|
||||||
|
"pangolinUpdateAvailable": "새 버전 사용 가능",
|
||||||
|
"pangolinUpdateAvailableInfo": "버전 {version}을(를) 설치할 준비가 되었습니다",
|
||||||
|
"pangolinUpdateAvailableReleaseNotes": "릴리스 노트 보기",
|
||||||
"newtUpdateAvailable": "업데이트 가능",
|
"newtUpdateAvailable": "업데이트 가능",
|
||||||
"newtUpdateAvailableInfo": "뉴트의 새 버전이 출시되었습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
|
"newtUpdateAvailableInfo": "뉴트의 새 버전이 출시되었습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
|
||||||
"domainPickerEnterDomain": "도메인",
|
"domainPickerEnterDomain": "도메인",
|
||||||
@@ -1525,6 +1534,12 @@
|
|||||||
"resourcesTableTheseResourcesForUseWith": "이 리소스는 다음과 함께 사용하기 위한 것입니다.",
|
"resourcesTableTheseResourcesForUseWith": "이 리소스는 다음과 함께 사용하기 위한 것입니다.",
|
||||||
"resourcesTableClients": "클라이언트",
|
"resourcesTableClients": "클라이언트",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "클라이언트와 연결되었을 때만 내부적으로 접근 가능합니다.",
|
"resourcesTableAndOnlyAccessibleInternally": "클라이언트와 연결되었을 때만 내부적으로 접근 가능합니다.",
|
||||||
|
"resourcesTableNoTargets": "대상 없음",
|
||||||
|
"resourcesTableHealthy": "정상",
|
||||||
|
"resourcesTableDegraded": "저하됨",
|
||||||
|
"resourcesTableOffline": "오프라인",
|
||||||
|
"resourcesTableUnknown": "알 수 없음",
|
||||||
|
"resourcesTableNotMonitored": "모니터링되지 않음",
|
||||||
"editInternalResourceDialogEditClientResource": "클라이언트 리소스 수정",
|
"editInternalResourceDialogEditClientResource": "클라이언트 리소스 수정",
|
||||||
"editInternalResourceDialogUpdateResourceProperties": "{resourceName}의 리소스 속성과 대상 구성을 업데이트하세요.",
|
"editInternalResourceDialogUpdateResourceProperties": "{resourceName}의 리소스 속성과 대상 구성을 업데이트하세요.",
|
||||||
"editInternalResourceDialogResourceProperties": "리소스 속성",
|
"editInternalResourceDialogResourceProperties": "리소스 속성",
|
||||||
@@ -2095,5 +2110,31 @@
|
|||||||
"selectedResources": "선택된 리소스",
|
"selectedResources": "선택된 리소스",
|
||||||
"enableSelected": "선택된 항목 활성화",
|
"enableSelected": "선택된 항목 활성화",
|
||||||
"disableSelected": "선택된 항목 비활성화",
|
"disableSelected": "선택된 항목 비활성화",
|
||||||
"checkSelectedStatus": "선택된 항목 상태 확인"
|
"checkSelectedStatus": "선택된 항목 상태 확인",
|
||||||
}
|
"credentials": "자격 증명",
|
||||||
|
"savecredentials": "자격 증명 저장",
|
||||||
|
"regeneratecredentials": "재생성",
|
||||||
|
"regenerateCredentials": "자격 증명을 재생성하고 저장합니다",
|
||||||
|
"generatedcredentials": "생성된 자격 증명",
|
||||||
|
"copyandsavethesecredentials": "이 자격 증명을 복사하여 저장합니다",
|
||||||
|
"copyandsavethesecredentialsdescription": "이 페이지를 떠난 후에는 자격 증명이 다시 표시되지 않습니다. 지금 안전하게 저장하십시오.",
|
||||||
|
"credentialsSaved": "자격 증명 저장됨",
|
||||||
|
"credentialsSavedDescription": "자격 증명이 성공적으로 재생성 및 저장되었습니다.",
|
||||||
|
"credentialsSaveError": "자격 증명 저장 오류",
|
||||||
|
"credentialsSaveErrorDescription": "자격 증명을 재생성하고 저장하는 동안 오류가 발생했습니다.",
|
||||||
|
"regenerateCredentialsWarning": "자격 증명을 재생성하면 이전 자격 증명이 무효화됩니다. 이 자격 증명을 사용하는 모든 구성을 업데이트하십시오.",
|
||||||
|
"confirm": "확인",
|
||||||
|
"regenerateCredentialsConfirmation": "자격 증명을 재생성하시겠습니까?",
|
||||||
|
"endpoint": "엔드포인트",
|
||||||
|
"Id": "아이디",
|
||||||
|
"SecretKey": "비밀 키",
|
||||||
|
"featureDisabledTooltip": "이 기능은 엔터프라이즈 플랜에서만 사용할 수 있으며 라이센스가 필요합니다.",
|
||||||
|
"niceId": "예쁜 ID",
|
||||||
|
"niceIdUpdated": "예쁜 ID 업데이트됨",
|
||||||
|
"niceIdUpdatedSuccessfully": "예쁜 ID가 성공적으로 업데이트되었습니다",
|
||||||
|
"niceIdUpdateError": "예쁜 ID 업데이트 오류",
|
||||||
|
"niceIdUpdateErrorDescription": "예쁜 ID를 업데이트하는 동안 오류가 발생했습니다.",
|
||||||
|
"niceIdCannotBeEmpty": "예쁜 ID는 비워둘 수 없습니다",
|
||||||
|
"enterIdentifier": "식별자 입력",
|
||||||
|
"identifier": "식별자"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,15 @@
|
|||||||
"settingsErrorUpdateDescription": "En feil oppstod under oppdatering av innstillinger",
|
"settingsErrorUpdateDescription": "En feil oppstod under oppdatering av innstillinger",
|
||||||
"sidebarCollapse": "Skjul",
|
"sidebarCollapse": "Skjul",
|
||||||
"sidebarExpand": "Utvid",
|
"sidebarExpand": "Utvid",
|
||||||
|
"productUpdateMoreInfo": "{noOfUpdates} flere oppdateringer",
|
||||||
|
"productUpdateInfo": "{noOfUpdates} oppdateringer",
|
||||||
|
"productUpdateWhatsNew": "Hva er nytt",
|
||||||
|
"productUpdateTitle": "Oppdateringer om produktet",
|
||||||
|
"productUpdateEmpty": "Ingen oppdateringer",
|
||||||
|
"dismissAll": "Avvis alle",
|
||||||
|
"pangolinUpdateAvailable": "Ny versjon tilgjengelig",
|
||||||
|
"pangolinUpdateAvailableInfo": "Versjon {version} er klar til å installere",
|
||||||
|
"pangolinUpdateAvailableReleaseNotes": "Se utgivelsnotater",
|
||||||
"newtUpdateAvailable": "Oppdatering tilgjengelig",
|
"newtUpdateAvailable": "Oppdatering tilgjengelig",
|
||||||
"newtUpdateAvailableInfo": "En ny versjon av Newt er tilgjengelig. Vennligst oppdater til den nyeste versjonen for den beste opplevelsen.",
|
"newtUpdateAvailableInfo": "En ny versjon av Newt er tilgjengelig. Vennligst oppdater til den nyeste versjonen for den beste opplevelsen.",
|
||||||
"domainPickerEnterDomain": "Domene",
|
"domainPickerEnterDomain": "Domene",
|
||||||
@@ -1525,6 +1534,12 @@
|
|||||||
"resourcesTableTheseResourcesForUseWith": "Disse ressursene er til bruk med",
|
"resourcesTableTheseResourcesForUseWith": "Disse ressursene er til bruk med",
|
||||||
"resourcesTableClients": "Klienter",
|
"resourcesTableClients": "Klienter",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "og er kun tilgjengelig internt når de er koblet til med en klient.",
|
"resourcesTableAndOnlyAccessibleInternally": "og er kun tilgjengelig internt når de er koblet til med en klient.",
|
||||||
|
"resourcesTableNoTargets": "Ingen mål",
|
||||||
|
"resourcesTableHealthy": "Frisk",
|
||||||
|
"resourcesTableDegraded": "Nedgradert",
|
||||||
|
"resourcesTableOffline": "Frakoblet",
|
||||||
|
"resourcesTableUnknown": "Ukjent",
|
||||||
|
"resourcesTableNotMonitored": "Ikke overvåket",
|
||||||
"editInternalResourceDialogEditClientResource": "Rediger klientressurs",
|
"editInternalResourceDialogEditClientResource": "Rediger klientressurs",
|
||||||
"editInternalResourceDialogUpdateResourceProperties": "Oppdater ressursens egenskaper og målkonfigurasjon for {resourceName}.",
|
"editInternalResourceDialogUpdateResourceProperties": "Oppdater ressursens egenskaper og målkonfigurasjon for {resourceName}.",
|
||||||
"editInternalResourceDialogResourceProperties": "Ressursegenskaper",
|
"editInternalResourceDialogResourceProperties": "Ressursegenskaper",
|
||||||
@@ -2095,5 +2110,31 @@
|
|||||||
"selectedResources": "Valgte ressurser",
|
"selectedResources": "Valgte ressurser",
|
||||||
"enableSelected": "Aktiver valgte",
|
"enableSelected": "Aktiver valgte",
|
||||||
"disableSelected": "Deaktiver valgte",
|
"disableSelected": "Deaktiver valgte",
|
||||||
"checkSelectedStatus": "Kontroller status for valgte"
|
"checkSelectedStatus": "Kontroller status for valgte",
|
||||||
}
|
"credentials": "Legitimasjon",
|
||||||
|
"savecredentials": "Lagre brukeropplysninger",
|
||||||
|
"regeneratecredentials": "Ny nøkkel",
|
||||||
|
"regenerateCredentials": "Regenerer og lagre opplysningene dine",
|
||||||
|
"generatedcredentials": "Genererte brukeropplysninger",
|
||||||
|
"copyandsavethesecredentials": "Kopier og lagre disse opplysningene",
|
||||||
|
"copyandsavethesecredentialsdescription": "Disse opplysningene vil ikke bli vist igjen etter at du forlater siden. Lagre dem trygt nå.",
|
||||||
|
"credentialsSaved": "Påloggingsinformasjon lagret",
|
||||||
|
"credentialsSavedDescription": "Påloggingsinformasjonen har blitt regenerert og lagret.",
|
||||||
|
"credentialsSaveError": "Påloggingsinformasjon lagre feil",
|
||||||
|
"credentialsSaveErrorDescription": "En feil oppstod under regenerering og lagring av legitimasjon.",
|
||||||
|
"regenerateCredentialsWarning": "Regenerering av legitimasjon vil ugyldiggjøre de forrige. Sørg for at alle konfigurasjoner som bruker disse legitimasjonene.",
|
||||||
|
"confirm": "Bekreft",
|
||||||
|
"regenerateCredentialsConfirmation": "Er du sikker på at du vil regenerere legetimasjonene?",
|
||||||
|
"endpoint": "Endpoint",
|
||||||
|
"Id": "Id",
|
||||||
|
"SecretKey": "Hemmelig nøkkel",
|
||||||
|
"featureDisabledTooltip": "Denne funksjonen er bare tilgjengelig i virksomhetsplanen og krever en lisens til å bruke den.",
|
||||||
|
"niceId": "God ID",
|
||||||
|
"niceIdUpdated": "Flott ID oppdatert",
|
||||||
|
"niceIdUpdatedSuccessfully": "Id-en ble oppdatert",
|
||||||
|
"niceIdUpdateError": "Feil under oppdatering av hyggelig ID",
|
||||||
|
"niceIdUpdateErrorDescription": "Det oppstod en feil under oppdatering av Nice ID.",
|
||||||
|
"niceIdCannotBeEmpty": "God ID kan ikke være tom",
|
||||||
|
"enterIdentifier": "Angi identifikator",
|
||||||
|
"identifier": "Identifier"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,15 @@
|
|||||||
"settingsErrorUpdateDescription": "Er is een fout opgetreden bij het bijwerken van instellingen",
|
"settingsErrorUpdateDescription": "Er is een fout opgetreden bij het bijwerken van instellingen",
|
||||||
"sidebarCollapse": "Inklappen",
|
"sidebarCollapse": "Inklappen",
|
||||||
"sidebarExpand": "Uitklappen",
|
"sidebarExpand": "Uitklappen",
|
||||||
|
"productUpdateMoreInfo": "Nog {noOfUpdates} updates",
|
||||||
|
"productUpdateInfo": "{noOfUpdates} updates",
|
||||||
|
"productUpdateWhatsNew": "Wat is nieuw",
|
||||||
|
"productUpdateTitle": "Update Producten",
|
||||||
|
"productUpdateEmpty": "Geen updates",
|
||||||
|
"dismissAll": "Alles afwijzen",
|
||||||
|
"pangolinUpdateAvailable": "Nieuwe versie beschikbaar",
|
||||||
|
"pangolinUpdateAvailableInfo": "Versie {version} is klaar om te installeren",
|
||||||
|
"pangolinUpdateAvailableReleaseNotes": "Bekijk release notities",
|
||||||
"newtUpdateAvailable": "Update beschikbaar",
|
"newtUpdateAvailable": "Update beschikbaar",
|
||||||
"newtUpdateAvailableInfo": "Er is een nieuwe versie van Newt beschikbaar. Update naar de nieuwste versie voor de beste ervaring.",
|
"newtUpdateAvailableInfo": "Er is een nieuwe versie van Newt beschikbaar. Update naar de nieuwste versie voor de beste ervaring.",
|
||||||
"domainPickerEnterDomain": "Domein",
|
"domainPickerEnterDomain": "Domein",
|
||||||
@@ -1525,6 +1534,12 @@
|
|||||||
"resourcesTableTheseResourcesForUseWith": "Deze bronnen zijn bedoeld voor gebruik met",
|
"resourcesTableTheseResourcesForUseWith": "Deze bronnen zijn bedoeld voor gebruik met",
|
||||||
"resourcesTableClients": "Clienten",
|
"resourcesTableClients": "Clienten",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "en zijn alleen intern toegankelijk wanneer verbonden met een client.",
|
"resourcesTableAndOnlyAccessibleInternally": "en zijn alleen intern toegankelijk wanneer verbonden met een client.",
|
||||||
|
"resourcesTableNoTargets": "Geen doelen",
|
||||||
|
"resourcesTableHealthy": "Gezond",
|
||||||
|
"resourcesTableDegraded": "Verminderde",
|
||||||
|
"resourcesTableOffline": "Offline",
|
||||||
|
"resourcesTableUnknown": "onbekend",
|
||||||
|
"resourcesTableNotMonitored": "Niet gecontroleerd",
|
||||||
"editInternalResourceDialogEditClientResource": "Bewerk clientbron",
|
"editInternalResourceDialogEditClientResource": "Bewerk clientbron",
|
||||||
"editInternalResourceDialogUpdateResourceProperties": "Werk de eigenschapen van de bron en doelconfiguratie bij voor {resourceName}.",
|
"editInternalResourceDialogUpdateResourceProperties": "Werk de eigenschapen van de bron en doelconfiguratie bij voor {resourceName}.",
|
||||||
"editInternalResourceDialogResourceProperties": "Bron eigenschappen",
|
"editInternalResourceDialogResourceProperties": "Bron eigenschappen",
|
||||||
@@ -2095,5 +2110,31 @@
|
|||||||
"selectedResources": "Geselecteerde bronnen",
|
"selectedResources": "Geselecteerde bronnen",
|
||||||
"enableSelected": "Selectie inschakelen",
|
"enableSelected": "Selectie inschakelen",
|
||||||
"disableSelected": "Selectie uitschakelen",
|
"disableSelected": "Selectie uitschakelen",
|
||||||
"checkSelectedStatus": "Controleer de status van de geselecteerde"
|
"checkSelectedStatus": "Controleer de status van de geselecteerde",
|
||||||
}
|
"credentials": "Aanmeldgegevens",
|
||||||
|
"savecredentials": "Referenties opslaan",
|
||||||
|
"regeneratecredentials": "Hersleutel",
|
||||||
|
"regenerateCredentials": "Opnieuw genereren en opslaan van uw referenties",
|
||||||
|
"generatedcredentials": "Gegenereerde referenties",
|
||||||
|
"copyandsavethesecredentials": "Kopieer en bewaar deze inloggegevens",
|
||||||
|
"copyandsavethesecredentialsdescription": "Deze referenties worden niet meer getoond nadat u deze pagina verlaat. Sla ze nu veilig op.",
|
||||||
|
"credentialsSaved": "Referenties opgeslagen",
|
||||||
|
"credentialsSavedDescription": "Referenties werden met succes opnieuw gegenereerd en opgeslagen.",
|
||||||
|
"credentialsSaveError": "Fout bij opslaan referenties",
|
||||||
|
"credentialsSaveErrorDescription": "Er is een fout opgetreden tijdens het opnieuw genereren en opslaan van de inloggegevens.",
|
||||||
|
"regenerateCredentialsWarning": "Het opnieuw genereren van inloggegevens zal de vorige ongeldig maken. Zorg ervoor dat alle configuraties die deze inloggegevens gebruiken bijgewerkt worden.",
|
||||||
|
"confirm": "Bevestigen",
|
||||||
|
"regenerateCredentialsConfirmation": "Weet u zeker dat u de inloggegevens opnieuw wilt genereren?",
|
||||||
|
"endpoint": "Endpoint",
|
||||||
|
"Id": "Id",
|
||||||
|
"SecretKey": "Geheime sleutel",
|
||||||
|
"featureDisabledTooltip": "Deze functie is alleen beschikbaar in het bedrijfsplan en vereist een licentie om deze te gebruiken.",
|
||||||
|
"niceId": "Leuk ID",
|
||||||
|
"niceIdUpdated": "Leuke ID bijgewerkt",
|
||||||
|
"niceIdUpdatedSuccessfully": "Nice ID Updated Successfully",
|
||||||
|
"niceIdUpdateError": "Fout bij bijwerken ID Nice",
|
||||||
|
"niceIdUpdateErrorDescription": "Fout opgetreden tijdens het bijwerken van de ID van Nice.",
|
||||||
|
"niceIdCannotBeEmpty": "Nice ID mag niet leeg zijn",
|
||||||
|
"enterIdentifier": "ID invoeren",
|
||||||
|
"identifier": "Identifier"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,15 @@
|
|||||||
"settingsErrorUpdateDescription": "Wystąpił błąd podczas aktualizacji ustawień",
|
"settingsErrorUpdateDescription": "Wystąpił błąd podczas aktualizacji ustawień",
|
||||||
"sidebarCollapse": "Zwiń",
|
"sidebarCollapse": "Zwiń",
|
||||||
"sidebarExpand": "Rozwiń",
|
"sidebarExpand": "Rozwiń",
|
||||||
|
"productUpdateMoreInfo": "{noOfUpdates} więcej aktualizacji",
|
||||||
|
"productUpdateInfo": "Aktualizacje {noOfUpdates}",
|
||||||
|
"productUpdateWhatsNew": "Co nowego",
|
||||||
|
"productUpdateTitle": "Aktualizacje produktu",
|
||||||
|
"productUpdateEmpty": "Brak aktualizacji",
|
||||||
|
"dismissAll": "Zamknij wszystkie",
|
||||||
|
"pangolinUpdateAvailable": "Dostępna jest nowa wersja",
|
||||||
|
"pangolinUpdateAvailableInfo": "Wersja {version} jest gotowa do zainstalowania",
|
||||||
|
"pangolinUpdateAvailableReleaseNotes": "Zobacz notatki o wydaniu",
|
||||||
"newtUpdateAvailable": "Dostępna aktualizacja",
|
"newtUpdateAvailable": "Dostępna aktualizacja",
|
||||||
"newtUpdateAvailableInfo": "Nowa wersja Newt jest dostępna. Prosimy o aktualizację do najnowszej wersji dla najlepszej pracy.",
|
"newtUpdateAvailableInfo": "Nowa wersja Newt jest dostępna. Prosimy o aktualizację do najnowszej wersji dla najlepszej pracy.",
|
||||||
"domainPickerEnterDomain": "Domena",
|
"domainPickerEnterDomain": "Domena",
|
||||||
@@ -1525,6 +1534,12 @@
|
|||||||
"resourcesTableTheseResourcesForUseWith": "Te zasoby są do użytku z",
|
"resourcesTableTheseResourcesForUseWith": "Te zasoby są do użytku z",
|
||||||
"resourcesTableClients": "Klientami",
|
"resourcesTableClients": "Klientami",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "i są dostępne tylko wewnętrznie po połączeniu z klientem.",
|
"resourcesTableAndOnlyAccessibleInternally": "i są dostępne tylko wewnętrznie po połączeniu z klientem.",
|
||||||
|
"resourcesTableNoTargets": "Brak celów",
|
||||||
|
"resourcesTableHealthy": "Zdrowe",
|
||||||
|
"resourcesTableDegraded": "Degradacja",
|
||||||
|
"resourcesTableOffline": "Offline",
|
||||||
|
"resourcesTableUnknown": "Nieznane",
|
||||||
|
"resourcesTableNotMonitored": "Nie monitorowano",
|
||||||
"editInternalResourceDialogEditClientResource": "Edytuj zasób klienta",
|
"editInternalResourceDialogEditClientResource": "Edytuj zasób klienta",
|
||||||
"editInternalResourceDialogUpdateResourceProperties": "Zaktualizuj właściwości zasobu i konfigurację celu dla {resourceName}.",
|
"editInternalResourceDialogUpdateResourceProperties": "Zaktualizuj właściwości zasobu i konfigurację celu dla {resourceName}.",
|
||||||
"editInternalResourceDialogResourceProperties": "Właściwości zasobów",
|
"editInternalResourceDialogResourceProperties": "Właściwości zasobów",
|
||||||
@@ -2095,5 +2110,31 @@
|
|||||||
"selectedResources": "Wybrane Zasoby",
|
"selectedResources": "Wybrane Zasoby",
|
||||||
"enableSelected": "Włącz zaznaczone",
|
"enableSelected": "Włącz zaznaczone",
|
||||||
"disableSelected": "Wyłącz zaznaczone",
|
"disableSelected": "Wyłącz zaznaczone",
|
||||||
"checkSelectedStatus": "Sprawdź status zaznaczonych"
|
"checkSelectedStatus": "Sprawdź status zaznaczonych",
|
||||||
}
|
"credentials": "Dane logowania",
|
||||||
|
"savecredentials": "Zapisz dane logowania",
|
||||||
|
"regeneratecredentials": "Przycisk ponownie",
|
||||||
|
"regenerateCredentials": "Ponownie wygeneruj i zapisz swoje dane logowania",
|
||||||
|
"generatedcredentials": "Wygenerowane dane logowania",
|
||||||
|
"copyandsavethesecredentials": "Skopiuj i zapisz te dane logowania",
|
||||||
|
"copyandsavethesecredentialsdescription": "Te dane uwierzytelniające nie będą wyświetlane ponownie po opuszczeniu tej strony. Zapisz je teraz bezpiecznie.",
|
||||||
|
"credentialsSaved": "Zapisano dane logowania",
|
||||||
|
"credentialsSavedDescription": "Dane logowania zostały wygenerowane i zapisane pomyślnie.",
|
||||||
|
"credentialsSaveError": "Błąd zapisu danych logowania",
|
||||||
|
"credentialsSaveErrorDescription": "Wystąpił błąd podczas regeneracji i zapisywania poświadczeń.",
|
||||||
|
"regenerateCredentialsWarning": "Regeneracja poświadczeń spowoduje unieważnienie poprzednich poświadczeń. Upewnij się, że zaktualizowano wszystkie konfiguracje, które używają tych poświadczeń.",
|
||||||
|
"confirm": "Potwierdź",
|
||||||
|
"regenerateCredentialsConfirmation": "Czy na pewno chcesz wygenerować dane logowania?",
|
||||||
|
"endpoint": "Endpoint",
|
||||||
|
"Id": "Id",
|
||||||
|
"SecretKey": "Sekretny klucz",
|
||||||
|
"featureDisabledTooltip": "Ta funkcja jest dostępna tylko w planie przedsiębiorstwa i wymaga licencji, aby z niej korzystać.",
|
||||||
|
"niceId": "Niepoprawne ID",
|
||||||
|
"niceIdUpdated": "Zaktualizowano błędne ID",
|
||||||
|
"niceIdUpdatedSuccessfully": "Zaktualizowano błędne ID",
|
||||||
|
"niceIdUpdateError": "Błąd podczas aktualizacji Nice ID",
|
||||||
|
"niceIdUpdateErrorDescription": "Wystąpił błąd podczas aktualizowania Nicei ID.",
|
||||||
|
"niceIdCannotBeEmpty": "Niepoprawny identyfikator nie może być pusty",
|
||||||
|
"enterIdentifier": "Wprowadź identyfikator",
|
||||||
|
"identifier": "Identifier"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,15 @@
|
|||||||
"settingsErrorUpdateDescription": "Ocorreu um erro ao atualizar configurações",
|
"settingsErrorUpdateDescription": "Ocorreu um erro ao atualizar configurações",
|
||||||
"sidebarCollapse": "Recolher",
|
"sidebarCollapse": "Recolher",
|
||||||
"sidebarExpand": "Expandir",
|
"sidebarExpand": "Expandir",
|
||||||
|
"productUpdateMoreInfo": "Mais {noOfUpdates} atualizações",
|
||||||
|
"productUpdateInfo": "Atualizações {noOfUpdates}",
|
||||||
|
"productUpdateWhatsNew": "Novidades",
|
||||||
|
"productUpdateTitle": "Atualizações de Produto",
|
||||||
|
"productUpdateEmpty": "Não há atualizações",
|
||||||
|
"dismissAll": "Recusar tudo",
|
||||||
|
"pangolinUpdateAvailable": "Nova versão disponível",
|
||||||
|
"pangolinUpdateAvailableInfo": "A versão {version} está pronta para ser instalada",
|
||||||
|
"pangolinUpdateAvailableReleaseNotes": "Ver notas de lançamento",
|
||||||
"newtUpdateAvailable": "Nova Atualização Disponível",
|
"newtUpdateAvailable": "Nova Atualização Disponível",
|
||||||
"newtUpdateAvailableInfo": "Uma nova versão do Newt está disponível. Atualize para a versão mais recente para uma melhor experiência.",
|
"newtUpdateAvailableInfo": "Uma nova versão do Newt está disponível. Atualize para a versão mais recente para uma melhor experiência.",
|
||||||
"domainPickerEnterDomain": "Domínio",
|
"domainPickerEnterDomain": "Domínio",
|
||||||
@@ -1525,6 +1534,12 @@
|
|||||||
"resourcesTableTheseResourcesForUseWith": "Esses recursos são para uso com",
|
"resourcesTableTheseResourcesForUseWith": "Esses recursos são para uso com",
|
||||||
"resourcesTableClients": "Clientes",
|
"resourcesTableClients": "Clientes",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "e são acessíveis apenas internamente quando conectados com um cliente.",
|
"resourcesTableAndOnlyAccessibleInternally": "e são acessíveis apenas internamente quando conectados com um cliente.",
|
||||||
|
"resourcesTableNoTargets": "Nenhum alvo",
|
||||||
|
"resourcesTableHealthy": "Saudável",
|
||||||
|
"resourcesTableDegraded": "Degradado",
|
||||||
|
"resourcesTableOffline": "Desconectado",
|
||||||
|
"resourcesTableUnknown": "Desconhecido",
|
||||||
|
"resourcesTableNotMonitored": "Não monitorado",
|
||||||
"editInternalResourceDialogEditClientResource": "Editar Recurso do Cliente",
|
"editInternalResourceDialogEditClientResource": "Editar Recurso do Cliente",
|
||||||
"editInternalResourceDialogUpdateResourceProperties": "Atualize as propriedades do recurso e a configuração do alvo para {resourceName}.",
|
"editInternalResourceDialogUpdateResourceProperties": "Atualize as propriedades do recurso e a configuração do alvo para {resourceName}.",
|
||||||
"editInternalResourceDialogResourceProperties": "Propriedades do Recurso",
|
"editInternalResourceDialogResourceProperties": "Propriedades do Recurso",
|
||||||
@@ -2095,5 +2110,31 @@
|
|||||||
"selectedResources": "Recursos Selecionados",
|
"selectedResources": "Recursos Selecionados",
|
||||||
"enableSelected": "Habilitar Selecionados",
|
"enableSelected": "Habilitar Selecionados",
|
||||||
"disableSelected": "Desativar Selecionados",
|
"disableSelected": "Desativar Selecionados",
|
||||||
"checkSelectedStatus": "Status de Verificação dos Selecionados"
|
"checkSelectedStatus": "Status de Verificação dos Selecionados",
|
||||||
}
|
"credentials": "Credenciais",
|
||||||
|
"savecredentials": "Salvar Credenciais",
|
||||||
|
"regeneratecredentials": "Rechave",
|
||||||
|
"regenerateCredentials": "Regenerar e salvar suas credenciais",
|
||||||
|
"generatedcredentials": "Credenciais Geradas",
|
||||||
|
"copyandsavethesecredentials": "Copiar e salvar estas credenciais",
|
||||||
|
"copyandsavethesecredentialsdescription": "Essas credenciais não serão exibidas novamente depois que você sair desta página. Salve elas com segurança agora.",
|
||||||
|
"credentialsSaved": "Credenciais salvas",
|
||||||
|
"credentialsSavedDescription": "As credenciais foram regeneradas e salvas com sucesso.",
|
||||||
|
"credentialsSaveError": "Erro ao Salvar Credenciais",
|
||||||
|
"credentialsSaveErrorDescription": "Ocorreu um erro enquanto regenerava e salvava as credenciais.",
|
||||||
|
"regenerateCredentialsWarning": "Regenerar credenciais irá invalidar as anteriores. Certifique-se de atualizar qualquer configuração que use essas credenciais.",
|
||||||
|
"confirm": "Confirmar",
|
||||||
|
"regenerateCredentialsConfirmation": "Você tem certeza que deseja recriar as credenciais?",
|
||||||
|
"endpoint": "Endpoint",
|
||||||
|
"Id": "Id",
|
||||||
|
"SecretKey": "Chave secreta",
|
||||||
|
"featureDisabledTooltip": "Este recurso só está disponível no plano corporativo e requer que uma licença utilize.",
|
||||||
|
"niceId": "Belo ID",
|
||||||
|
"niceIdUpdated": "Bom ID atualizado",
|
||||||
|
"niceIdUpdatedSuccessfully": "Bom ID atualizado com sucesso",
|
||||||
|
"niceIdUpdateError": "Erro ao atualizar Nice ID",
|
||||||
|
"niceIdUpdateErrorDescription": "Ocorreu um erro ao atualizar a ID de Nice.",
|
||||||
|
"niceIdCannotBeEmpty": "Bom ID não pode estar vazio",
|
||||||
|
"enterIdentifier": "Inserir identificador",
|
||||||
|
"identifier": "Identifier"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,15 @@
|
|||||||
"settingsErrorUpdateDescription": "Произошла ошибка при обновлении настроек",
|
"settingsErrorUpdateDescription": "Произошла ошибка при обновлении настроек",
|
||||||
"sidebarCollapse": "Свернуть",
|
"sidebarCollapse": "Свернуть",
|
||||||
"sidebarExpand": "Развернуть",
|
"sidebarExpand": "Развернуть",
|
||||||
|
"productUpdateMoreInfo": "{noOfUpdates} больше обновлений",
|
||||||
|
"productUpdateInfo": "{noOfUpdates} обновлений",
|
||||||
|
"productUpdateWhatsNew": "Что нового",
|
||||||
|
"productUpdateTitle": "Обновления продуктов",
|
||||||
|
"productUpdateEmpty": "Нет обновлений",
|
||||||
|
"dismissAll": "Отклонить все",
|
||||||
|
"pangolinUpdateAvailable": "Доступна новая версия",
|
||||||
|
"pangolinUpdateAvailableInfo": "Версия {version} готова к установке",
|
||||||
|
"pangolinUpdateAvailableReleaseNotes": "Просмотреть заметки о выпуске",
|
||||||
"newtUpdateAvailable": "Доступно обновление",
|
"newtUpdateAvailable": "Доступно обновление",
|
||||||
"newtUpdateAvailableInfo": "Доступна новая версия Newt. Пожалуйста, обновитесь до последней версии для лучшего опыта.",
|
"newtUpdateAvailableInfo": "Доступна новая версия Newt. Пожалуйста, обновитесь до последней версии для лучшего опыта.",
|
||||||
"domainPickerEnterDomain": "Домен",
|
"domainPickerEnterDomain": "Домен",
|
||||||
@@ -1525,6 +1534,12 @@
|
|||||||
"resourcesTableTheseResourcesForUseWith": "Эти ресурсы предназначены для использования с",
|
"resourcesTableTheseResourcesForUseWith": "Эти ресурсы предназначены для использования с",
|
||||||
"resourcesTableClients": "Клиенты",
|
"resourcesTableClients": "Клиенты",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "и доступны только внутренне при подключении с клиентом.",
|
"resourcesTableAndOnlyAccessibleInternally": "и доступны только внутренне при подключении с клиентом.",
|
||||||
|
"resourcesTableNoTargets": "Нет ярлыков",
|
||||||
|
"resourcesTableHealthy": "Здоровые",
|
||||||
|
"resourcesTableDegraded": "Ухудшение",
|
||||||
|
"resourcesTableOffline": "Оффлайн",
|
||||||
|
"resourcesTableUnknown": "Неизвестен",
|
||||||
|
"resourcesTableNotMonitored": "Не отслеживается",
|
||||||
"editInternalResourceDialogEditClientResource": "Редактировать ресурс клиента",
|
"editInternalResourceDialogEditClientResource": "Редактировать ресурс клиента",
|
||||||
"editInternalResourceDialogUpdateResourceProperties": "Обновите свойства ресурса и настройку цели для {resourceName}.",
|
"editInternalResourceDialogUpdateResourceProperties": "Обновите свойства ресурса и настройку цели для {resourceName}.",
|
||||||
"editInternalResourceDialogResourceProperties": "Свойства ресурса",
|
"editInternalResourceDialogResourceProperties": "Свойства ресурса",
|
||||||
@@ -2095,5 +2110,31 @@
|
|||||||
"selectedResources": "Выбранные ресурсы",
|
"selectedResources": "Выбранные ресурсы",
|
||||||
"enableSelected": "Включить выбранные",
|
"enableSelected": "Включить выбранные",
|
||||||
"disableSelected": "Отключить выбранные",
|
"disableSelected": "Отключить выбранные",
|
||||||
"checkSelectedStatus": "Проверить статус выбранных"
|
"checkSelectedStatus": "Проверить статус выбранных",
|
||||||
}
|
"credentials": "Полномочия",
|
||||||
|
"savecredentials": "Сохранить учетные данные",
|
||||||
|
"regeneratecredentials": "Пере-ключ",
|
||||||
|
"regenerateCredentials": "Сгенерировать и сохранить ваши учетные данные",
|
||||||
|
"generatedcredentials": "Сгенерированные учетные данные",
|
||||||
|
"copyandsavethesecredentials": "Копировать и сохранить эти учетные данные",
|
||||||
|
"copyandsavethesecredentialsdescription": "Эти учетные данные не будут отображаться снова после того, как вы покинете эту страницу. Сохраните их сейчас.",
|
||||||
|
"credentialsSaved": "Учетные данные сохранены",
|
||||||
|
"credentialsSavedDescription": "Учетные данные были успешно восстановлены и сохранены.",
|
||||||
|
"credentialsSaveError": "Ошибка сохранения учетных данных",
|
||||||
|
"credentialsSaveErrorDescription": "Произошла ошибка при восстановлении и сохранении учетных данных.",
|
||||||
|
"regenerateCredentialsWarning": "Восстановление учётных данных приведет к недействительным предыдущим. Убедитесь, что все конфигурации, использующие эти учетные данные.",
|
||||||
|
"confirm": "Подтвердить",
|
||||||
|
"regenerateCredentialsConfirmation": "Вы уверены, что хотите восстановить учетные данные?",
|
||||||
|
"endpoint": "Endpoint",
|
||||||
|
"Id": "Id",
|
||||||
|
"SecretKey": "Секретный ключ",
|
||||||
|
"featureDisabledTooltip": "Эта функция доступна только в плане предприятия и требует лицензии на ее использование.",
|
||||||
|
"niceId": "Неплохой ID",
|
||||||
|
"niceIdUpdated": "Хороший ID обновлен",
|
||||||
|
"niceIdUpdatedSuccessfully": "Неплохой ID успешно обновлен",
|
||||||
|
"niceIdUpdateError": "Ошибка обновления Nice ID",
|
||||||
|
"niceIdUpdateErrorDescription": "Произошла ошибка при обновлении Nice ID.",
|
||||||
|
"niceIdCannotBeEmpty": "Неправильный ID не может быть пустым",
|
||||||
|
"enterIdentifier": "Введите идентификатор",
|
||||||
|
"identifier": "Identifier"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,15 @@
|
|||||||
"settingsErrorUpdateDescription": "Ayarları güncellerken bir hata oluştu",
|
"settingsErrorUpdateDescription": "Ayarları güncellerken bir hata oluştu",
|
||||||
"sidebarCollapse": "Daralt",
|
"sidebarCollapse": "Daralt",
|
||||||
"sidebarExpand": "Genişlet",
|
"sidebarExpand": "Genişlet",
|
||||||
|
"productUpdateMoreInfo": "{noOfUpdates} daha fazla güncelleme",
|
||||||
|
"productUpdateInfo": "{noOfUpdates} güncellemeler",
|
||||||
|
"productUpdateWhatsNew": "Neler Yeni",
|
||||||
|
"productUpdateTitle": "Ürün Güncellemeleri",
|
||||||
|
"productUpdateEmpty": "Güncelleme yok",
|
||||||
|
"dismissAll": "Hepsini Kapat",
|
||||||
|
"pangolinUpdateAvailable": "Yeni sürüm mevcut",
|
||||||
|
"pangolinUpdateAvailableInfo": "Sürüm {version} yüklenmeye hazır",
|
||||||
|
"pangolinUpdateAvailableReleaseNotes": "Sürüm notlarını görüntüleyin",
|
||||||
"newtUpdateAvailable": "Güncelleme Mevcut",
|
"newtUpdateAvailable": "Güncelleme Mevcut",
|
||||||
"newtUpdateAvailableInfo": "Newt'in yeni bir versiyonu mevcut. En iyi deneyim için lütfen en son sürüme güncelleyin.",
|
"newtUpdateAvailableInfo": "Newt'in yeni bir versiyonu mevcut. En iyi deneyim için lütfen en son sürüme güncelleyin.",
|
||||||
"domainPickerEnterDomain": "Alan Adı",
|
"domainPickerEnterDomain": "Alan Adı",
|
||||||
@@ -1525,6 +1534,12 @@
|
|||||||
"resourcesTableTheseResourcesForUseWith": "Bu kaynaklar ile kullanılmak için",
|
"resourcesTableTheseResourcesForUseWith": "Bu kaynaklar ile kullanılmak için",
|
||||||
"resourcesTableClients": "İstemciler",
|
"resourcesTableClients": "İstemciler",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "veyalnızca bir istemci ile bağlandığında dahili olarak erişilebilir.",
|
"resourcesTableAndOnlyAccessibleInternally": "veyalnızca bir istemci ile bağlandığında dahili olarak erişilebilir.",
|
||||||
|
"resourcesTableNoTargets": "Hedef yok",
|
||||||
|
"resourcesTableHealthy": "Sağlıklı",
|
||||||
|
"resourcesTableDegraded": "Düşük Performanslı",
|
||||||
|
"resourcesTableOffline": "Çevrimdışı",
|
||||||
|
"resourcesTableUnknown": "Bilinmiyor",
|
||||||
|
"resourcesTableNotMonitored": "İzlenmiyor",
|
||||||
"editInternalResourceDialogEditClientResource": "İstemci Kaynağı Düzenleyin",
|
"editInternalResourceDialogEditClientResource": "İstemci Kaynağı Düzenleyin",
|
||||||
"editInternalResourceDialogUpdateResourceProperties": "{resourceName} için kaynak özelliklerini ve hedef yapılandırmasını güncelleyin.",
|
"editInternalResourceDialogUpdateResourceProperties": "{resourceName} için kaynak özelliklerini ve hedef yapılandırmasını güncelleyin.",
|
||||||
"editInternalResourceDialogResourceProperties": "Kaynak Özellikleri",
|
"editInternalResourceDialogResourceProperties": "Kaynak Özellikleri",
|
||||||
@@ -2095,5 +2110,31 @@
|
|||||||
"selectedResources": "Seçilen Kaynaklar",
|
"selectedResources": "Seçilen Kaynaklar",
|
||||||
"enableSelected": "Seçilenleri Etkinleştir",
|
"enableSelected": "Seçilenleri Etkinleştir",
|
||||||
"disableSelected": "Seçilenleri Devre Dışı Bırak",
|
"disableSelected": "Seçilenleri Devre Dışı Bırak",
|
||||||
"checkSelectedStatus": "Seçilenlerin Durumunu Kontrol Et"
|
"checkSelectedStatus": "Seçilenlerin Durumunu Kontrol Et",
|
||||||
}
|
"credentials": "Kimlik Bilgileri",
|
||||||
|
"savecredentials": "Kimlik Bilgilerini Kaydet",
|
||||||
|
"regeneratecredentials": "Yeniden Anahtarla",
|
||||||
|
"regenerateCredentials": "Kimlik bilgilerinizi yeniden oluşturun ve kaydedin",
|
||||||
|
"generatedcredentials": "Oluşturulan Kimlik Bilgileri",
|
||||||
|
"copyandsavethesecredentials": "Bu kimlik bilgilerini kopyalayın ve kaydedin",
|
||||||
|
"copyandsavethesecredentialsdescription": "Bu sayfadan ayrıldıktan sonra bu kimlik bilgileri tekrar gösterilmeyecek. Onları şimdi güvenli bir şekilde saklayın.",
|
||||||
|
"credentialsSaved": "Kimlik Bilgileri Kaydedildi",
|
||||||
|
"credentialsSavedDescription": "Kimlik bilgileri başarılı bir şekilde yeniden oluşturuldu ve kaydedildi.",
|
||||||
|
"credentialsSaveError": "Kimlik Bilgileri Kayıt Hatası",
|
||||||
|
"credentialsSaveErrorDescription": "Kimlik bilgilerini yeniden oluştururken ve kaydederken bir hata oluştu.",
|
||||||
|
"regenerateCredentialsWarning": "Kimlik bilgilerini yeniden oluşturmak önceki bilgileri geçersiz kılacaktır. Bu kimlik bilgilerini kullanan tüm yapılandırmaları güncellediğinizden emin olun.",
|
||||||
|
"confirm": "Onayla",
|
||||||
|
"regenerateCredentialsConfirmation": "Kimlik bilgilerini yeniden oluşturmak istediğinizden emin misiniz?",
|
||||||
|
"endpoint": "Uç Nokta",
|
||||||
|
"Id": "Kimlik",
|
||||||
|
"SecretKey": "Gizli Anahtar",
|
||||||
|
"featureDisabledTooltip": "Bu özellik yalnızca kurumsal planda mevcuttur ve kullanmak için lisans gerektirir.",
|
||||||
|
"niceId": "Güzel Kimlik",
|
||||||
|
"niceIdUpdated": "Güzel Kimlik Güncellendi",
|
||||||
|
"niceIdUpdatedSuccessfully": "Güzel Kimlik Başarıyla Güncellendi",
|
||||||
|
"niceIdUpdateError": "Güzel Kimlik güncellenirken hata",
|
||||||
|
"niceIdUpdateErrorDescription": "Güzel Kimlik güncellenirken bir hata oluştu.",
|
||||||
|
"niceIdCannotBeEmpty": "Güzel Kimlik boş olamaz",
|
||||||
|
"enterIdentifier": "Tanımlayıcıyı girin",
|
||||||
|
"identifier": "Tanımlayıcı"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,15 @@
|
|||||||
"settingsErrorUpdateDescription": "更新设置时发生错误",
|
"settingsErrorUpdateDescription": "更新设置时发生错误",
|
||||||
"sidebarCollapse": "折叠",
|
"sidebarCollapse": "折叠",
|
||||||
"sidebarExpand": "展开",
|
"sidebarExpand": "展开",
|
||||||
|
"productUpdateMoreInfo": "{noOfUpdates} 个更新",
|
||||||
|
"productUpdateInfo": "{noOfUpdates} 个更新",
|
||||||
|
"productUpdateWhatsNew": "新功能",
|
||||||
|
"productUpdateTitle": "产品更新",
|
||||||
|
"productUpdateEmpty": "无更新",
|
||||||
|
"dismissAll": "关闭所有",
|
||||||
|
"pangolinUpdateAvailable": "新版本可用",
|
||||||
|
"pangolinUpdateAvailableInfo": "版本 {version} 已准备就绪",
|
||||||
|
"pangolinUpdateAvailableReleaseNotes": "查看发布笔记",
|
||||||
"newtUpdateAvailable": "更新可用",
|
"newtUpdateAvailable": "更新可用",
|
||||||
"newtUpdateAvailableInfo": "新版本的 Newt 已可用。请更新到最新版本以获得最佳体验。",
|
"newtUpdateAvailableInfo": "新版本的 Newt 已可用。请更新到最新版本以获得最佳体验。",
|
||||||
"domainPickerEnterDomain": "域名",
|
"domainPickerEnterDomain": "域名",
|
||||||
@@ -1525,6 +1534,12 @@
|
|||||||
"resourcesTableTheseResourcesForUseWith": "这些资源供...使用",
|
"resourcesTableTheseResourcesForUseWith": "这些资源供...使用",
|
||||||
"resourcesTableClients": "客户端",
|
"resourcesTableClients": "客户端",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "且仅在与客户端连接时可内部访问。",
|
"resourcesTableAndOnlyAccessibleInternally": "且仅在与客户端连接时可内部访问。",
|
||||||
|
"resourcesTableNoTargets": "没有目标",
|
||||||
|
"resourcesTableHealthy": "健康的",
|
||||||
|
"resourcesTableDegraded": "降级",
|
||||||
|
"resourcesTableOffline": "离线的",
|
||||||
|
"resourcesTableUnknown": "未知的",
|
||||||
|
"resourcesTableNotMonitored": "未监视的",
|
||||||
"editInternalResourceDialogEditClientResource": "编辑客户端资源",
|
"editInternalResourceDialogEditClientResource": "编辑客户端资源",
|
||||||
"editInternalResourceDialogUpdateResourceProperties": "更新{resourceName}的资源属性和目标配置。",
|
"editInternalResourceDialogUpdateResourceProperties": "更新{resourceName}的资源属性和目标配置。",
|
||||||
"editInternalResourceDialogResourceProperties": "资源属性",
|
"editInternalResourceDialogResourceProperties": "资源属性",
|
||||||
@@ -2095,5 +2110,31 @@
|
|||||||
"selectedResources": "选定的资源",
|
"selectedResources": "选定的资源",
|
||||||
"enableSelected": "启用选中的",
|
"enableSelected": "启用选中的",
|
||||||
"disableSelected": "禁用选中的",
|
"disableSelected": "禁用选中的",
|
||||||
"checkSelectedStatus": "检查选中的状态"
|
"checkSelectedStatus": "检查选中的状态",
|
||||||
}
|
"credentials": "全权证书",
|
||||||
|
"savecredentials": "保存证书",
|
||||||
|
"regeneratecredentials": "重置键",
|
||||||
|
"regenerateCredentials": "重新生成和保存您的凭据",
|
||||||
|
"generatedcredentials": "生成的证书",
|
||||||
|
"copyandsavethesecredentials": "复制和保存这些凭据",
|
||||||
|
"copyandsavethesecredentialsdescription": "这些凭据将不会在您离开此页面后再显示。现在安全地保存。",
|
||||||
|
"credentialsSaved": "凭据已保存",
|
||||||
|
"credentialsSavedDescription": "已成功生成和保存凭据。",
|
||||||
|
"credentialsSaveError": "证书保存错误",
|
||||||
|
"credentialsSaveErrorDescription": "更新和保存凭据时出错。",
|
||||||
|
"regenerateCredentialsWarning": "重新生成凭据将使以前的凭据失效。请确保更新使用这些凭据的任何配置。",
|
||||||
|
"confirm": "确认",
|
||||||
|
"regenerateCredentialsConfirmation": "您确定要重新生成凭据吗?",
|
||||||
|
"endpoint": "Endpoint",
|
||||||
|
"Id": "Id",
|
||||||
|
"SecretKey": "秘密密钥",
|
||||||
|
"featureDisabledTooltip": "此功能仅在企业计划中可用,需要许可证才能使用。",
|
||||||
|
"niceId": "好的 ID",
|
||||||
|
"niceIdUpdated": "好的 ID 已更新",
|
||||||
|
"niceIdUpdatedSuccessfully": "Nice ID 更新成功",
|
||||||
|
"niceIdUpdateError": "更新Nice ID时出错",
|
||||||
|
"niceIdUpdateErrorDescription": "更新Nice ID时出错。",
|
||||||
|
"niceIdCannotBeEmpty": "好的 ID 不能为空",
|
||||||
|
"enterIdentifier": "输入标识符",
|
||||||
|
"identifier": "Identifier"
|
||||||
|
}
|
||||||
|
|||||||
2099
messages/zh-TW.json
Normal file
2099
messages/zh-TW.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,16 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
import createNextIntlPlugin from "next-intl/plugin";
|
import createNextIntlPlugin from "next-intl/plugin";
|
||||||
|
|
||||||
const withNextIntl = createNextIntlPlugin();
|
const withNextIntl = createNextIntlPlugin();
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
const nextConfig: NextConfig = {
|
||||||
const nextConfig = {
|
|
||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true
|
ignoreDuringBuilds: true
|
||||||
},
|
},
|
||||||
output: "standalone",
|
experimental: {
|
||||||
|
reactCompiler: true
|
||||||
|
},
|
||||||
|
output: "standalone"
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withNextIntl(nextConfig);
|
export default withNextIntl(nextConfig);
|
||||||
9735
package-lock.json
generated
9735
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
64
package.json
64
package.json
@@ -22,8 +22,8 @@
|
|||||||
"set:oss": "echo 'export const build = \"oss\" as any;' > server/build.ts && cp tsconfig.oss.json tsconfig.json",
|
"set:oss": "echo 'export const build = \"oss\" as any;' > server/build.ts && cp tsconfig.oss.json tsconfig.json",
|
||||||
"set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts && cp tsconfig.saas.json tsconfig.json",
|
"set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts && cp tsconfig.saas.json tsconfig.json",
|
||||||
"set:enterprise": "echo 'export const build = \"enterprise\" as any;' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json",
|
"set:enterprise": "echo 'export const build = \"enterprise\" as any;' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json",
|
||||||
"set:sqlite": "echo 'export * from \"./sqlite\";' > server/db/index.ts",
|
"set:sqlite": "echo 'export * from \"./sqlite\";\nexport const driver: \"pg\" | \"sqlite\" = \"sqlite\";' > server/db/index.ts",
|
||||||
"set:pg": "echo 'export * from \"./pg\";' > server/db/index.ts",
|
"set:pg": "echo 'export * from \"./pg\";\nexport const driver: \"pg\" | \"sqlite\" = \"pg\";' > server/db/index.ts",
|
||||||
"next:build": "next build",
|
"next:build": "next build",
|
||||||
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
|
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
|
||||||
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
|
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
|
||||||
@@ -32,27 +32,29 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asteasolutions/zod-to-openapi": "^7.3.4",
|
"@asteasolutions/zod-to-openapi": "8.1.0",
|
||||||
"@aws-sdk/client-s3": "3.922.0",
|
"@aws-sdk/client-s3": "3.922.0",
|
||||||
|
"@faker-js/faker": "^10.1.0",
|
||||||
|
"@headlessui/react": "^2.2.9",
|
||||||
"@hookform/resolvers": "5.2.2",
|
"@hookform/resolvers": "5.2.2",
|
||||||
"@monaco-editor/react": "^4.7.0",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@node-rs/argon2": "^2.0.2",
|
"@node-rs/argon2": "^2.0.2",
|
||||||
"@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.11",
|
||||||
"@radix-ui/react-checkbox": "1.3.3",
|
"@radix-ui/react-checkbox": "1.3.3",
|
||||||
"@radix-ui/react-collapsible": "1.1.12",
|
"@radix-ui/react-collapsible": "1.1.12",
|
||||||
"@radix-ui/react-dialog": "1.1.15",
|
"@radix-ui/react-dialog": "1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "2.1.16",
|
"@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.8",
|
||||||
"@radix-ui/react-popover": "1.1.15",
|
"@radix-ui/react-popover": "1.1.15",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
"@radix-ui/react-radio-group": "1.3.8",
|
"@radix-ui/react-radio-group": "1.3.8",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-select": "2.2.6",
|
"@radix-ui/react-select": "2.2.6",
|
||||||
"@radix-ui/react-separator": "1.1.7",
|
"@radix-ui/react-separator": "1.1.8",
|
||||||
"@radix-ui/react-slot": "1.2.3",
|
"@radix-ui/react-slot": "1.2.4",
|
||||||
"@radix-ui/react-switch": "1.2.6",
|
"@radix-ui/react-switch": "1.2.6",
|
||||||
"@radix-ui/react-tabs": "1.1.13",
|
"@radix-ui/react-tabs": "1.1.13",
|
||||||
"@radix-ui/react-toast": "1.2.15",
|
"@radix-ui/react-toast": "1.2.15",
|
||||||
@@ -63,9 +65,10 @@
|
|||||||
"@simplewebauthn/browser": "^13.2.2",
|
"@simplewebauthn/browser": "^13.2.2",
|
||||||
"@simplewebauthn/server": "^13.2.2",
|
"@simplewebauthn/server": "^13.2.2",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
|
"@tanstack/react-query": "^5.90.6",
|
||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
"arctic": "^3.7.0",
|
"arctic": "^3.7.0",
|
||||||
"axios": "^1.13.1",
|
"axios": "^1.13.2",
|
||||||
"better-sqlite3": "11.7.0",
|
"better-sqlite3": "11.7.0",
|
||||||
"canvas-confetti": "1.9.4",
|
"canvas-confetti": "1.9.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -76,89 +79,96 @@
|
|||||||
"cookies": "^0.9.1",
|
"cookies": "^0.9.1",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
|
"d3": "^7.9.0",
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "4.1.0",
|
||||||
"drizzle-orm": "0.44.7",
|
"drizzle-orm": "0.44.7",
|
||||||
"eslint": "9.39.0",
|
"eslint": "9.39.1",
|
||||||
"eslint-config-next": "16.0.1",
|
"eslint-config-next": "16.0.3",
|
||||||
"express": "5.1.0",
|
"express": "5.1.0",
|
||||||
"express-rate-limit": "8.2.1",
|
"express-rate-limit": "8.2.1",
|
||||||
"glob": "11.0.3",
|
"glob": "11.1.0",
|
||||||
"helmet": "8.1.0",
|
"helmet": "8.1.0",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
"i": "^0.3.7",
|
"i": "^0.3.7",
|
||||||
"input-otp": "1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"ioredis": "5.8.2",
|
"ioredis": "5.8.2",
|
||||||
"jmespath": "^0.16.0",
|
"jmespath": "^0.16.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "^0.552.0",
|
"lucide-react": "^0.552.0",
|
||||||
"maxmind": "5.0.0",
|
"maxmind": "5.0.1",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.5.6",
|
"next": "15.5.7",
|
||||||
"next-intl": "^4.4.0",
|
"next-intl": "^4.4.0",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"nextjs-toploader": "^3.9.17",
|
"nextjs-toploader": "^3.9.17",
|
||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"nodemailer": "7.0.10",
|
"nodemailer": "7.0.10",
|
||||||
"npm": "^11.6.2",
|
"npm": "^11.6.4",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"oslo": "1.2.1",
|
"oslo": "1.2.1",
|
||||||
"pg": "^8.16.2",
|
"pg": "^8.16.2",
|
||||||
"posthog-node": "^5.11.0",
|
"posthog-node": "^5.11.2",
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.2.0",
|
"react": "19.2.1",
|
||||||
"react-day-picker": "9.11.1",
|
"react-day-picker": "9.11.1",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.1",
|
||||||
"react-easy-sort": "^1.8.0",
|
"react-easy-sort": "^1.8.0",
|
||||||
"react-hook-form": "7.66.0",
|
"react-hook-form": "7.66.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"rebuild": "0.1.2",
|
"rebuild": "0.1.2",
|
||||||
|
"recharts": "^2.15.4",
|
||||||
"reodotdev": "^1.0.0",
|
"reodotdev": "^1.0.0",
|
||||||
"resend": "^6.4.0",
|
"resend": "^6.4.2",
|
||||||
"semver": "^7.7.3",
|
"semver": "^7.7.3",
|
||||||
"stripe": "18.2.1",
|
"stripe": "18.2.1",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"tailwind-merge": "3.3.1",
|
"tailwind-merge": "3.3.1",
|
||||||
|
"topojson-client": "^3.1.0",
|
||||||
"tw-animate-css": "^1.3.8",
|
"tw-animate-css": "^1.3.8",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
|
"visionscarto-world-atlas": "^1.0.0",
|
||||||
"winston": "3.18.3",
|
"winston": "3.18.3",
|
||||||
"winston-daily-rotate-file": "5.0.0",
|
"winston-daily-rotate-file": "5.0.0",
|
||||||
"ws": "8.18.3",
|
"ws": "8.18.3",
|
||||||
"yaml": "^2.8.1",
|
"yaml": "^2.8.1",
|
||||||
"yargs": "18.0.0",
|
"yargs": "18.0.0",
|
||||||
"zod": "3.25.76",
|
"zod": "4.1.12",
|
||||||
"zod-validation-error": "3.5.2",
|
"zod-validation-error": "5.0.0"
|
||||||
"@faker-js/faker": "^10.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dotenvx/dotenvx": "1.51.1",
|
"@dotenvx/dotenvx": "1.51.1",
|
||||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||||
"@react-email/preview-server": "4.3.2",
|
"@react-email/preview-server": "4.3.2",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
|
"@tanstack/react-query-devtools": "^5.90.2",
|
||||||
"@types/better-sqlite3": "7.6.12",
|
"@types/better-sqlite3": "7.6.12",
|
||||||
"@types/cookie-parser": "1.4.10",
|
"@types/cookie-parser": "1.4.10",
|
||||||
"@types/cors": "2.8.19",
|
"@types/cors": "2.8.19",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
"@types/d3": "^7.4.3",
|
||||||
"@types/express": "5.0.5",
|
"@types/express": "5.0.5",
|
||||||
"@types/express-session": "^1.18.2",
|
"@types/express-session": "^1.18.2",
|
||||||
"@types/jmespath": "^0.15.2",
|
"@types/jmespath": "^0.15.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/nprogress": "^0.2.3",
|
"@types/node": "24.10.1",
|
||||||
"@types/node": "24.9.2",
|
|
||||||
"@types/nodemailer": "7.0.3",
|
"@types/nodemailer": "7.0.3",
|
||||||
|
"@types/nprogress": "^0.2.3",
|
||||||
"@types/pg": "8.15.6",
|
"@types/pg": "8.15.6",
|
||||||
"@types/react": "19.2.2",
|
"@types/react": "19.2.2",
|
||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.2",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/swagger-ui-express": "^4.1.8",
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
|
"@types/topojson-client": "^3.1.5",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
"@types/yargs": "17.0.34",
|
"@types/yargs": "17.0.34",
|
||||||
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"drizzle-kit": "0.31.6",
|
"drizzle-kit": "0.31.6",
|
||||||
"esbuild": "0.25.12",
|
"esbuild": "0.27.0",
|
||||||
"esbuild-node-externals": "1.18.0",
|
"esbuild-node-externals": "1.19.1",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"react-email": "4.3.2",
|
"react-email": "4.3.2",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
|
|||||||
@@ -79,6 +79,12 @@ export function createApiServer() {
|
|||||||
// Add request timeout middleware
|
// Add request timeout middleware
|
||||||
apiServer.use(requestTimeoutMiddleware(60000)); // 60 second timeout
|
apiServer.use(requestTimeoutMiddleware(60000)); // 60 second timeout
|
||||||
|
|
||||||
|
apiServer.use(logIncomingMiddleware);
|
||||||
|
|
||||||
|
if (build !== "oss") {
|
||||||
|
apiServer.use(`${prefix}/hybrid`, hybridRouter); // put before rate limiting because we will rate limit there separately because some of the routes are heavily used
|
||||||
|
}
|
||||||
|
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
apiServer.use(
|
apiServer.use(
|
||||||
rateLimit({
|
rateLimit({
|
||||||
@@ -101,11 +107,7 @@ export function createApiServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
apiServer.use(logIncomingMiddleware);
|
|
||||||
apiServer.use(prefix, unauthenticated);
|
apiServer.use(prefix, unauthenticated);
|
||||||
if (build !== "oss") {
|
|
||||||
apiServer.use(`${prefix}/hybrid`, hybridRouter);
|
|
||||||
}
|
|
||||||
apiServer.use(prefix, authenticated);
|
apiServer.use(prefix, authenticated);
|
||||||
|
|
||||||
// WebSocket routes
|
// WebSocket routes
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export enum ActionsEnum {
|
|||||||
getSite = "getSite",
|
getSite = "getSite",
|
||||||
listSites = "listSites",
|
listSites = "listSites",
|
||||||
updateSite = "updateSite",
|
updateSite = "updateSite",
|
||||||
|
reGenerateSecret = "reGenerateSecret",
|
||||||
createResource = "createResource",
|
createResource = "createResource",
|
||||||
deleteResource = "deleteResource",
|
deleteResource = "deleteResource",
|
||||||
getResource = "getResource",
|
getResource = "getResource",
|
||||||
@@ -85,6 +86,7 @@ export enum ActionsEnum {
|
|||||||
updateOrgDomain = "updateOrgDomain",
|
updateOrgDomain = "updateOrgDomain",
|
||||||
getDNSRecords = "getDNSRecords",
|
getDNSRecords = "getDNSRecords",
|
||||||
createNewt = "createNewt",
|
createNewt = "createNewt",
|
||||||
|
createOlm = "createOlm",
|
||||||
createIdp = "createIdp",
|
createIdp = "createIdp",
|
||||||
updateIdp = "updateIdp",
|
updateIdp = "updateIdp",
|
||||||
deleteIdp = "deleteIdp",
|
deleteIdp = "deleteIdp",
|
||||||
|
|||||||
@@ -36,13 +36,15 @@ export async function createSession(
|
|||||||
const sessionId = encodeHexLowerCase(
|
const sessionId = encodeHexLowerCase(
|
||||||
sha256(new TextEncoder().encode(token))
|
sha256(new TextEncoder().encode(token))
|
||||||
);
|
);
|
||||||
const session: Session = {
|
const [session] = await db
|
||||||
sessionId: sessionId,
|
.insert(sessions)
|
||||||
userId,
|
.values({
|
||||||
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
|
sessionId: sessionId,
|
||||||
issuedAt: new Date().getTime()
|
userId,
|
||||||
};
|
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
|
||||||
await db.insert(sessions).values(session);
|
issuedAt: new Date().getTime()
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,43 @@
|
|||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
import { validateSessionToken, SESSION_COOKIE_NAME } from "@server/auth/sessions/app";
|
import {
|
||||||
|
validateSessionToken,
|
||||||
|
SESSION_COOKIE_NAME
|
||||||
|
} from "@server/auth/sessions/app";
|
||||||
|
|
||||||
export async function verifySession(req: Request) {
|
export async function verifySession(req: Request, forceLogin?: boolean) {
|
||||||
const res = await validateSessionToken(
|
const res = await validateSessionToken(
|
||||||
req.cookies[SESSION_COOKIE_NAME] ?? "",
|
req.cookies[SESSION_COOKIE_NAME] ?? ""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!forceLogin) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
if (!res.session || !res.user) {
|
||||||
|
return {
|
||||||
|
session: null,
|
||||||
|
user: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (res.session.deviceAuthUsed) {
|
||||||
|
return {
|
||||||
|
session: null,
|
||||||
|
user: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!res.session.issuedAt) {
|
||||||
|
return {
|
||||||
|
session: null,
|
||||||
|
user: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const mins = 5 * 60 * 1000;
|
||||||
|
const now = new Date().getTime();
|
||||||
|
if (now - res.session.issuedAt > mins) {
|
||||||
|
return {
|
||||||
|
session: null,
|
||||||
|
user: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,11 +42,17 @@ export async function getUniqueResourceName(orgId: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const name = generateName();
|
const name = generateName();
|
||||||
const count = await db
|
const [resourceCount, siteResourceCount] = await Promise.all([
|
||||||
.select({ niceId: resources.niceId, orgId: resources.orgId })
|
db
|
||||||
.from(resources)
|
.select({ niceId: resources.niceId, orgId: resources.orgId })
|
||||||
.where(and(eq(resources.niceId, name), eq(resources.orgId, orgId)));
|
.from(resources)
|
||||||
if (count.length === 0) {
|
.where(and(eq(resources.niceId, name), eq(resources.orgId, orgId))),
|
||||||
|
db
|
||||||
|
.select({ niceId: siteResources.niceId, orgId: siteResources.orgId })
|
||||||
|
.from(siteResources)
|
||||||
|
.where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId)))
|
||||||
|
]);
|
||||||
|
if (resourceCount.length === 0 && siteResourceCount.length === 0) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
loops++;
|
loops++;
|
||||||
@@ -61,11 +67,17 @@ export async function getUniqueSiteResourceName(orgId: string): Promise<string>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const name = generateName();
|
const name = generateName();
|
||||||
const count = await db
|
const [resourceCount, siteResourceCount] = await Promise.all([
|
||||||
.select({ niceId: siteResources.niceId, orgId: siteResources.orgId })
|
db
|
||||||
.from(siteResources)
|
.select({ niceId: resources.niceId, orgId: resources.orgId })
|
||||||
.where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId)));
|
.from(resources)
|
||||||
if (count.length === 0) {
|
.where(and(eq(resources.niceId, name), eq(resources.orgId, orgId))),
|
||||||
|
db
|
||||||
|
.select({ niceId: siteResources.niceId, orgId: siteResources.orgId })
|
||||||
|
.from(siteResources)
|
||||||
|
.where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId)))
|
||||||
|
]);
|
||||||
|
if (resourceCount.length === 0 && siteResourceCount.length === 0) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
loops++;
|
loops++;
|
||||||
|
|||||||
@@ -13,9 +13,12 @@ function createDb() {
|
|||||||
connection_string: process.env.POSTGRES_CONNECTION_STRING
|
connection_string: process.env.POSTGRES_CONNECTION_STRING
|
||||||
};
|
};
|
||||||
if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) {
|
if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) {
|
||||||
const replicas = process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(",").map((conn) => ({
|
const replicas =
|
||||||
connection_string: conn.trim()
|
process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(
|
||||||
}));
|
","
|
||||||
|
).map((conn) => ({
|
||||||
|
connection_string: conn.trim()
|
||||||
|
}));
|
||||||
config.postgres.replicas = replicas;
|
config.postgres.replicas = replicas;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -40,28 +43,44 @@ function createDb() {
|
|||||||
connectionString,
|
connectionString,
|
||||||
max: poolConfig?.max_connections || 20,
|
max: poolConfig?.max_connections || 20,
|
||||||
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
||||||
connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000,
|
connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000
|
||||||
});
|
});
|
||||||
|
|
||||||
const replicas = [];
|
const replicas = [];
|
||||||
|
|
||||||
if (!replicaConnections.length) {
|
if (!replicaConnections.length) {
|
||||||
replicas.push(DrizzlePostgres(primaryPool));
|
replicas.push(
|
||||||
|
DrizzlePostgres(primaryPool, {
|
||||||
|
logger: process.env.NODE_ENV === "development"
|
||||||
|
})
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
for (const conn of replicaConnections) {
|
for (const conn of replicaConnections) {
|
||||||
const replicaPool = new Pool({
|
const replicaPool = new Pool({
|
||||||
connectionString: conn.connection_string,
|
connectionString: conn.connection_string,
|
||||||
max: poolConfig?.max_replica_connections || 20,
|
max: poolConfig?.max_replica_connections || 20,
|
||||||
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
||||||
connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000,
|
connectionTimeoutMillis:
|
||||||
|
poolConfig?.connection_timeout_ms || 5000
|
||||||
});
|
});
|
||||||
replicas.push(DrizzlePostgres(replicaPool));
|
replicas.push(
|
||||||
|
DrizzlePostgres(replicaPool, {
|
||||||
|
logger: process.env.NODE_ENV === "development"
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return withReplicas(DrizzlePostgres(primaryPool), replicas as any);
|
return withReplicas(
|
||||||
|
DrizzlePostgres(primaryPool, {
|
||||||
|
logger: process.env.QUERY_LOGGING === "true"
|
||||||
|
}),
|
||||||
|
replicas as any
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const db = createDb();
|
export const db = createDb();
|
||||||
export default db;
|
export default db;
|
||||||
export type Transaction = Parameters<Parameters<typeof db["transaction"]>[0]>[0];
|
export type Transaction = Parameters<
|
||||||
|
Parameters<(typeof db)["transaction"]>[0]
|
||||||
|
>[0];
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const runMigrations = async () => {
|
|||||||
migrationsFolder: migrationsFolder
|
migrationsFolder: migrationsFolder
|
||||||
});
|
});
|
||||||
console.log("Migrations completed successfully.");
|
console.log("Migrations completed successfully.");
|
||||||
|
process.exit(0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error running migrations:", error);
|
console.error("Error running migrations:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
import { InferSelectModel } from "drizzle-orm";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
|
import { alias } from "yargs";
|
||||||
|
|
||||||
export const domains = pgTable("domains", {
|
export const domains = pgTable("domains", {
|
||||||
domainId: varchar("domainId").primaryKey(),
|
domainId: varchar("domainId").primaryKey(),
|
||||||
@@ -40,6 +41,7 @@ export const orgs = pgTable("orgs", {
|
|||||||
orgId: varchar("orgId").primaryKey(),
|
orgId: varchar("orgId").primaryKey(),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
subnet: varchar("subnet"),
|
subnet: varchar("subnet"),
|
||||||
|
utilitySubnet: varchar("utilitySubnet"), // this is the subnet for utility addresses
|
||||||
createdAt: text("createdAt"),
|
createdAt: text("createdAt"),
|
||||||
requireTwoFactor: boolean("requireTwoFactor"),
|
requireTwoFactor: boolean("requireTwoFactor"),
|
||||||
maxSessionLengthHours: integer("maxSessionLengthHours"),
|
maxSessionLengthHours: integer("maxSessionLengthHours"),
|
||||||
@@ -88,8 +90,7 @@ export const sites = pgTable("sites", {
|
|||||||
publicKey: varchar("publicKey"),
|
publicKey: varchar("publicKey"),
|
||||||
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
||||||
listenPort: integer("listenPort"),
|
listenPort: integer("listenPort"),
|
||||||
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
|
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true)
|
||||||
remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resources = pgTable("resources", {
|
export const resources = pgTable("resources", {
|
||||||
@@ -175,7 +176,8 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
|
|||||||
hcFollowRedirects: boolean("hcFollowRedirects").default(true),
|
hcFollowRedirects: boolean("hcFollowRedirects").default(true),
|
||||||
hcMethod: varchar("hcMethod").default("GET"),
|
hcMethod: varchar("hcMethod").default("GET"),
|
||||||
hcStatus: integer("hcStatus"), // http code
|
hcStatus: integer("hcStatus"), // http code
|
||||||
hcHealth: text("hcHealth").default("unknown") // "unknown", "healthy", "unhealthy"
|
hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy"
|
||||||
|
hcTlsServerName: text("hcTlsServerName"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const exitNodes = pgTable("exitNodes", {
|
export const exitNodes = pgTable("exitNodes", {
|
||||||
@@ -204,11 +206,41 @@ export const siteResources = pgTable("siteResources", {
|
|||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
niceId: varchar("niceId").notNull(),
|
niceId: varchar("niceId").notNull(),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
protocol: varchar("protocol").notNull(),
|
mode: varchar("mode").notNull(), // "host" | "cidr" | "port"
|
||||||
proxyPort: integer("proxyPort").notNull(),
|
protocol: varchar("protocol"), // only for port mode
|
||||||
destinationPort: integer("destinationPort").notNull(),
|
proxyPort: integer("proxyPort"), // only for port mode
|
||||||
destinationIp: varchar("destinationIp").notNull(),
|
destinationPort: integer("destinationPort"), // only for port mode
|
||||||
enabled: boolean("enabled").notNull().default(true)
|
destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode
|
||||||
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
alias: varchar("alias"),
|
||||||
|
aliasAddress: varchar("aliasAddress")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clientSiteResources = pgTable("clientSiteResources", {
|
||||||
|
clientId: integer("clientId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => clients.clientId, { onDelete: "cascade" }),
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||||
|
});
|
||||||
|
|
||||||
|
export const roleSiteResources = pgTable("roleSiteResources", {
|
||||||
|
roleId: integer("roleId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userSiteResources = pgTable("userSiteResources", {
|
||||||
|
userId: varchar("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const users = pgTable("user", {
|
export const users = pgTable("user", {
|
||||||
@@ -256,7 +288,8 @@ export const sessions = pgTable("session", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, { onDelete: "cascade" }),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
|
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
|
||||||
issuedAt: bigint("issuedAt", { mode: "number" })
|
issuedAt: bigint("issuedAt", { mode: "number" }),
|
||||||
|
deviceAuthUsed: boolean("deviceAuthUsed").notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const newtSessions = pgTable("newtSession", {
|
export const newtSessions = pgTable("newtSession", {
|
||||||
@@ -598,7 +631,7 @@ export const idpOrg = pgTable("idpOrg", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const clients = pgTable("clients", {
|
export const clients = pgTable("clients", {
|
||||||
clientId: serial("id").primaryKey(),
|
clientId: serial("clientId").primaryKey(),
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
.references(() => orgs.orgId, {
|
.references(() => orgs.orgId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
@@ -607,6 +640,11 @@ export const clients = pgTable("clients", {
|
|||||||
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
|
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
|
||||||
onDelete: "set null"
|
onDelete: "set null"
|
||||||
}),
|
}),
|
||||||
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
olmId: text("olmId"), // to lock it to a specific olm optionally
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
pubKey: varchar("pubKey"),
|
pubKey: varchar("pubKey"),
|
||||||
subnet: varchar("subnet").notNull(),
|
subnet: varchar("subnet").notNull(),
|
||||||
@@ -621,23 +659,40 @@ export const clients = pgTable("clients", {
|
|||||||
maxConnections: integer("maxConnections")
|
maxConnections: integer("maxConnections")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clientSites = pgTable("clientSites", {
|
export const clientSitesAssociationsCache = pgTable(
|
||||||
clientId: integer("clientId")
|
"clientSitesAssociationsCache",
|
||||||
.notNull()
|
{
|
||||||
.references(() => clients.clientId, { onDelete: "cascade" }),
|
clientId: integer("clientId") // not a foreign key here so after its deleted the rebuild function can delete it and send the message
|
||||||
siteId: integer("siteId")
|
.notNull(),
|
||||||
.notNull()
|
siteId: integer("siteId").notNull(),
|
||||||
.references(() => sites.siteId, { onDelete: "cascade" }),
|
isRelayed: boolean("isRelayed").notNull().default(false),
|
||||||
isRelayed: boolean("isRelayed").notNull().default(false),
|
endpoint: varchar("endpoint"),
|
||||||
endpoint: varchar("endpoint")
|
publicKey: varchar("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const clientSiteResourcesAssociationsCache = pgTable(
|
||||||
|
"clientSiteResourcesAssociationsCache",
|
||||||
|
{
|
||||||
|
clientId: integer("clientId") // not a foreign key here so after its deleted the rebuild function can delete it and send the message
|
||||||
|
.notNull(),
|
||||||
|
siteResourceId: integer("siteResourceId").notNull()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const olms = pgTable("olms", {
|
export const olms = pgTable("olms", {
|
||||||
olmId: varchar("id").primaryKey(),
|
olmId: varchar("id").primaryKey(),
|
||||||
secretHash: varchar("secretHash").notNull(),
|
secretHash: varchar("secretHash").notNull(),
|
||||||
dateCreated: varchar("dateCreated").notNull(),
|
dateCreated: varchar("dateCreated").notNull(),
|
||||||
version: text("version"),
|
version: text("version"),
|
||||||
|
agent: text("agent"),
|
||||||
|
name: varchar("name"),
|
||||||
clientId: integer("clientId").references(() => clients.clientId, {
|
clientId: integer("clientId").references(() => clients.clientId, {
|
||||||
|
// we will switch this depending on the current org it wants to connect to
|
||||||
|
onDelete: "set null"
|
||||||
|
}),
|
||||||
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -753,6 +808,21 @@ export const requestAuditLog = pgTable(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const deviceWebAuthCodes = pgTable("deviceWebAuthCodes", {
|
||||||
|
codeId: serial("codeId").primaryKey(),
|
||||||
|
code: text("code").notNull().unique(),
|
||||||
|
ip: text("ip"),
|
||||||
|
city: text("city"),
|
||||||
|
deviceName: text("deviceName"),
|
||||||
|
applicationName: text("applicationName").notNull(),
|
||||||
|
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
|
||||||
|
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||||
|
verified: boolean("verified").notNull().default(false),
|
||||||
|
userId: varchar("userId").references(() => users.userId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
export type Org = InferSelectModel<typeof orgs>;
|
export type Org = InferSelectModel<typeof orgs>;
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
export type Site = InferSelectModel<typeof sites>;
|
export type Site = InferSelectModel<typeof sites>;
|
||||||
@@ -793,7 +863,7 @@ export type ApiKey = InferSelectModel<typeof apiKeys>;
|
|||||||
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
||||||
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
||||||
export type Client = InferSelectModel<typeof clients>;
|
export type Client = InferSelectModel<typeof clients>;
|
||||||
export type ClientSite = InferSelectModel<typeof clientSites>;
|
export type ClientSite = InferSelectModel<typeof clientSitesAssociationsCache>;
|
||||||
export type Olm = InferSelectModel<typeof olms>;
|
export type Olm = InferSelectModel<typeof olms>;
|
||||||
export type OlmSession = InferSelectModel<typeof olmSessions>;
|
export type OlmSession = InferSelectModel<typeof olmSessions>;
|
||||||
export type UserClient = InferSelectModel<typeof userClients>;
|
export type UserClient = InferSelectModel<typeof userClients>;
|
||||||
@@ -808,4 +878,5 @@ export type Blueprint = InferSelectModel<typeof blueprints>;
|
|||||||
export type LicenseKey = InferSelectModel<typeof licenseKey>;
|
export type LicenseKey = InferSelectModel<typeof licenseKey>;
|
||||||
export type SecurityKey = InferSelectModel<typeof securityKeys>;
|
export type SecurityKey = InferSelectModel<typeof securityKeys>;
|
||||||
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
|
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
|
||||||
|
export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
|
||||||
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
import { InferSelectModel } from "drizzle-orm";
|
||||||
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
||||||
import { boolean } from "yargs";
|
import { no } from "zod/v4/locales";
|
||||||
|
|
||||||
export const domains = sqliteTable("domains", {
|
export const domains = sqliteTable("domains", {
|
||||||
domainId: text("domainId").primaryKey(),
|
domainId: text("domainId").primaryKey(),
|
||||||
@@ -25,15 +25,15 @@ export const dnsRecords = sqliteTable("dnsRecords", {
|
|||||||
|
|
||||||
recordType: text("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT"
|
recordType: text("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT"
|
||||||
baseDomain: text("baseDomain"),
|
baseDomain: text("baseDomain"),
|
||||||
value: text("value").notNull(),
|
value: text("value").notNull(),
|
||||||
verified: integer("verified", { mode: "boolean" }).notNull().default(false),
|
verified: integer("verified", { mode: "boolean" }).notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export const orgs = sqliteTable("orgs", {
|
export const orgs = sqliteTable("orgs", {
|
||||||
orgId: text("orgId").primaryKey(),
|
orgId: text("orgId").primaryKey(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
subnet: text("subnet"),
|
subnet: text("subnet"),
|
||||||
|
utilitySubnet: text("utilitySubnet"), // this is the subnet for utility addresses
|
||||||
createdAt: text("createdAt"),
|
createdAt: text("createdAt"),
|
||||||
requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" }),
|
requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" }),
|
||||||
maxSessionLengthHours: integer("maxSessionLengthHours"), // hours
|
maxSessionLengthHours: integer("maxSessionLengthHours"), // hours
|
||||||
@@ -95,8 +95,7 @@ export const sites = sqliteTable("sites", {
|
|||||||
listenPort: integer("listenPort"),
|
listenPort: integer("listenPort"),
|
||||||
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(true),
|
.default(true)
|
||||||
remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resources = sqliteTable("resources", {
|
export const resources = sqliteTable("resources", {
|
||||||
@@ -142,9 +141,10 @@ export const resources = sqliteTable("resources", {
|
|||||||
onDelete: "set null"
|
onDelete: "set null"
|
||||||
}),
|
}),
|
||||||
headers: text("headers"), // comma-separated list of headers to add to the request
|
headers: text("headers"), // comma-separated list of headers to add to the request
|
||||||
proxyProtocol: integer("proxyProtocol", { mode: "boolean" }).notNull().default(false),
|
proxyProtocol: integer("proxyProtocol", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
proxyProtocolVersion: integer("proxyProtocolVersion").default(1)
|
proxyProtocolVersion: integer("proxyProtocolVersion").default(1)
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = sqliteTable("targets", {
|
export const targets = sqliteTable("targets", {
|
||||||
@@ -195,7 +195,8 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
|||||||
}).default(true),
|
}).default(true),
|
||||||
hcMethod: text("hcMethod").default("GET"),
|
hcMethod: text("hcMethod").default("GET"),
|
||||||
hcStatus: integer("hcStatus"), // http code
|
hcStatus: integer("hcStatus"), // http code
|
||||||
hcHealth: text("hcHealth").default("unknown") // "unknown", "healthy", "unhealthy"
|
hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy"
|
||||||
|
hcTlsServerName: text("hcTlsServerName"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const exitNodes = sqliteTable("exitNodes", {
|
export const exitNodes = sqliteTable("exitNodes", {
|
||||||
@@ -226,11 +227,41 @@ export const siteResources = sqliteTable("siteResources", {
|
|||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
niceId: text("niceId").notNull(),
|
niceId: text("niceId").notNull(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
protocol: text("protocol").notNull(),
|
mode: text("mode").notNull(), // "host" | "cidr" | "port"
|
||||||
proxyPort: integer("proxyPort").notNull(),
|
protocol: text("protocol"), // only for port mode
|
||||||
destinationPort: integer("destinationPort").notNull(),
|
proxyPort: integer("proxyPort"), // only for port mode
|
||||||
destinationIp: text("destinationIp").notNull(),
|
destinationPort: integer("destinationPort"), // only for port mode
|
||||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true)
|
destination: text("destination").notNull(), // ip, cidr, hostname
|
||||||
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
|
alias: text("alias"),
|
||||||
|
aliasAddress: text("aliasAddress")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clientSiteResources = sqliteTable("clientSiteResources", {
|
||||||
|
clientId: integer("clientId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => clients.clientId, { onDelete: "cascade" }),
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||||
|
});
|
||||||
|
|
||||||
|
export const roleSiteResources = sqliteTable("roleSiteResources", {
|
||||||
|
roleId: integer("roleId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userSiteResources = sqliteTable("userSiteResources", {
|
||||||
|
userId: text("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const users = sqliteTable("user", {
|
export const users = sqliteTable("user", {
|
||||||
@@ -306,7 +337,7 @@ export const newts = sqliteTable("newt", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const clients = sqliteTable("clients", {
|
export const clients = sqliteTable("clients", {
|
||||||
clientId: integer("id").primaryKey({ autoIncrement: true }),
|
clientId: integer("clientId").primaryKey({ autoIncrement: true }),
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
.references(() => orgs.orgId, {
|
.references(() => orgs.orgId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
@@ -315,8 +346,14 @@ export const clients = sqliteTable("clients", {
|
|||||||
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
|
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
|
||||||
onDelete: "set null"
|
onDelete: "set null"
|
||||||
}),
|
}),
|
||||||
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
pubKey: text("pubKey"),
|
pubKey: text("pubKey"),
|
||||||
|
olmId: text("olmId"), // to lock it to a specific olm optionally
|
||||||
subnet: text("subnet").notNull(),
|
subnet: text("subnet").notNull(),
|
||||||
megabytesIn: integer("bytesIn"),
|
megabytesIn: integer("bytesIn"),
|
||||||
megabytesOut: integer("bytesOut"),
|
megabytesOut: integer("bytesOut"),
|
||||||
@@ -328,25 +365,42 @@ export const clients = sqliteTable("clients", {
|
|||||||
lastHolePunch: integer("lastHolePunch")
|
lastHolePunch: integer("lastHolePunch")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clientSites = sqliteTable("clientSites", {
|
export const clientSitesAssociationsCache = sqliteTable(
|
||||||
clientId: integer("clientId")
|
"clientSitesAssociationsCache",
|
||||||
.notNull()
|
{
|
||||||
.references(() => clients.clientId, { onDelete: "cascade" }),
|
clientId: integer("clientId") // not a foreign key here so after its deleted the rebuild function can delete it and send the message
|
||||||
siteId: integer("siteId")
|
.notNull(),
|
||||||
.notNull()
|
siteId: integer("siteId").notNull(),
|
||||||
.references(() => sites.siteId, { onDelete: "cascade" }),
|
isRelayed: integer("isRelayed", { mode: "boolean" })
|
||||||
isRelayed: integer("isRelayed", { mode: "boolean" })
|
.notNull()
|
||||||
.notNull()
|
.default(false),
|
||||||
.default(false),
|
endpoint: text("endpoint"),
|
||||||
endpoint: text("endpoint")
|
publicKey: text("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const clientSiteResourcesAssociationsCache = sqliteTable(
|
||||||
|
"clientSiteResourcesAssociationsCache",
|
||||||
|
{
|
||||||
|
clientId: integer("clientId") // not a foreign key here so after its deleted the rebuild function can delete it and send the message
|
||||||
|
.notNull(),
|
||||||
|
siteResourceId: integer("siteResourceId").notNull()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const olms = sqliteTable("olms", {
|
export const olms = sqliteTable("olms", {
|
||||||
olmId: text("id").primaryKey(),
|
olmId: text("id").primaryKey(),
|
||||||
secretHash: text("secretHash").notNull(),
|
secretHash: text("secretHash").notNull(),
|
||||||
dateCreated: text("dateCreated").notNull(),
|
dateCreated: text("dateCreated").notNull(),
|
||||||
version: text("version"),
|
version: text("version"),
|
||||||
|
agent: text("agent"),
|
||||||
|
name: text("name"),
|
||||||
clientId: integer("clientId").references(() => clients.clientId, {
|
clientId: integer("clientId").references(() => clients.clientId, {
|
||||||
|
// we will switch this depending on the current org it wants to connect to
|
||||||
|
onDelete: "set null"
|
||||||
|
}),
|
||||||
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -365,7 +419,10 @@ export const sessions = sqliteTable("session", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, { onDelete: "cascade" }),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
expiresAt: integer("expiresAt").notNull(),
|
expiresAt: integer("expiresAt").notNull(),
|
||||||
issuedAt: integer("issuedAt")
|
issuedAt: integer("issuedAt"),
|
||||||
|
deviceAuthUsed: integer("deviceAuthUsed", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const newtSessions = sqliteTable("newtSession", {
|
export const newtSessions = sqliteTable("newtSession", {
|
||||||
@@ -802,6 +859,21 @@ export const requestAuditLog = sqliteTable(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const deviceWebAuthCodes = sqliteTable("deviceWebAuthCodes", {
|
||||||
|
codeId: integer("codeId").primaryKey({ autoIncrement: true }),
|
||||||
|
code: text("code").notNull().unique(),
|
||||||
|
ip: text("ip"),
|
||||||
|
city: text("city"),
|
||||||
|
deviceName: text("deviceName"),
|
||||||
|
applicationName: text("applicationName").notNull(),
|
||||||
|
expiresAt: integer("expiresAt").notNull(),
|
||||||
|
createdAt: integer("createdAt").notNull(),
|
||||||
|
verified: integer("verified", { mode: "boolean" }).notNull().default(false),
|
||||||
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
export type Org = InferSelectModel<typeof orgs>;
|
export type Org = InferSelectModel<typeof orgs>;
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
export type Site = InferSelectModel<typeof sites>;
|
export type Site = InferSelectModel<typeof sites>;
|
||||||
@@ -840,7 +912,7 @@ export type ResourceRule = InferSelectModel<typeof resourceRules>;
|
|||||||
export type Domain = InferSelectModel<typeof domains>;
|
export type Domain = InferSelectModel<typeof domains>;
|
||||||
export type DnsRecord = InferSelectModel<typeof dnsRecords>;
|
export type DnsRecord = InferSelectModel<typeof dnsRecords>;
|
||||||
export type Client = InferSelectModel<typeof clients>;
|
export type Client = InferSelectModel<typeof clients>;
|
||||||
export type ClientSite = InferSelectModel<typeof clientSites>;
|
export type ClientSite = InferSelectModel<typeof clientSitesAssociationsCache>;
|
||||||
export type RoleClient = InferSelectModel<typeof roleClients>;
|
export type RoleClient = InferSelectModel<typeof roleClients>;
|
||||||
export type UserClient = InferSelectModel<typeof userClients>;
|
export type UserClient = InferSelectModel<typeof userClients>;
|
||||||
export type SupporterKey = InferSelectModel<typeof supporterKey>;
|
export type SupporterKey = InferSelectModel<typeof supporterKey>;
|
||||||
@@ -859,3 +931,4 @@ export type LicenseKey = InferSelectModel<typeof licenseKey>;
|
|||||||
export type SecurityKey = InferSelectModel<typeof securityKeys>;
|
export type SecurityKey = InferSelectModel<typeof securityKeys>;
|
||||||
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
|
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
|
||||||
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
||||||
|
export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
ApiKeyOrg,
|
ApiKeyOrg,
|
||||||
RemoteExitNode,
|
RemoteExitNode,
|
||||||
Session,
|
Session,
|
||||||
|
SiteResource,
|
||||||
User,
|
User,
|
||||||
UserOrg
|
UserOrg
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
@@ -77,6 +78,8 @@ declare global {
|
|||||||
userOrgId?: string;
|
userOrgId?: string;
|
||||||
userOrgIds?: string[];
|
userOrgIds?: string[];
|
||||||
remoteExitNode?: RemoteExitNode;
|
remoteExitNode?: RemoteExitNode;
|
||||||
|
siteResource?: SiteResource;
|
||||||
|
orgPolicyAllowed?: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,19 +122,17 @@ export async function applyBlueprint({
|
|||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (site) {
|
logger.debug(
|
||||||
logger.debug(
|
`Updating client resource ${result.resource.siteResourceId} on site ${site.sites.siteId}`
|
||||||
`Updating client resource ${result.resource.siteResourceId} on site ${site.sites.siteId}`
|
);
|
||||||
);
|
|
||||||
|
|
||||||
await addClientTargets(
|
// await addClientTargets(
|
||||||
site.newt.newtId,
|
// site.newt.newtId,
|
||||||
result.resource.destinationIp,
|
// result.resource.destination,
|
||||||
result.resource.destinationPort,
|
// result.resource.destinationPort,
|
||||||
result.resource.protocol,
|
// result.resource.protocol,
|
||||||
result.resource.proxyPort
|
// result.resource.proxyPort
|
||||||
);
|
// );
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
blueprintSucceeded = true;
|
blueprintSucceeded = true;
|
||||||
|
|||||||
@@ -75,8 +75,9 @@ export async function updateClientResources(
|
|||||||
.set({
|
.set({
|
||||||
name: resourceData.name || resourceNiceId,
|
name: resourceData.name || resourceNiceId,
|
||||||
siteId: site.siteId,
|
siteId: site.siteId,
|
||||||
|
mode: "port",
|
||||||
proxyPort: resourceData["proxy-port"]!,
|
proxyPort: resourceData["proxy-port"]!,
|
||||||
destinationIp: resourceData.hostname,
|
destination: resourceData.hostname,
|
||||||
destinationPort: resourceData["internal-port"],
|
destinationPort: resourceData["internal-port"],
|
||||||
protocol: resourceData.protocol
|
protocol: resourceData.protocol
|
||||||
})
|
})
|
||||||
@@ -98,8 +99,9 @@ export async function updateClientResources(
|
|||||||
siteId: site.siteId,
|
siteId: site.siteId,
|
||||||
niceId: resourceNiceId,
|
niceId: resourceNiceId,
|
||||||
name: resourceData.name || resourceNiceId,
|
name: resourceData.name || resourceNiceId,
|
||||||
|
mode: "port",
|
||||||
proxyPort: resourceData["proxy-port"]!,
|
proxyPort: resourceData["proxy-port"]!,
|
||||||
destinationIp: resourceData.hostname,
|
destination: resourceData.hostname,
|
||||||
destinationPort: resourceData["internal-port"],
|
destinationPort: resourceData["internal-port"],
|
||||||
protocol: resourceData.protocol
|
protocol: resourceData.protocol
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ export async function updateProxyResources(
|
|||||||
domainId: domain ? domain.domainId : null,
|
domainId: domain ? domain.domainId : null,
|
||||||
enabled: resourceEnabled,
|
enabled: resourceEnabled,
|
||||||
sso: resourceData.auth?.["sso-enabled"] || false,
|
sso: resourceData.auth?.["sso-enabled"] || false,
|
||||||
|
skipToIdpId: resourceData.auth?.["auto-login-idp"] || null,
|
||||||
ssl: resourceSsl,
|
ssl: resourceSsl,
|
||||||
setHostHeader: resourceData["host-header"] || null,
|
setHostHeader: resourceData["host-header"] || null,
|
||||||
tlsServerName: resourceData["tls-server-name"] || null,
|
tlsServerName: resourceData["tls-server-name"] || null,
|
||||||
@@ -610,6 +611,7 @@ export async function updateProxyResources(
|
|||||||
domainId: domain ? domain.domainId : null,
|
domainId: domain ? domain.domainId : null,
|
||||||
enabled: resourceEnabled,
|
enabled: resourceEnabled,
|
||||||
sso: resourceData.auth?.["sso-enabled"] || false,
|
sso: resourceData.auth?.["sso-enabled"] || false,
|
||||||
|
skipToIdpId: resourceData.auth?.["auto-login-idp"] || null,
|
||||||
setHostHeader: resourceData["host-header"] || null,
|
setHostHeader: resourceData["host-header"] || null,
|
||||||
tlsServerName: resourceData["tls-server-name"] || null,
|
tlsServerName: resourceData["tls-server-name"] || null,
|
||||||
ssl: resourceSsl,
|
ssl: resourceSsl,
|
||||||
@@ -789,10 +791,6 @@ async function syncRoleResources(
|
|||||||
.where(eq(roleResources.resourceId, resourceId));
|
.where(eq(roleResources.resourceId, resourceId));
|
||||||
|
|
||||||
for (const roleName of ssoRoles) {
|
for (const roleName of ssoRoles) {
|
||||||
if (roleName === "Admin") {
|
|
||||||
continue; // never add admin access
|
|
||||||
}
|
|
||||||
|
|
||||||
const [role] = await trx
|
const [role] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(roles)
|
.from(roles)
|
||||||
@@ -803,6 +801,10 @@ async function syncRoleResources(
|
|||||||
throw new Error(`Role not found: ${roleName} in org ${orgId}`);
|
throw new Error(`Role not found: ${roleName} in org ${orgId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (role.isAdmin) {
|
||||||
|
continue; // never add admin access
|
||||||
|
}
|
||||||
|
|
||||||
const existingRoleResource = existingRoleResources.find(
|
const existingRoleResource = existingRoleResources.find(
|
||||||
(rr) => rr.roleId === role.roleId
|
(rr) => rr.roleId === role.roleId
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,20 +7,20 @@ export const SiteSchema = z.object({
|
|||||||
|
|
||||||
export const TargetHealthCheckSchema = z.object({
|
export const TargetHealthCheckSchema = z.object({
|
||||||
hostname: z.string(),
|
hostname: z.string(),
|
||||||
port: z.number().int().min(1).max(65535),
|
port: z.int().min(1).max(65535),
|
||||||
enabled: z.boolean().optional().default(true),
|
enabled: z.boolean().optional().default(true),
|
||||||
path: z.string().optional(),
|
path: z.string().optional(),
|
||||||
scheme: z.string().optional(),
|
scheme: z.string().optional(),
|
||||||
mode: z.string().default("http"),
|
mode: z.string().default("http"),
|
||||||
interval: z.number().int().default(30),
|
interval: z.int().default(30),
|
||||||
"unhealthy-interval": z.number().int().default(30),
|
"unhealthy-interval": z.int().default(30),
|
||||||
unhealthyInterval: z.number().int().optional(), // deprecated alias
|
unhealthyInterval: z.int().optional(), // deprecated alias
|
||||||
timeout: z.number().int().default(5),
|
timeout: z.int().default(5),
|
||||||
headers: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional().default(null),
|
headers: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional().default(null),
|
||||||
"follow-redirects": z.boolean().default(true),
|
"follow-redirects": z.boolean().default(true),
|
||||||
followRedirects: z.boolean().optional(), // deprecated alias
|
followRedirects: z.boolean().optional(), // deprecated alias
|
||||||
method: z.string().default("GET"),
|
method: z.string().default("GET"),
|
||||||
status: z.number().int().optional()
|
status: z.int().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Schema for individual target within a resource
|
// Schema for individual target within a resource
|
||||||
@@ -28,16 +28,16 @@ export const TargetSchema = z.object({
|
|||||||
site: z.string().optional(),
|
site: z.string().optional(),
|
||||||
method: z.enum(["http", "https", "h2c"]).optional(),
|
method: z.enum(["http", "https", "h2c"]).optional(),
|
||||||
hostname: z.string(),
|
hostname: z.string(),
|
||||||
port: z.number().int().min(1).max(65535),
|
port: z.int().min(1).max(65535),
|
||||||
enabled: z.boolean().optional().default(true),
|
enabled: z.boolean().optional().default(true),
|
||||||
"internal-port": z.number().int().min(1).max(65535).optional(),
|
"internal-port": z.int().min(1).max(65535).optional(),
|
||||||
path: z.string().optional(),
|
path: z.string().optional(),
|
||||||
"path-match": z.enum(["exact", "prefix", "regex"]).optional().nullable(),
|
"path-match": z.enum(["exact", "prefix", "regex"]).optional().nullable(),
|
||||||
healthcheck: TargetHealthCheckSchema.optional(),
|
healthcheck: TargetHealthCheckSchema.optional(),
|
||||||
rewritePath: z.string().optional(), // deprecated alias
|
rewritePath: z.string().optional(), // deprecated alias
|
||||||
"rewrite-path": z.string().optional(),
|
"rewrite-path": z.string().optional(),
|
||||||
"rewrite-match": z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(),
|
"rewrite-match": z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(),
|
||||||
priority: z.number().int().min(1).max(1000).optional().default(100)
|
priority: z.int().min(1).max(1000).optional().default(100)
|
||||||
});
|
});
|
||||||
export type TargetData = z.infer<typeof TargetSchema>;
|
export type TargetData = z.infer<typeof TargetSchema>;
|
||||||
|
|
||||||
@@ -55,10 +55,11 @@ export const AuthSchema = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
.default([])
|
.default([])
|
||||||
.refine((roles) => !roles.includes("Admin"), {
|
.refine((roles) => !roles.includes("Admin"), {
|
||||||
message: "Admin role cannot be included in sso-roles"
|
error: "Admin role cannot be included in sso-roles"
|
||||||
}),
|
}),
|
||||||
"sso-users": z.array(z.string().email()).optional().default([]),
|
"sso-users": z.array(z.email()).optional().default([]),
|
||||||
"whitelist-users": z.array(z.string().email()).optional().default([]),
|
"whitelist-users": z.array(z.email()).optional().default([]),
|
||||||
|
"auto-login-idp": z.int().positive().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RuleSchema = z.object({
|
export const RuleSchema = z.object({
|
||||||
@@ -79,7 +80,7 @@ export const ResourceSchema = z
|
|||||||
protocol: z.enum(["http", "tcp", "udp"]).optional(),
|
protocol: z.enum(["http", "tcp", "udp"]).optional(),
|
||||||
ssl: z.boolean().optional(),
|
ssl: z.boolean().optional(),
|
||||||
"full-domain": z.string().optional(),
|
"full-domain": z.string().optional(),
|
||||||
"proxy-port": z.number().int().min(1).max(65535).optional(),
|
"proxy-port": z.int().min(1).max(65535).optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
targets: z.array(TargetSchema.nullable()).optional().default([]),
|
targets: z.array(TargetSchema.nullable()).optional().default([]),
|
||||||
auth: AuthSchema.optional(),
|
auth: AuthSchema.optional(),
|
||||||
@@ -100,9 +101,8 @@ export const ResourceSchema = z
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message:
|
path: ["name", "protocol"],
|
||||||
"Resource must either be targets-only (only 'targets' field) or have both 'name' and 'protocol' fields at a minimum",
|
error: "Resource must either be targets-only (only 'targets' field) or have both 'name' and 'protocol' fields at a minimum"
|
||||||
path: ["name", "protocol"]
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
@@ -117,6 +117,20 @@ export const ResourceSchema = z
|
|||||||
(target) => target == null || target.method !== undefined
|
(target) => target == null || target.method !== undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ["targets"],
|
||||||
|
error: "When protocol is 'http', all targets must have a 'method' field"
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(resource) => {
|
||||||
|
if (isTargetsOnlyResource(resource)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// If protocol is tcp or udp, no target should have method field
|
// If protocol is tcp or udp, no target should have method field
|
||||||
if (resource.protocol === "tcp" || resource.protocol === "udp") {
|
if (resource.protocol === "tcp" || resource.protocol === "udp") {
|
||||||
return resource.targets.every(
|
return resource.targets.every(
|
||||||
@@ -125,19 +139,9 @@ export const ResourceSchema = z
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
(resource) => {
|
{
|
||||||
if (resource.protocol === "http") {
|
path: ["targets"],
|
||||||
return {
|
error: "When protocol is 'tcp' or 'udp', targets must not have a 'method' field"
|
||||||
message:
|
|
||||||
"When protocol is 'http', all targets must have a 'method' field",
|
|
||||||
path: ["targets"]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
message:
|
|
||||||
"When protocol is 'tcp' or 'udp', targets must not have a 'method' field",
|
|
||||||
path: ["targets"]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
@@ -156,9 +160,8 @@ export const ResourceSchema = z
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message:
|
path: ["full-domain"],
|
||||||
"When protocol is 'http', a 'full-domain' must be provided",
|
error: "When protocol is 'http', a 'full-domain' must be provided"
|
||||||
path: ["full-domain"]
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
@@ -174,9 +177,8 @@ export const ResourceSchema = z
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message:
|
path: ["proxy-port", "exit-node"],
|
||||||
"When protocol is 'tcp' or 'udp', 'proxy-port' must be provided",
|
error: "When protocol is 'tcp' or 'udp', 'proxy-port' must be provided"
|
||||||
path: ["proxy-port", "exit-node"]
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
@@ -193,9 +195,8 @@ export const ResourceSchema = z
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message:
|
path: ["auth"],
|
||||||
"When protocol is 'tcp' or 'udp', 'auth' must not be provided",
|
error: "When protocol is 'tcp' or 'udp', 'auth' must not be provided"
|
||||||
path: ["auth"]
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -216,36 +217,12 @@ export const ClientResourceSchema = z.object({
|
|||||||
// Schema for the entire configuration object
|
// Schema for the entire configuration object
|
||||||
export const ConfigSchema = z
|
export const ConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
"proxy-resources": z.record(z.string(), ResourceSchema).optional().default({}),
|
"proxy-resources": z.record(z.string(), ResourceSchema).optional().prefault({}),
|
||||||
"client-resources": z.record(z.string(), ClientResourceSchema).optional().default({}),
|
"client-resources": z.record(z.string(), ClientResourceSchema).optional().prefault({}),
|
||||||
sites: z.record(z.string(), SiteSchema).optional().default({})
|
sites: z.record(z.string(), SiteSchema).optional().prefault({})
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
// Enforce the full-domain uniqueness across resources in the same stack
|
// Enforce the full-domain uniqueness across resources in the same stack
|
||||||
(config) => {
|
|
||||||
// Extract all full-domain values with their resource keys
|
|
||||||
const fullDomainMap = new Map<string, string[]>();
|
|
||||||
|
|
||||||
Object.entries(config["proxy-resources"]).forEach(
|
|
||||||
([resourceKey, resource]) => {
|
|
||||||
const fullDomain = resource["full-domain"];
|
|
||||||
if (fullDomain) {
|
|
||||||
// Only process if full-domain is defined
|
|
||||||
if (!fullDomainMap.has(fullDomain)) {
|
|
||||||
fullDomainMap.set(fullDomain, []);
|
|
||||||
}
|
|
||||||
fullDomainMap.get(fullDomain)!.push(resourceKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Find duplicates
|
|
||||||
const duplicates = Array.from(fullDomainMap.entries()).filter(
|
|
||||||
([_, resourceKeys]) => resourceKeys.length > 1
|
|
||||||
);
|
|
||||||
|
|
||||||
return duplicates.length === 0;
|
|
||||||
},
|
|
||||||
(config) => {
|
(config) => {
|
||||||
// Extract duplicates for error message
|
// Extract duplicates for error message
|
||||||
const fullDomainMap = new Map<string, string[]>();
|
const fullDomainMap = new Map<string, string[]>();
|
||||||
@@ -271,38 +248,16 @@ export const ConfigSchema = z
|
|||||||
)
|
)
|
||||||
.join("; ");
|
.join("; ");
|
||||||
|
|
||||||
return {
|
if (duplicates.length !== 0) {
|
||||||
message: `Duplicate 'full-domain' values found: ${duplicates}`,
|
return {
|
||||||
path: ["resources"]
|
path: ["resources"],
|
||||||
};
|
error: `Duplicate 'full-domain' values found: ${duplicates}`
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
// Enforce proxy-port uniqueness within proxy-resources per protocol
|
// Enforce proxy-port uniqueness within proxy-resources per protocol
|
||||||
(config) => {
|
|
||||||
const protocolPortMap = new Map<string, string[]>();
|
|
||||||
|
|
||||||
Object.entries(config["proxy-resources"]).forEach(
|
|
||||||
([resourceKey, resource]) => {
|
|
||||||
const proxyPort = resource["proxy-port"];
|
|
||||||
const protocol = resource.protocol;
|
|
||||||
if (proxyPort !== undefined && protocol !== undefined) {
|
|
||||||
const key = `${protocol}:${proxyPort}`;
|
|
||||||
if (!protocolPortMap.has(key)) {
|
|
||||||
protocolPortMap.set(key, []);
|
|
||||||
}
|
|
||||||
protocolPortMap.get(key)!.push(resourceKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Find duplicates
|
|
||||||
const duplicates = Array.from(protocolPortMap.entries()).filter(
|
|
||||||
([_, resourceKeys]) => resourceKeys.length > 1
|
|
||||||
);
|
|
||||||
|
|
||||||
return duplicates.length === 0;
|
|
||||||
},
|
|
||||||
(config) => {
|
(config) => {
|
||||||
// Extract duplicates for error message
|
// Extract duplicates for error message
|
||||||
const protocolPortMap = new Map<string, string[]>();
|
const protocolPortMap = new Map<string, string[]>();
|
||||||
@@ -331,36 +286,16 @@ export const ConfigSchema = z
|
|||||||
)
|
)
|
||||||
.join("; ");
|
.join("; ");
|
||||||
|
|
||||||
return {
|
if (duplicates.length !== 0) {
|
||||||
message: `Duplicate 'proxy-port' values found in proxy-resources: ${duplicates}`,
|
return {
|
||||||
path: ["proxy-resources"]
|
path: ["proxy-resources"],
|
||||||
};
|
error: `Duplicate 'proxy-port' values found in proxy-resources: ${duplicates}`
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
// Enforce proxy-port uniqueness within client-resources
|
// Enforce proxy-port uniqueness within client-resources
|
||||||
(config) => {
|
|
||||||
const proxyPortMap = new Map<number, string[]>();
|
|
||||||
|
|
||||||
Object.entries(config["client-resources"]).forEach(
|
|
||||||
([resourceKey, resource]) => {
|
|
||||||
const proxyPort = resource["proxy-port"];
|
|
||||||
if (proxyPort !== undefined) {
|
|
||||||
if (!proxyPortMap.has(proxyPort)) {
|
|
||||||
proxyPortMap.set(proxyPort, []);
|
|
||||||
}
|
|
||||||
proxyPortMap.get(proxyPort)!.push(resourceKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Find duplicates
|
|
||||||
const duplicates = Array.from(proxyPortMap.entries()).filter(
|
|
||||||
([_, resourceKeys]) => resourceKeys.length > 1
|
|
||||||
);
|
|
||||||
|
|
||||||
return duplicates.length === 0;
|
|
||||||
},
|
|
||||||
(config) => {
|
(config) => {
|
||||||
// Extract duplicates for error message
|
// Extract duplicates for error message
|
||||||
const proxyPortMap = new Map<number, string[]>();
|
const proxyPortMap = new Map<number, string[]>();
|
||||||
@@ -385,10 +320,12 @@ export const ConfigSchema = z
|
|||||||
)
|
)
|
||||||
.join("; ");
|
.join("; ");
|
||||||
|
|
||||||
return {
|
if (duplicates.length !== 0) {
|
||||||
message: `Duplicate 'proxy-port' values found in client-resources: ${duplicates}`,
|
return {
|
||||||
path: ["client-resources"]
|
path: ["client-resources"],
|
||||||
};
|
error: `Duplicate 'proxy-port' values found in client-resources: ${duplicates}`
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
286
server/lib/calculateUserClientsForOrgs.ts
Normal file
286
server/lib/calculateUserClientsForOrgs.ts
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
import {
|
||||||
|
clients,
|
||||||
|
db,
|
||||||
|
olms,
|
||||||
|
orgs,
|
||||||
|
roleClients,
|
||||||
|
roles,
|
||||||
|
userClients,
|
||||||
|
userOrgs,
|
||||||
|
Transaction
|
||||||
|
} from "@server/db";
|
||||||
|
import { eq, and, notInArray } from "drizzle-orm";
|
||||||
|
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
||||||
|
import { getNextAvailableClientSubnet } from "@server/lib/ip";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { rebuildClientAssociationsFromClient } from "./rebuildClientAssociations";
|
||||||
|
import { sendTerminateClient } from "@server/routers/client/terminate";
|
||||||
|
|
||||||
|
export async function calculateUserClientsForOrgs(
|
||||||
|
userId: string,
|
||||||
|
trx?: Transaction
|
||||||
|
): Promise<void> {
|
||||||
|
const execute = async (transaction: Transaction) => {
|
||||||
|
// Get all OLMs for this user
|
||||||
|
const userOlms = await transaction
|
||||||
|
.select()
|
||||||
|
.from(olms)
|
||||||
|
.where(eq(olms.userId, userId));
|
||||||
|
|
||||||
|
if (userOlms.length === 0) {
|
||||||
|
// No OLMs for this user, but we should still clean up any orphaned clients
|
||||||
|
await cleanupOrphanedClients(userId, transaction);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all user orgs
|
||||||
|
const allUserOrgs = await transaction
|
||||||
|
.select()
|
||||||
|
.from(userOrgs)
|
||||||
|
.where(eq(userOrgs.userId, userId));
|
||||||
|
|
||||||
|
const userOrgIds = allUserOrgs.map((uo) => uo.orgId);
|
||||||
|
|
||||||
|
// For each OLM, ensure there's a client in each org the user is in
|
||||||
|
for (const olm of userOlms) {
|
||||||
|
for (const userOrg of allUserOrgs) {
|
||||||
|
const orgId = userOrg.orgId;
|
||||||
|
|
||||||
|
const [org] = await transaction
|
||||||
|
.select()
|
||||||
|
.from(orgs)
|
||||||
|
.where(eq(orgs.orgId, orgId));
|
||||||
|
|
||||||
|
if (!org) {
|
||||||
|
logger.warn(
|
||||||
|
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): org not found`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!org.subnet) {
|
||||||
|
logger.warn(
|
||||||
|
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): org has no subnet configured`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get admin role for this org (needed for access grants)
|
||||||
|
const [adminRole] = await transaction
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!adminRole) {
|
||||||
|
logger.warn(
|
||||||
|
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no admin role found`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a client already exists for this OLM+user+org combination
|
||||||
|
const [existingClient] = await transaction
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clients.userId, userId),
|
||||||
|
eq(clients.orgId, orgId),
|
||||||
|
eq(clients.olmId, olm.olmId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existingClient) {
|
||||||
|
// Ensure admin role has access to the client
|
||||||
|
const [existingRoleClient] = await transaction
|
||||||
|
.select()
|
||||||
|
.from(roleClients)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(roleClients.roleId, adminRole.roleId),
|
||||||
|
eq(
|
||||||
|
roleClients.clientId,
|
||||||
|
existingClient.clientId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existingRoleClient) {
|
||||||
|
await transaction.insert(roleClients).values({
|
||||||
|
roleId: adminRole.roleId,
|
||||||
|
clientId: existingClient.clientId
|
||||||
|
});
|
||||||
|
logger.debug(
|
||||||
|
`Granted admin role access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure user has access to the client
|
||||||
|
const [existingUserClient] = await transaction
|
||||||
|
.select()
|
||||||
|
.from(userClients)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userClients.userId, userId),
|
||||||
|
eq(
|
||||||
|
userClients.clientId,
|
||||||
|
existingClient.clientId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existingUserClient) {
|
||||||
|
await transaction.insert(userClients).values({
|
||||||
|
userId,
|
||||||
|
clientId: existingClient.clientId
|
||||||
|
});
|
||||||
|
logger.debug(
|
||||||
|
`Granted user access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Client already exists for OLM ${olm.olmId} in org ${orgId} (user ${userId}), skipping creation`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get exit nodes for this org
|
||||||
|
const exitNodesList = await listExitNodes(orgId);
|
||||||
|
|
||||||
|
if (exitNodesList.length === 0) {
|
||||||
|
logger.warn(
|
||||||
|
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no exit nodes found`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const randomExitNode =
|
||||||
|
exitNodesList[
|
||||||
|
Math.floor(Math.random() * exitNodesList.length)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get next available subnet
|
||||||
|
const newSubnet = await getNextAvailableClientSubnet(orgId);
|
||||||
|
if (!newSubnet) {
|
||||||
|
logger.warn(
|
||||||
|
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no available subnet found`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subnet = newSubnet.split("/")[0];
|
||||||
|
const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`;
|
||||||
|
|
||||||
|
// Create the client
|
||||||
|
const [newClient] = await transaction
|
||||||
|
.insert(clients)
|
||||||
|
.values({
|
||||||
|
userId,
|
||||||
|
orgId: userOrg.orgId,
|
||||||
|
exitNodeId: randomExitNode.exitNodeId,
|
||||||
|
name: olm.name || "User Client",
|
||||||
|
subnet: updatedSubnet,
|
||||||
|
olmId: olm.olmId,
|
||||||
|
type: "olm"
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
await rebuildClientAssociationsFromClient(
|
||||||
|
newClient,
|
||||||
|
transaction
|
||||||
|
);
|
||||||
|
|
||||||
|
// Grant admin role access to the client
|
||||||
|
await transaction.insert(roleClients).values({
|
||||||
|
roleId: adminRole.roleId,
|
||||||
|
clientId: newClient.clientId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Grant user access to the client
|
||||||
|
await transaction.insert(userClients).values({
|
||||||
|
userId,
|
||||||
|
clientId: newClient.clientId
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Created client for OLM ${olm.olmId} in org ${orgId} (user ${userId}) with access granted to admin role and user`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up clients in orgs the user is no longer in
|
||||||
|
await cleanupOrphanedClients(userId, transaction, userOrgIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (trx) {
|
||||||
|
// Use provided transaction
|
||||||
|
await execute(trx);
|
||||||
|
} else {
|
||||||
|
// Create new transaction
|
||||||
|
await db.transaction(async (transaction) => {
|
||||||
|
await execute(transaction);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupOrphanedClients(
|
||||||
|
userId: string,
|
||||||
|
trx: Transaction,
|
||||||
|
userOrgIds: string[] = []
|
||||||
|
): Promise<void> {
|
||||||
|
// Find all OLM clients for this user that should be deleted
|
||||||
|
// If userOrgIds is empty, delete all OLM clients (user has no orgs)
|
||||||
|
// If userOrgIds has values, delete clients in orgs they're not in
|
||||||
|
const clientsToDelete = await trx
|
||||||
|
.select({ clientId: clients.clientId })
|
||||||
|
.from(clients)
|
||||||
|
.where(
|
||||||
|
userOrgIds.length > 0
|
||||||
|
? and(
|
||||||
|
eq(clients.userId, userId),
|
||||||
|
notInArray(clients.orgId, userOrgIds)
|
||||||
|
)
|
||||||
|
: and(eq(clients.userId, userId))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (clientsToDelete.length > 0) {
|
||||||
|
const deletedClients = await trx
|
||||||
|
.delete(clients)
|
||||||
|
.where(
|
||||||
|
userOrgIds.length > 0
|
||||||
|
? and(
|
||||||
|
eq(clients.userId, userId),
|
||||||
|
notInArray(clients.orgId, userOrgIds)
|
||||||
|
)
|
||||||
|
: and(eq(clients.userId, userId))
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// Rebuild associations for each deleted client to clean up related data
|
||||||
|
for (const deletedClient of deletedClients) {
|
||||||
|
await rebuildClientAssociationsFromClient(deletedClient, trx);
|
||||||
|
|
||||||
|
if (deletedClient.olmId) {
|
||||||
|
await sendTerminateClient(
|
||||||
|
deletedClient.clientId,
|
||||||
|
deletedClient.olmId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userOrgIds.length === 0) {
|
||||||
|
logger.debug(
|
||||||
|
`Deleted all ${clientsToDelete.length} OLM client(s) for user ${userId} (user has no orgs)`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
`Deleted ${clientsToDelete.length} orphaned OLM client(s) for user ${userId} in orgs they're no longer in`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,7 +85,13 @@ export class Config {
|
|||||||
? "true"
|
? "true"
|
||||||
: "false";
|
: "false";
|
||||||
|
|
||||||
process.env.FLAGS_ENABLE_CLIENTS = parsedConfig.flags?.enable_clients
|
process.env.PRODUCT_UPDATES_NOTIFICATION_ENABLED = parsedConfig.app
|
||||||
|
.notifications.product_updates
|
||||||
|
? "true"
|
||||||
|
: "false";
|
||||||
|
|
||||||
|
process.env.NEW_RELEASES_NOTIFICATION_ENABLED = parsedConfig.app
|
||||||
|
.notifications.new_releases
|
||||||
? "true"
|
? "true"
|
||||||
: "false";
|
: "false";
|
||||||
|
|
||||||
@@ -158,7 +164,7 @@ export class Config {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
"https://api.fossorial.io/api/v1/license/validate",
|
`https://api.fossorial.io/api/v1/license/validate`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -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.12.1";
|
export const APP_VERSION = "1.12.3";
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { defaultRoleAllowedActions } from "@server/routers/role";
|
|||||||
import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/billing";
|
import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/billing";
|
||||||
import { createCustomer } from "#dynamic/lib/billing";
|
import { createCustomer } from "#dynamic/lib/billing";
|
||||||
import { usageService } from "@server/lib/billing/usageService";
|
import { usageService } from "@server/lib/billing/usageService";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
|
||||||
export async function createUserAccountOrg(
|
export async function createUserAccountOrg(
|
||||||
userId: string,
|
userId: string,
|
||||||
@@ -76,6 +77,8 @@ export async function createUserAccountOrg(
|
|||||||
.from(domains)
|
.from(domains)
|
||||||
.where(eq(domains.configManaged, true));
|
.where(eq(domains.configManaged, true));
|
||||||
|
|
||||||
|
const utilitySubnet = config.getRawConfig().orgs.utility_subnet_group;
|
||||||
|
|
||||||
const newOrg = await trx
|
const newOrg = await trx
|
||||||
.insert(orgs)
|
.insert(orgs)
|
||||||
.values({
|
.values({
|
||||||
@@ -83,6 +86,7 @@ export async function createUserAccountOrg(
|
|||||||
name,
|
name,
|
||||||
// subnet
|
// subnet
|
||||||
subnet: "100.90.128.0/24", // TODO: this should not be hardcoded - or can it be the same in all orgs?
|
subnet: "100.90.128.0/24", // TODO: this should not be hardcoded - or can it be the same in all orgs?
|
||||||
|
utilitySubnet: utilitySubnet,
|
||||||
createdAt: new Date().toISOString()
|
createdAt: new Date().toISOString()
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
170
server/lib/ip.ts
170
server/lib/ip.ts
@@ -1,7 +1,15 @@
|
|||||||
import { db } from "@server/db";
|
import {
|
||||||
|
clientSitesAssociationsCache,
|
||||||
|
db,
|
||||||
|
SiteResource,
|
||||||
|
siteResources,
|
||||||
|
Transaction
|
||||||
|
} from "@server/db";
|
||||||
import { clients, orgs, sites } from "@server/db";
|
import { clients, orgs, sites } from "@server/db";
|
||||||
import { and, eq, isNotNull } from "drizzle-orm";
|
import { and, eq, isNotNull } from "drizzle-orm";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
|
import z from "zod";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
interface IPRange {
|
interface IPRange {
|
||||||
start: bigint;
|
start: bigint;
|
||||||
@@ -279,6 +287,56 @@ export async function getNextAvailableClientSubnet(
|
|||||||
return subnet;
|
return subnet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getNextAvailableAliasAddress(
|
||||||
|
orgId: string
|
||||||
|
): Promise<string> {
|
||||||
|
const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId));
|
||||||
|
|
||||||
|
if (!org) {
|
||||||
|
throw new Error(`Organization with ID ${orgId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!org.subnet) {
|
||||||
|
throw new Error(`Organization with ID ${orgId} has no subnet defined`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!org.utilitySubnet) {
|
||||||
|
throw new Error(
|
||||||
|
`Organization with ID ${orgId} has no utility subnet defined`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingAddresses = await db
|
||||||
|
.select({
|
||||||
|
aliasAddress: siteResources.aliasAddress
|
||||||
|
})
|
||||||
|
.from(siteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
isNotNull(siteResources.aliasAddress),
|
||||||
|
eq(siteResources.orgId, orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const addresses = [
|
||||||
|
...existingAddresses.map(
|
||||||
|
(site) => `${site.aliasAddress?.split("/")[0]}/32`
|
||||||
|
),
|
||||||
|
// reserve a /29 for the dns server and other stuff
|
||||||
|
`${org.utilitySubnet.split("/")[0]}/29`
|
||||||
|
].filter((address) => address !== null) as string[];
|
||||||
|
|
||||||
|
let subnet = findNextAvailableCidr(addresses, 32, org.utilitySubnet);
|
||||||
|
if (!subnet) {
|
||||||
|
throw new Error("No available subnets remaining in space");
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the cidr
|
||||||
|
subnet = subnet.split("/")[0];
|
||||||
|
|
||||||
|
return subnet;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getNextAvailableOrgSubnet(): Promise<string> {
|
export async function getNextAvailableOrgSubnet(): Promise<string> {
|
||||||
const existingAddresses = await db
|
const existingAddresses = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -300,3 +358,113 @@ export async function getNextAvailableOrgSubnet(): Promise<string> {
|
|||||||
|
|
||||||
return subnet;
|
return subnet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateRemoteSubnets(allSiteResources: SiteResource[]): string[] {
|
||||||
|
const remoteSubnets = allSiteResources
|
||||||
|
.filter((sr) => {
|
||||||
|
if (sr.mode === "cidr") return true;
|
||||||
|
if (sr.mode === "host") {
|
||||||
|
// check if its a valid IP using zod
|
||||||
|
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
|
||||||
|
const parseResult = ipSchema.safeParse(sr.destination);
|
||||||
|
return parseResult.success;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.map((sr) => {
|
||||||
|
if (sr.mode === "cidr") return sr.destination;
|
||||||
|
if (sr.mode === "host") {
|
||||||
|
return `${sr.destination}/32`;
|
||||||
|
}
|
||||||
|
return ""; // This should never be reached due to filtering, but satisfies TypeScript
|
||||||
|
})
|
||||||
|
.filter((subnet) => subnet !== ""); // Remove empty strings just to be safe
|
||||||
|
// remove duplicates
|
||||||
|
return Array.from(new Set(remoteSubnets));
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Alias = { alias: string | null; aliasAddress: string | null };
|
||||||
|
|
||||||
|
export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
|
||||||
|
let aliasConfigs = allSiteResources
|
||||||
|
.filter((sr) => sr.alias && sr.aliasAddress && sr.mode == "host")
|
||||||
|
.map((sr) => ({
|
||||||
|
alias: sr.alias,
|
||||||
|
aliasAddress: sr.aliasAddress
|
||||||
|
}));
|
||||||
|
return aliasConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SubnetProxyTarget = {
|
||||||
|
sourcePrefix: string; // must be a cidr
|
||||||
|
destPrefix: string; // must be a cidr
|
||||||
|
rewriteTo?: string; // must be a cidr
|
||||||
|
portRange?: {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function generateSubnetProxyTargets(
|
||||||
|
siteResource: SiteResource,
|
||||||
|
clients: {
|
||||||
|
clientId: number;
|
||||||
|
pubKey: string | null;
|
||||||
|
subnet: string | null;
|
||||||
|
}[]
|
||||||
|
): SubnetProxyTarget[] {
|
||||||
|
const targets: SubnetProxyTarget[] = [];
|
||||||
|
|
||||||
|
if (clients.length === 0) {
|
||||||
|
logger.debug(
|
||||||
|
`No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.`
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const clientSite of clients) {
|
||||||
|
if (!clientSite.subnet) {
|
||||||
|
logger.debug(
|
||||||
|
`Client ${clientSite.clientId} has no subnet, skipping for site resource ${siteResource.siteResourceId}.`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`;
|
||||||
|
|
||||||
|
if (siteResource.mode == "host") {
|
||||||
|
let destination = siteResource.destination;
|
||||||
|
// check if this is a valid ip
|
||||||
|
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
|
||||||
|
if (ipSchema.safeParse(destination).success) {
|
||||||
|
destination = `${destination}/32`;
|
||||||
|
|
||||||
|
targets.push({
|
||||||
|
sourcePrefix: clientPrefix,
|
||||||
|
destPrefix: destination
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (siteResource.alias && siteResource.aliasAddress) {
|
||||||
|
// also push a match for the alias address
|
||||||
|
targets.push({
|
||||||
|
sourcePrefix: clientPrefix,
|
||||||
|
destPrefix: `${siteResource.aliasAddress}/32`,
|
||||||
|
rewriteTo: destination
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (siteResource.mode == "cidr") {
|
||||||
|
targets.push({
|
||||||
|
sourcePrefix: clientPrefix,
|
||||||
|
destPrefix: siteResource.destination
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// print a nice representation of the targets
|
||||||
|
// logger.debug(
|
||||||
|
// `Generated subnet proxy targets for: ${JSON.stringify(targets, null, 2)}`
|
||||||
|
// );
|
||||||
|
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
|||||||
111
server/lib/lock.ts
Normal file
111
server/lib/lock.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
export class LockManager {
|
||||||
|
/**
|
||||||
|
* Acquire a distributed lock using Redis SET with NX and PX options
|
||||||
|
* @param lockKey - Unique identifier for the lock
|
||||||
|
* @param ttlMs - Time to live in milliseconds
|
||||||
|
* @returns Promise<boolean> - true if lock acquired, false otherwise
|
||||||
|
*/
|
||||||
|
async acquireLock(
|
||||||
|
lockKey: string,
|
||||||
|
ttlMs: number = 30000
|
||||||
|
): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release a lock using Lua script to ensure atomicity
|
||||||
|
* @param lockKey - Unique identifier for the lock
|
||||||
|
*/
|
||||||
|
async releaseLock(lockKey: string): Promise<void> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force release a lock regardless of owner (use with caution)
|
||||||
|
* @param lockKey - Unique identifier for the lock
|
||||||
|
*/
|
||||||
|
async forceReleaseLock(lockKey: string): Promise<void> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a lock exists and get its info
|
||||||
|
* @param lockKey - Unique identifier for the lock
|
||||||
|
* @returns Promise<{exists: boolean, ownedByMe: boolean, ttl: number}>
|
||||||
|
*/
|
||||||
|
async getLockInfo(lockKey: string): Promise<{
|
||||||
|
exists: boolean;
|
||||||
|
ownedByMe: boolean;
|
||||||
|
ttl: number;
|
||||||
|
owner?: string;
|
||||||
|
}> {
|
||||||
|
return { exists: true, ownedByMe: true, ttl: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend the TTL of an existing lock owned by this worker
|
||||||
|
* @param lockKey - Unique identifier for the lock
|
||||||
|
* @param ttlMs - New TTL in milliseconds
|
||||||
|
* @returns Promise<boolean> - true if extended successfully
|
||||||
|
*/
|
||||||
|
async extendLock(lockKey: string, ttlMs: number): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to acquire lock with retries and exponential backoff
|
||||||
|
* @param lockKey - Unique identifier for the lock
|
||||||
|
* @param ttlMs - Time to live in milliseconds
|
||||||
|
* @param maxRetries - Maximum number of retry attempts
|
||||||
|
* @param baseDelayMs - Base delay between retries in milliseconds
|
||||||
|
* @returns Promise<boolean> - true if lock acquired
|
||||||
|
*/
|
||||||
|
async acquireLockWithRetry(
|
||||||
|
lockKey: string,
|
||||||
|
ttlMs: number = 30000,
|
||||||
|
maxRetries: number = 5,
|
||||||
|
baseDelayMs: number = 100
|
||||||
|
): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a function while holding a lock
|
||||||
|
* @param lockKey - Unique identifier for the lock
|
||||||
|
* @param fn - Function to execute while holding the lock
|
||||||
|
* @param ttlMs - Lock TTL in milliseconds
|
||||||
|
* @returns Promise<T> - Result of the executed function
|
||||||
|
*/
|
||||||
|
async withLock<T>(
|
||||||
|
lockKey: string,
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
ttlMs: number = 30000
|
||||||
|
): Promise<T> {
|
||||||
|
const acquired = await this.acquireLock(lockKey, ttlMs);
|
||||||
|
|
||||||
|
if (!acquired) {
|
||||||
|
throw new Error(`Failed to acquire lock: ${lockKey}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} finally {
|
||||||
|
await this.releaseLock(lockKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up expired locks - Redis handles this automatically, but this method
|
||||||
|
* can be used to get statistics about locks
|
||||||
|
* @returns Promise<{activeLocksCount: number, locksOwnedByMe: number}>
|
||||||
|
*/
|
||||||
|
async getLockStatistics(): Promise<{
|
||||||
|
activeLocksCount: number;
|
||||||
|
locksOwnedByMe: number;
|
||||||
|
}> {
|
||||||
|
return { activeLocksCount: 0, locksOwnedByMe: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the Redis connection
|
||||||
|
*/
|
||||||
|
async disconnect(): Promise<void> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const lockManager = new LockManager();
|
||||||
@@ -14,10 +14,8 @@ export const configSchema = z
|
|||||||
.object({
|
.object({
|
||||||
app: z
|
app: z
|
||||||
.object({
|
.object({
|
||||||
dashboard_url: z
|
dashboard_url: z.url()
|
||||||
.string()
|
.pipe(z.url())
|
||||||
.url()
|
|
||||||
.pipe(z.string().url())
|
|
||||||
.transform((url) => url.toLowerCase())
|
.transform((url) => url.toLowerCase())
|
||||||
.optional(),
|
.optional(),
|
||||||
log_level: z
|
log_level: z
|
||||||
@@ -31,7 +29,14 @@ export const configSchema = z
|
|||||||
anonymous_usage: z.boolean().optional().default(true)
|
anonymous_usage: z.boolean().optional().default(true)
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({})
|
.prefault({}),
|
||||||
|
notifications: z
|
||||||
|
.object({
|
||||||
|
product_updates: z.boolean().optional().default(true),
|
||||||
|
new_releases: z.boolean().optional().default(true)
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.prefault({})
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({
|
.default({
|
||||||
@@ -40,6 +45,10 @@ export const configSchema = z
|
|||||||
log_failed_attempts: false,
|
log_failed_attempts: false,
|
||||||
telemetry: {
|
telemetry: {
|
||||||
anonymous_usage: true
|
anonymous_usage: true
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
product_updates: true,
|
||||||
|
new_releases: true
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
domains: z
|
domains: z
|
||||||
@@ -96,7 +105,7 @@ export const configSchema = z
|
|||||||
token: z.string().optional().default("P-Access-Token")
|
token: z.string().optional().default("P-Access-Token")
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({}),
|
.prefault({}),
|
||||||
resource_session_request_param: z
|
resource_session_request_param: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -121,7 +130,7 @@ export const configSchema = z
|
|||||||
credentials: z.boolean().optional()
|
credentials: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
trust_proxy: z.number().int().gte(0).optional().default(1),
|
trust_proxy: z.int().gte(0).optional().default(1),
|
||||||
secret: z.string().pipe(z.string().min(8)).optional(),
|
secret: z.string().pipe(z.string().min(8)).optional(),
|
||||||
maxmind_db_path: z.string().optional()
|
maxmind_db_path: z.string().optional()
|
||||||
})
|
})
|
||||||
@@ -178,7 +187,7 @@ export const configSchema = z
|
|||||||
.default(5000)
|
.default(5000)
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({})
|
.prefault({})
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
traefik: z
|
traefik: z
|
||||||
@@ -205,10 +214,13 @@ export const configSchema = z
|
|||||||
.default(["newt", "wireguard", "local"]),
|
.default(["newt", "wireguard", "local"]),
|
||||||
allow_raw_resources: z.boolean().optional().default(true),
|
allow_raw_resources: z.boolean().optional().default(true),
|
||||||
file_mode: z.boolean().optional().default(false),
|
file_mode: z.boolean().optional().default(false),
|
||||||
pp_transport_prefix: z.string().optional().default("pp-transport-v")
|
pp_transport_prefix: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.default("pp-transport-v")
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({}),
|
.prefault({}),
|
||||||
gerbil: z
|
gerbil: z
|
||||||
.object({
|
.object({
|
||||||
exit_node_name: z.string().optional(),
|
exit_node_name: z.string().optional(),
|
||||||
@@ -217,6 +229,11 @@ export const configSchema = z
|
|||||||
.default(51820)
|
.default(51820)
|
||||||
.transform(stoi)
|
.transform(stoi)
|
||||||
.pipe(portSchema),
|
.pipe(portSchema),
|
||||||
|
clients_start_port: portSchema
|
||||||
|
.optional()
|
||||||
|
.default(21820)
|
||||||
|
.transform(stoi)
|
||||||
|
.pipe(portSchema),
|
||||||
base_endpoint: z
|
base_endpoint: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -233,16 +250,18 @@ export const configSchema = z
|
|||||||
.default(30)
|
.default(30)
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({}),
|
.prefault({}),
|
||||||
orgs: z
|
orgs: z
|
||||||
.object({
|
.object({
|
||||||
block_size: z.number().positive().gt(0).optional().default(24),
|
block_size: z.number().positive().gt(0).optional().default(24),
|
||||||
subnet_group: z.string().optional().default("100.90.128.0/24")
|
subnet_group: z.string().optional().default("100.90.128.0/24"),
|
||||||
|
utility_subnet_group: z.string().optional().default("100.96.128.0/24") //just hardcode this for now as well
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({
|
.default({
|
||||||
block_size: 24,
|
block_size: 24,
|
||||||
subnet_group: "100.90.128.0/24"
|
subnet_group: "100.90.128.0/24",
|
||||||
|
utility_subnet_group: "100.96.128.0/24"
|
||||||
}),
|
}),
|
||||||
rate_limits: z
|
rate_limits: z
|
||||||
.object({
|
.object({
|
||||||
@@ -262,7 +281,7 @@ export const configSchema = z
|
|||||||
.default(500)
|
.default(500)
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({}),
|
.prefault({}),
|
||||||
auth: z
|
auth: z
|
||||||
.object({
|
.object({
|
||||||
window_minutes: z
|
window_minutes: z
|
||||||
@@ -279,10 +298,10 @@ export const configSchema = z
|
|||||||
.default(500)
|
.default(500)
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({})
|
.prefault({})
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({}),
|
.prefault({}),
|
||||||
email: z
|
email: z
|
||||||
.object({
|
.object({
|
||||||
smtp_host: z.string().optional(),
|
smtp_host: z.string().optional(),
|
||||||
@@ -294,7 +313,7 @@ export const configSchema = z
|
|||||||
.transform(getEnvOrYaml("EMAIL_SMTP_PASS")),
|
.transform(getEnvOrYaml("EMAIL_SMTP_PASS")),
|
||||||
smtp_secure: z.boolean().optional(),
|
smtp_secure: z.boolean().optional(),
|
||||||
smtp_tls_reject_unauthorized: z.boolean().optional(),
|
smtp_tls_reject_unauthorized: z.boolean().optional(),
|
||||||
no_reply: z.string().email().optional()
|
no_reply: z.email().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
flags: z
|
flags: z
|
||||||
@@ -306,8 +325,7 @@ export const configSchema = z
|
|||||||
enable_integration_api: z.boolean().optional(),
|
enable_integration_api: z.boolean().optional(),
|
||||||
disable_local_sites: z.boolean().optional(),
|
disable_local_sites: z.boolean().optional(),
|
||||||
disable_basic_wireguard_sites: z.boolean().optional(),
|
disable_basic_wireguard_sites: z.boolean().optional(),
|
||||||
disable_config_managed_domains: z.boolean().optional(),
|
disable_config_managed_domains: z.boolean().optional()
|
||||||
enable_clients: z.boolean().optional().default(true)
|
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
dns: z
|
dns: z
|
||||||
@@ -315,11 +333,18 @@ export const configSchema = z
|
|||||||
nameservers: z
|
nameservers: z
|
||||||
.array(z.string().optional().optional())
|
.array(z.string().optional().optional())
|
||||||
.optional()
|
.optional()
|
||||||
.default(["ns1.pangolin.net", "ns2.pangolin.net", "ns3.pangolin.net"]),
|
.default([
|
||||||
cname_extension: z.string().optional().default("cname.pangolin.net")
|
"ns1.pangolin.net",
|
||||||
|
"ns2.pangolin.net",
|
||||||
|
"ns3.pangolin.net"
|
||||||
|
]),
|
||||||
|
cname_extension: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.default("cname.pangolin.net")
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({})
|
.prefault({})
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
@@ -334,7 +359,7 @@ export const configSchema = z
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "At least one domain must be defined"
|
error: "At least one domain must be defined"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
@@ -349,7 +374,7 @@ export const configSchema = z
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Server secret must be defined"
|
error: "Server secret must be defined"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
@@ -361,7 +386,7 @@ export const configSchema = z
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Dashboard URL must be defined"
|
error: "Dashboard URL must be defined"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
1243
server/lib/rebuildClientAssociations.ts
Normal file
1243
server/lib/rebuildClientAssociations.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ import { getHostMeta } from "./hostMeta";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { apiKeys, db, roles } from "@server/db";
|
import { apiKeys, db, roles } from "@server/db";
|
||||||
import { sites, users, orgs, resources, clients, idp } from "@server/db";
|
import { sites, users, orgs, resources, clients, idp } from "@server/db";
|
||||||
import { eq, count, notInArray } from "drizzle-orm";
|
import { eq, count, notInArray, and } from "drizzle-orm";
|
||||||
import { APP_VERSION } from "./consts";
|
import { APP_VERSION } from "./consts";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { UserType } from "@server/types/UserTypes";
|
import { UserType } from "@server/types/UserTypes";
|
||||||
@@ -113,7 +113,12 @@ class TelemetryClient {
|
|||||||
const [customRoles] = await db
|
const [customRoles] = await db
|
||||||
.select({ count: count() })
|
.select({ count: count() })
|
||||||
.from(roles)
|
.from(roles)
|
||||||
.where(notInArray(roles.name, ["Admin", "Member"]));
|
.where(
|
||||||
|
and(
|
||||||
|
eq(roles.isAdmin, false),
|
||||||
|
notInArray(roles.name, ["Member"])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const adminUsers = await db
|
const adminUsers = await db
|
||||||
.select({ email: users.email })
|
.select({ email: users.email })
|
||||||
@@ -188,7 +193,7 @@ class TelemetryClient {
|
|||||||
license_tier: licenseStatus.tier || "unknown"
|
license_tier: licenseStatus.tier || "unknown"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
logger.debug("Sending enterprise startup telemtry payload:", {
|
logger.debug("Sending enterprise startup telemetry payload:", {
|
||||||
payload
|
payload
|
||||||
});
|
});
|
||||||
// this.client.capture(payload);
|
// this.client.capture(payload);
|
||||||
|
|||||||
@@ -345,9 +345,9 @@ export async function getTraefikConfig(
|
|||||||
routerMiddlewares.push(rewriteMiddlewareName);
|
routerMiddlewares.push(rewriteMiddlewareName);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(
|
// logger.debug(
|
||||||
`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`
|
// `Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`
|
||||||
);
|
// );
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`
|
`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
import ipaddr from "ipaddr.js";
|
||||||
|
|
||||||
export function isValidCIDR(cidr: string): boolean {
|
export function isValidCIDR(cidr: string): boolean {
|
||||||
return z.string().cidr().safeParse(cidr).success;
|
return z.cidrv4().safeParse(cidr).success || z.cidrv6().safeParse(cidr).success;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidIP(ip: string): boolean {
|
export function isValidIP(ip: string): boolean {
|
||||||
return z.string().ip().safeParse(ip).success;
|
return z.ipv4().safeParse(ip).success || z.ipv6().safeParse(ip).success;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidUrlGlobPattern(pattern: string): boolean {
|
export function isValidUrlGlobPattern(pattern: string): boolean {
|
||||||
@@ -68,11 +69,11 @@ export function isUrlValid(url: string | undefined) {
|
|||||||
if (!url) return true; // the link is optional in the schema so if it's empty it's valid
|
if (!url) return true; // the link is optional in the schema so if it's empty it's valid
|
||||||
var pattern = new RegExp(
|
var pattern = new RegExp(
|
||||||
"^(https?:\\/\\/)?" + // protocol
|
"^(https?:\\/\\/)?" + // protocol
|
||||||
"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name
|
"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name
|
||||||
"((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address
|
"((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address
|
||||||
"(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path
|
"(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path
|
||||||
"(\\?[;&a-z\\d%_.~+=-]*)?" + // query string
|
"(\\?[;&a-z\\d%_.~+=-]*)?" + // query string
|
||||||
"(\\#[-a-z\\d_]*)?$",
|
"(\\#[-a-z\\d_]*)?$",
|
||||||
"i"
|
"i"
|
||||||
);
|
);
|
||||||
return !!pattern.test(url);
|
return !!pattern.test(url);
|
||||||
@@ -83,12 +84,15 @@ export function isTargetValid(value: string | undefined) {
|
|||||||
|
|
||||||
const DOMAIN_REGEX =
|
const DOMAIN_REGEX =
|
||||||
/^[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9_])?(?:\.[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9_])?)*$/;
|
/^[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9_])?(?:\.[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9_])?)*$/;
|
||||||
const IPV4_REGEX =
|
// const IPV4_REGEX =
|
||||||
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
// /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||||
const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i;
|
// const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i;
|
||||||
|
|
||||||
if (IPV4_REGEX.test(value) || IPV6_REGEX.test(value)) {
|
try {
|
||||||
return true;
|
const addr = ipaddr.parse(value);
|
||||||
|
return addr.kind() === "ipv4" || addr.kind() === "ipv6";
|
||||||
|
} catch {
|
||||||
|
// fall through to domain regex check
|
||||||
}
|
}
|
||||||
|
|
||||||
return DOMAIN_REGEX.test(value);
|
return DOMAIN_REGEX.test(value);
|
||||||
@@ -169,10 +173,10 @@ export function isSecondLevelDomain(domain: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const trimmedDomain = domain.trim().toLowerCase();
|
const trimmedDomain = domain.trim().toLowerCase();
|
||||||
|
|
||||||
// Split into parts
|
// Split into parts
|
||||||
const parts = trimmedDomain.split('.');
|
const parts = trimmedDomain.split('.');
|
||||||
|
|
||||||
// Should have exactly 2 parts for a second-level domain (e.g., "example.com")
|
// Should have exactly 2 parts for a second-level domain (e.g., "example.com")
|
||||||
if (parts.length !== 2) {
|
if (parts.length !== 2) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export * from "./verifyRoleAccess";
|
|||||||
export * from "./verifyUserAccess";
|
export * from "./verifyUserAccess";
|
||||||
export * from "./verifyAdmin";
|
export * from "./verifyAdmin";
|
||||||
export * from "./verifySetResourceUsers";
|
export * from "./verifySetResourceUsers";
|
||||||
|
export * from "./verifySetResourceClients";
|
||||||
export * from "./verifyUserInRole";
|
export * from "./verifyUserInRole";
|
||||||
export * from "./verifyAccessTokenAccess";
|
export * from "./verifyAccessTokenAccess";
|
||||||
export * from "./requestTimeout";
|
export * from "./requestTimeout";
|
||||||
@@ -24,7 +25,7 @@ export * from "./integration";
|
|||||||
export * from "./verifyUserHasAction";
|
export * from "./verifyUserHasAction";
|
||||||
export * from "./verifyApiKeyAccess";
|
export * from "./verifyApiKeyAccess";
|
||||||
export * from "./verifyDomainAccess";
|
export * from "./verifyDomainAccess";
|
||||||
export * from "./verifyClientsEnabled";
|
|
||||||
export * from "./verifyUserIsOrgOwner";
|
export * from "./verifyUserIsOrgOwner";
|
||||||
export * from "./verifySiteResourceAccess";
|
export * from "./verifySiteResourceAccess";
|
||||||
export * from "./logActionAudit";
|
export * from "./logActionAudit";
|
||||||
|
export * from "./verifyOlmAccess";
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export * from "./verifyApiKeyTargetAccess";
|
|||||||
export * from "./verifyApiKeyRoleAccess";
|
export * from "./verifyApiKeyRoleAccess";
|
||||||
export * from "./verifyApiKeyUserAccess";
|
export * from "./verifyApiKeyUserAccess";
|
||||||
export * from "./verifyApiKeySetResourceUsers";
|
export * from "./verifyApiKeySetResourceUsers";
|
||||||
|
export * from "./verifyApiKeySetResourceClients";
|
||||||
export * from "./verifyAccessTokenAccess";
|
export * from "./verifyAccessTokenAccess";
|
||||||
export * from "./verifyApiKeyIsRoot";
|
export * from "./verifyApiKeyIsRoot";
|
||||||
export * from "./verifyApiKeyApiKeyAccess";
|
export * from "./verifyApiKeyApiKeyAccess";
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { clients } from "@server/db";
|
||||||
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
|
||||||
|
export async function verifyApiKeySetResourceClients(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const apiKey = req.apiKey;
|
||||||
|
const singleClientId = req.params.clientId || req.body.clientId || req.query.clientId;
|
||||||
|
const { clientIds } = req.body;
|
||||||
|
const allClientIds = clientIds || (singleClientId ? [parseInt(singleClientId as string)] : []);
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
// Root keys can access any client in any org
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Key does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allClientIds.length === 0) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const orgId = req.apiKeyOrg.orgId;
|
||||||
|
const clientsData = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(clients.clientId, allClientIds),
|
||||||
|
eq(clients.orgId, orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (clientsData.length !== allClientIds.length) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Key does not have access to one or more specified clients"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error checking if key has access to the specified clients"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -11,7 +11,9 @@ export async function verifyApiKeySetResourceUsers(
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
) {
|
) {
|
||||||
const apiKey = req.apiKey;
|
const apiKey = req.apiKey;
|
||||||
const userIds = req.body.userIds;
|
const singleUserId = req.params.userId || req.body.userId || req.query.userId;
|
||||||
|
const { userIds } = req.body;
|
||||||
|
const allUserIds = userIds || (singleUserId ? [singleUserId] : []);
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return next(
|
return next(
|
||||||
@@ -33,11 +35,7 @@ export async function verifyApiKeySetResourceUsers(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userIds) {
|
if (allUserIds.length === 0) {
|
||||||
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userIds.length === 0) {
|
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,12 +46,12 @@ export async function verifyApiKeySetResourceUsers(
|
|||||||
.from(userOrgs)
|
.from(userOrgs)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
inArray(userOrgs.userId, userIds),
|
inArray(userOrgs.userId, allUserIds),
|
||||||
eq(userOrgs.orgId, orgId)
|
eq(userOrgs.orgId, orgId)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userOrgsData.length !== userIds.length) {
|
if (userOrgsData.length !== allUserIds.length) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ export async function verifyApiKeySiteResourceAccess(
|
|||||||
try {
|
try {
|
||||||
const apiKey = req.apiKey;
|
const apiKey = req.apiKey;
|
||||||
const siteResourceId = parseInt(req.params.siteResourceId);
|
const siteResourceId = parseInt(req.params.siteResourceId);
|
||||||
const siteId = parseInt(req.params.siteId);
|
|
||||||
const orgId = req.params.orgId;
|
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return next(
|
return next(
|
||||||
@@ -22,11 +20,11 @@ export async function verifyApiKeySiteResourceAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!siteResourceId || !siteId || !orgId) {
|
if (!siteResourceId) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
"Missing required parameters"
|
"Missing siteResourceId parameter"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -41,9 +39,7 @@ export async function verifyApiKeySiteResourceAccess(
|
|||||||
.select()
|
.select()
|
||||||
.from(siteResources)
|
.from(siteResources)
|
||||||
.where(and(
|
.where(and(
|
||||||
eq(siteResources.siteResourceId, siteResourceId),
|
eq(siteResources.siteResourceId, siteResourceId)
|
||||||
eq(siteResources.siteId, siteId),
|
|
||||||
eq(siteResources.orgId, orgId)
|
|
||||||
))
|
))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
@@ -64,11 +60,11 @@ export async function verifyApiKeySiteResourceAccess(
|
|||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||||
eq(apiKeyOrg.orgId, orgId)
|
eq(apiKeyOrg.orgId, siteResource.orgId)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (apiKeyOrgRes.length === 0) {
|
if (apiKeyOrgRes.length === 0) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -77,12 +73,11 @@ export async function verifyApiKeySiteResourceAccess(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.apiKeyOrg = apiKeyOrgRes[0];
|
req.apiKeyOrg = apiKeyOrgRes[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach the siteResource to the request for use in the next middleware/route
|
// Attach the siteResource to the request for use in the next middleware/route
|
||||||
// @ts-ignore - Extending Request type
|
|
||||||
req.siteResource = siteResource;
|
req.siteResource = siteResource;
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { and, eq } from "drizzle-orm";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { canUserAccessResource } from "@server/auth/canUserAccessResource";
|
import { canUserAccessResource } from "@server/auth/canUserAccessResource";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
|
||||||
export async function verifyAccessTokenAccess(
|
export async function verifyAccessTokenAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -96,6 +97,24 @@ export async function verifyAccessTokenAccess(
|
|||||||
req.userOrgId = resource[0].orgId!;
|
req.userOrgId = resource[0].orgId!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: req.userOrg.orgId,
|
||||||
|
userId,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Failed organization access policy check: " +
|
||||||
|
(policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resourceAllowed = await canUserAccessResource({
|
const resourceAllowed = await canUserAccessResource({
|
||||||
userId,
|
userId,
|
||||||
resourceId,
|
resourceId,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { roles, userOrgs } from "@server/db";
|
|||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
|
||||||
export async function verifyAdmin(
|
export async function verifyAdmin(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -43,6 +44,24 @@ export async function verifyAdmin(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: req.userOrg.orgId,
|
||||||
|
userId,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Failed organization access policy check: " +
|
||||||
|
(policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const userRole = await db
|
const userRole = await db
|
||||||
.select()
|
.select()
|
||||||
.from(roles)
|
.from(roles)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { userOrgs, apiKeys, apiKeyOrg } from "@server/db";
|
|||||||
import { and, eq, or } from "drizzle-orm";
|
import { and, eq, or } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
|
||||||
export async function verifyApiKeyAccess(
|
export async function verifyApiKeyAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -84,6 +85,24 @@ export async function verifyApiKeyAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: req.userOrg.orgId,
|
||||||
|
userId,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Failed organization access policy check: " +
|
||||||
|
(policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const userOrgRoleId = req.userOrg.roleId;
|
const userOrgRoleId = req.userOrg.roleId;
|
||||||
req.userOrgRoleId = userOrgRoleId;
|
req.userOrgRoleId = userOrgRoleId;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { userOrgs, clients, roleClients, userClients } from "@server/db";
|
|||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
|
||||||
export async function verifyClientAccess(
|
export async function verifyClientAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -75,6 +76,24 @@ export async function verifyClientAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: req.userOrg.orgId,
|
||||||
|
userId,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Failed organization access policy check: " +
|
||||||
|
(policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const userOrgRoleId = req.userOrg.roleId;
|
const userOrgRoleId = req.userOrg.roleId;
|
||||||
req.userOrgRoleId = userOrgRoleId;
|
req.userOrgRoleId = userOrgRoleId;
|
||||||
req.userOrgId = client.orgId;
|
req.userOrgId = client.orgId;
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import config from "@server/lib/config";
|
|
||||||
|
|
||||||
export async function verifyClientsEnabled(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
if (!config.getRawConfig().flags?.enable_clients) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_IMPLEMENTED,
|
|
||||||
"Clients are not enabled on this server."
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Failed to check if clients are enabled"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ import { userOrgs, apiKeyOrg } from "@server/db";
|
|||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
|
||||||
export async function verifyDomainAccess(
|
export async function verifyDomainAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -78,6 +79,24 @@ export async function verifyDomainAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: req.userOrg.orgId,
|
||||||
|
userId,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Failed organization access policy check: " +
|
||||||
|
(policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const userOrgRoleId = req.userOrg.roleId;
|
const userOrgRoleId = req.userOrg.roleId;
|
||||||
req.userOrgRoleId = userOrgRoleId;
|
req.userOrgRoleId = userOrgRoleId;
|
||||||
|
|
||||||
|
|||||||
45
server/middlewares/verifyOlmAccess.ts
Normal file
45
server/middlewares/verifyOlmAccess.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { db, olms } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
export async function verifyOlmAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const userId = req.user!.userId;
|
||||||
|
const olmId = req.params.olmId || req.body.olmId || req.query.olmId;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [existingOlm] = await db
|
||||||
|
.select()
|
||||||
|
.from(olms)
|
||||||
|
.where(and(eq(olms.olmId, olmId), eq(olms.userId, userId)));
|
||||||
|
|
||||||
|
if (!existingOlm) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"User does not have access to this olm"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error checking if user has access to this user"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,22 +47,22 @@ export async function verifyOrgAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const policyCheck = await checkOrgAccessPolicy({
|
if (req.orgPolicyAllowed === undefined) {
|
||||||
orgId,
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
userId,
|
orgId,
|
||||||
session: req.session
|
userId,
|
||||||
});
|
session: req.session
|
||||||
|
});
|
||||||
logger.debug("Org check policy result", { policyCheck });
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
if (!policyCheck.allowed || policyCheck.error) {
|
return next(
|
||||||
return next(
|
createHttpError(
|
||||||
createHttpError(
|
HttpCode.FORBIDDEN,
|
||||||
HttpCode.FORBIDDEN,
|
"Failed organization access policy check: " +
|
||||||
"Failed organization access policy check: " +
|
(policyCheck.error || "Unknown error")
|
||||||
(policyCheck.error || "Unknown error")
|
)
|
||||||
)
|
);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// User has access, attach the user's role to the request for potential future use
|
// User has access, attach the user's role to the request for potential future use
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import {
|
import { resources, userOrgs, userResources, roleResources } from "@server/db";
|
||||||
resources,
|
|
||||||
userOrgs,
|
|
||||||
userResources,
|
|
||||||
roleResources,
|
|
||||||
} from "@server/db";
|
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
|
||||||
export async function verifyResourceAccess(
|
export async function verifyResourceAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -73,6 +69,24 @@ export async function verifyResourceAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: req.userOrg.orgId,
|
||||||
|
userId,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Failed organization access policy check: " +
|
||||||
|
(policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const userOrgRoleId = req.userOrg.roleId;
|
const userOrgRoleId = req.userOrg.roleId;
|
||||||
req.userOrgRoleId = userOrgRoleId;
|
req.userOrgRoleId = userOrgRoleId;
|
||||||
req.userOrgId = resource[0].orgId;
|
req.userOrgId = resource[0].orgId;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { and, eq, inArray } from "drizzle-orm";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
|
||||||
export async function verifyRoleAccess(
|
export async function verifyRoleAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -105,6 +106,33 @@ export async function verifyRoleAccess(
|
|||||||
req.userOrgRoleId = userOrg[0].roleId;
|
req.userOrgRoleId = userOrg[0].roleId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!req.userOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"User does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: req.userOrg.orgId,
|
||||||
|
userId,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Failed organization access policy check: " +
|
||||||
|
(policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error verifying role access:", error);
|
logger.error("Error verifying role access:", error);
|
||||||
@@ -116,4 +144,3 @@ export async function verifyRoleAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import { NextFunction, Response } from "express";
|
import { NextFunction, Response } from "express";
|
||||||
import ErrorResponse from "@server/types/ErrorResponse";
|
import ErrorResponse from "@server/types/ErrorResponse";
|
||||||
import { db } from "@server/db";
|
|
||||||
import { users } from "@server/db";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import { verifySession } from "@server/auth/sessions/verifySession";
|
import { verifySession } from "@server/auth/sessions/verifySession";
|
||||||
import { unauthorized } from "@server/auth/unauthorizedResponse";
|
import { unauthorized } from "@server/auth/unauthorizedResponse";
|
||||||
|
|
||||||
@@ -13,24 +8,15 @@ export const verifySessionMiddleware = async (
|
|||||||
res: Response<ErrorResponse>,
|
res: Response<ErrorResponse>,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
) => {
|
) => {
|
||||||
const { session, user } = await verifySession(req);
|
const { forceLogin } = req.query;
|
||||||
|
|
||||||
|
const { session, user } = await verifySession(req, forceLogin === "true");
|
||||||
if (!session || !user) {
|
if (!session || !user) {
|
||||||
return next(unauthorized());
|
return next(unauthorized());
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingUser = await db
|
req.user = user;
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.userId, user.userId));
|
|
||||||
|
|
||||||
if (!existingUser || !existingUser[0]) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "User does not exist")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.user = existingUser[0];
|
|
||||||
req.session = session;
|
req.session = session;
|
||||||
|
|
||||||
next();
|
return next();
|
||||||
};
|
};
|
||||||
|
|||||||
90
server/middlewares/verifySetResourceClients.ts
Normal file
90
server/middlewares/verifySetResourceClients.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { clients } from "@server/db";
|
||||||
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
|
||||||
|
export async function verifySetResourceClients(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const userId = req.user!.userId;
|
||||||
|
const singleClientId =
|
||||||
|
req.params.clientId || req.body.clientId || req.query.clientId;
|
||||||
|
const { clientIds } = req.body;
|
||||||
|
const allClientIds =
|
||||||
|
clientIds ||
|
||||||
|
(singleClientId ? [parseInt(singleClientId as string)] : []);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.userOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"User does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: req.userOrg.orgId,
|
||||||
|
userId,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Failed organization access policy check: " +
|
||||||
|
(policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allClientIds.length === 0) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const orgId = req.userOrg.orgId;
|
||||||
|
// get all clients for the clientIds
|
||||||
|
const clientsData = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(clients.clientId, allClientIds),
|
||||||
|
eq(clients.orgId, orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (clientsData.length !== allClientIds.length) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"User does not have access to one or more specified clients"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error checking if user has access to the specified clients"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { userOrgs } from "@server/db";
|
|||||||
import { and, eq, inArray, or } from "drizzle-orm";
|
import { and, eq, inArray, or } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
|
||||||
export async function verifySetResourceUsers(
|
export async function verifySetResourceUsers(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -28,6 +29,24 @@ export async function verifySetResourceUsers(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: req.userOrg.orgId,
|
||||||
|
userId,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Failed organization access policy check: " +
|
||||||
|
(policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!userIds) {
|
if (!userIds) {
|
||||||
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs"));
|
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import {
|
import { sites, userOrgs, userSites, roleSites, roles } from "@server/db";
|
||||||
sites,
|
|
||||||
userOrgs,
|
|
||||||
userSites,
|
|
||||||
roleSites,
|
|
||||||
roles,
|
|
||||||
} from "@server/db";
|
|
||||||
import { and, eq, or } from "drizzle-orm";
|
import { and, eq, or } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
|
||||||
export async function verifySiteAccess(
|
export async function verifySiteAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -82,6 +77,24 @@ export async function verifySiteAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: req.userOrg.orgId,
|
||||||
|
userId,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Failed organization access policy check: " +
|
||||||
|
(policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const userOrgRoleId = req.userOrg.roleId;
|
const userOrgRoleId = req.userOrg.roleId;
|
||||||
req.userOrgRoleId = userOrgRoleId;
|
req.userOrgRoleId = userOrgRoleId;
|
||||||
req.userOrgId = site[0].orgId;
|
req.userOrgId = site[0].orgId;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { db } from "@server/db";
|
import { db, roleSiteResources, userOrgs, userSiteResources } from "@server/db";
|
||||||
import { siteResources } from "@server/db";
|
import { siteResources } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
|
||||||
export async function verifySiteResourceAccess(
|
export async function verifySiteResourceAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -12,44 +13,145 @@ export async function verifySiteResourceAccess(
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const siteResourceId = parseInt(req.params.siteResourceId);
|
const userId = req.user!.userId;
|
||||||
const siteId = parseInt(req.params.siteId);
|
const siteResourceId =
|
||||||
const orgId = req.params.orgId;
|
req.params.siteResourceId ||
|
||||||
|
req.body.siteResourceId ||
|
||||||
|
req.query.siteResourceId;
|
||||||
|
|
||||||
if (!siteResourceId || !siteId || !orgId) {
|
if (!userId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!siteResourceId) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
"Missing required parameters"
|
"Site resource ID is required"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const siteResourceIdNum = parseInt(siteResourceId as string, 10);
|
||||||
|
if (isNaN(siteResourceIdNum)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Invalid site resource ID"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the site resource exists and belongs to the specified site and org
|
|
||||||
const [siteResource] = await db
|
const [siteResource] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(siteResources)
|
.from(siteResources)
|
||||||
.where(and(
|
.where(eq(siteResources.siteResourceId, siteResourceIdNum))
|
||||||
eq(siteResources.siteResourceId, siteResourceId),
|
|
||||||
eq(siteResources.siteId, siteId),
|
|
||||||
eq(siteResources.orgId, orgId)
|
|
||||||
))
|
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!siteResource) {
|
if (!siteResource) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.NOT_FOUND,
|
HttpCode.NOT_FOUND,
|
||||||
"Site resource not found"
|
`Site resource with ID ${siteResourceIdNum} not found`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!siteResource.orgId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
`Site resource with ID ${siteResourceIdNum} does not have an organization ID`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.userOrg) {
|
||||||
|
const userOrgRole = await db
|
||||||
|
.select()
|
||||||
|
.from(userOrgs)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userOrgs.userId, userId),
|
||||||
|
eq(userOrgs.orgId, siteResource.orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
req.userOrg = userOrgRole[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.userOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"User does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: req.userOrg.orgId,
|
||||||
|
userId,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Failed organization access policy check: " +
|
||||||
|
(policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userOrgRoleId = req.userOrg.roleId;
|
||||||
|
req.userOrgRoleId = userOrgRoleId;
|
||||||
|
req.userOrgId = siteResource.orgId;
|
||||||
|
|
||||||
// Attach the siteResource to the request for use in the next middleware/route
|
// Attach the siteResource to the request for use in the next middleware/route
|
||||||
// @ts-ignore - Extending Request type
|
|
||||||
req.siteResource = siteResource;
|
req.siteResource = siteResource;
|
||||||
|
|
||||||
next();
|
const roleResourceAccess = await db
|
||||||
|
.select()
|
||||||
|
.from(roleSiteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(roleSiteResources.siteResourceId, siteResourceIdNum),
|
||||||
|
eq(roleSiteResources.roleId, userOrgRoleId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (roleResourceAccess.length > 0) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const userResourceAccess = await db
|
||||||
|
.select()
|
||||||
|
.from(userSiteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userSiteResources.userId, userId),
|
||||||
|
eq(userSiteResources.siteResourceId, siteResourceIdNum)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (userResourceAccess.length > 0) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"User does not have access to this resource"
|
||||||
|
)
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error verifying site resource access:", error);
|
logger.error("Error verifying site resource access:", error);
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { and, eq } from "drizzle-orm";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { canUserAccessResource } from "../auth/canUserAccessResource";
|
import { canUserAccessResource } from "../auth/canUserAccessResource";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
|
||||||
export async function verifyTargetAccess(
|
export async function verifyTargetAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -102,6 +103,26 @@ export async function verifyTargetAccess(
|
|||||||
req.userOrgId = resource[0].orgId!;
|
req.userOrgId = resource[0].orgId!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const orgId = req.userOrg.orgId;
|
||||||
|
|
||||||
|
if (req.orgPolicyAllowed === undefined && orgId) {
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId,
|
||||||
|
userId,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Failed organization access policy check: " +
|
||||||
|
(policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resourceAllowed = await canUserAccessResource({
|
const resourceAllowed = await canUserAccessResource({
|
||||||
userId,
|
userId,
|
||||||
resourceId,
|
resourceId,
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ export const verifySessionUserMiddleware = async (
|
|||||||
res: Response<ErrorResponse>,
|
res: Response<ErrorResponse>,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
) => {
|
) => {
|
||||||
const { session, user } = await verifySession(req);
|
const { forceLogin } = req.query;
|
||||||
|
|
||||||
|
const { session, user } = await verifySession(req, forceLogin === "true");
|
||||||
if (!session || !user) {
|
if (!session || !user) {
|
||||||
if (config.getRawConfig().app.log_failed_attempts) {
|
if (config.getRawConfig().app.log_failed_attempts) {
|
||||||
logger.info(`User session not found. IP: ${req.ip}.`);
|
logger.info(`User session not found. IP: ${req.ip}.`);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { userOrgs } from "@server/db";
|
|||||||
import { and, eq, or } from "drizzle-orm";
|
import { and, eq, or } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
|
||||||
export async function verifyUserAccess(
|
export async function verifyUserAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -47,6 +48,24 @@ export async function verifyUserAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: req.userOrg.orgId,
|
||||||
|
userId,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Failed organization access policy check: " +
|
||||||
|
(policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ export class PrivateConfig {
|
|||||||
|
|
||||||
this.rawPrivateConfig = parsedPrivateConfig;
|
this.rawPrivateConfig = parsedPrivateConfig;
|
||||||
|
|
||||||
|
process.env.BRANDING_HIDE_AUTH_LAYOUT_FOOTER =
|
||||||
|
this.rawPrivateConfig.branding?.hide_auth_layout_footer === true
|
||||||
|
? "true"
|
||||||
|
: "false";
|
||||||
|
|
||||||
if (this.rawPrivateConfig.branding?.colors) {
|
if (this.rawPrivateConfig.branding?.colors) {
|
||||||
process.env.BRANDING_COLORS = JSON.stringify(
|
process.env.BRANDING_COLORS = JSON.stringify(
|
||||||
this.rawPrivateConfig.branding?.colors
|
this.rawPrivateConfig.branding?.colors
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ export async function listExitNodes(orgId: string, filterOnline = false, noCloud
|
|||||||
|
|
||||||
// // set the item in the database if it is offline
|
// // set the item in the database if it is offline
|
||||||
// if (isActuallyOnline != node.online) {
|
// if (isActuallyOnline != node.online) {
|
||||||
// await db
|
// await trx
|
||||||
// .update(exitNodes)
|
// .update(exitNodes)
|
||||||
// .set({ online: isActuallyOnline })
|
// .set({ online: isActuallyOnline })
|
||||||
// .where(eq(exitNodes.exitNodeId, node.exitNodeId));
|
// .where(eq(exitNodes.exitNodeId, node.exitNodeId));
|
||||||
|
|||||||
363
server/private/lib/lock.ts
Normal file
363
server/private/lib/lock.ts
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { config } from "@server/lib/config";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { redis } from "#private/lib/redis";
|
||||||
|
|
||||||
|
export class LockManager {
|
||||||
|
/**
|
||||||
|
* Acquire a distributed lock using Redis SET with NX and PX options
|
||||||
|
* @param lockKey - Unique identifier for the lock
|
||||||
|
* @param ttlMs - Time to live in milliseconds
|
||||||
|
* @returns Promise<boolean> - true if lock acquired, false otherwise
|
||||||
|
*/
|
||||||
|
async acquireLock(
|
||||||
|
lockKey: string,
|
||||||
|
ttlMs: number = 30000
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!redis || !redis.status || redis.status !== "ready") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lockValue = `${
|
||||||
|
config.getRawConfig().gerbil.exit_node_name
|
||||||
|
}:${Date.now()}`;
|
||||||
|
const redisKey = `lock:${lockKey}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use SET with NX (only set if not exists) and PX (expire in milliseconds)
|
||||||
|
// This is atomic and handles both setting and expiration
|
||||||
|
const result = await redis.set(
|
||||||
|
redisKey,
|
||||||
|
lockValue,
|
||||||
|
"PX",
|
||||||
|
ttlMs,
|
||||||
|
"NX"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result === "OK") {
|
||||||
|
logger.debug(
|
||||||
|
`Lock acquired: ${lockKey} by ${
|
||||||
|
config.getRawConfig().gerbil.exit_node_name
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the existing lock is from this worker (reentrant behavior)
|
||||||
|
const existingValue = await redis.get(redisKey);
|
||||||
|
if (
|
||||||
|
existingValue &&
|
||||||
|
existingValue.startsWith(
|
||||||
|
`${config.getRawConfig().gerbil.exit_node_name}:`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Extend the lock TTL since it's the same worker
|
||||||
|
await redis.pexpire(redisKey, ttlMs);
|
||||||
|
logger.debug(
|
||||||
|
`Lock extended: ${lockKey} by ${
|
||||||
|
config.getRawConfig().gerbil.exit_node_name
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to acquire lock ${lockKey}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release a lock using Lua script to ensure atomicity
|
||||||
|
* @param lockKey - Unique identifier for the lock
|
||||||
|
*/
|
||||||
|
async releaseLock(lockKey: string): Promise<void> {
|
||||||
|
if (!redis || !redis.status || redis.status !== "ready") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const redisKey = `lock:${lockKey}`;
|
||||||
|
|
||||||
|
// Lua script to ensure we only delete the lock if it belongs to this worker
|
||||||
|
const luaScript = `
|
||||||
|
local key = KEYS[1]
|
||||||
|
local worker_prefix = ARGV[1]
|
||||||
|
local current_value = redis.call('GET', key)
|
||||||
|
|
||||||
|
if current_value and string.find(current_value, worker_prefix, 1, true) == 1 then
|
||||||
|
return redis.call('DEL', key)
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = (await redis.eval(
|
||||||
|
luaScript,
|
||||||
|
1,
|
||||||
|
redisKey,
|
||||||
|
`${config.getRawConfig().gerbil.exit_node_name}:`
|
||||||
|
)) as number;
|
||||||
|
|
||||||
|
if (result === 1) {
|
||||||
|
logger.debug(
|
||||||
|
`Lock released: ${lockKey} by ${
|
||||||
|
config.getRawConfig().gerbil.exit_node_name
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
`Lock not released - not owned by worker: ${lockKey} by ${
|
||||||
|
config.getRawConfig().gerbil.exit_node_name
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to release lock ${lockKey}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force release a lock regardless of owner (use with caution)
|
||||||
|
* @param lockKey - Unique identifier for the lock
|
||||||
|
*/
|
||||||
|
async forceReleaseLock(lockKey: string): Promise<void> {
|
||||||
|
if (!redis || !redis.status || redis.status !== "ready") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const redisKey = `lock:${lockKey}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await redis.del(redisKey);
|
||||||
|
if (result === 1) {
|
||||||
|
logger.debug(`Lock force released: ${lockKey}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to force release lock ${lockKey}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a lock exists and get its info
|
||||||
|
* @param lockKey - Unique identifier for the lock
|
||||||
|
* @returns Promise<{exists: boolean, ownedByMe: boolean, ttl: number}>
|
||||||
|
*/
|
||||||
|
async getLockInfo(lockKey: string): Promise<{
|
||||||
|
exists: boolean;
|
||||||
|
ownedByMe: boolean;
|
||||||
|
ttl: number;
|
||||||
|
owner?: string;
|
||||||
|
}> {
|
||||||
|
if (!redis || !redis.status || redis.status !== "ready") {
|
||||||
|
return { exists: false, ownedByMe: true, ttl: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const redisKey = `lock:${lockKey}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [value, ttl] = await Promise.all([
|
||||||
|
redis.get(redisKey),
|
||||||
|
redis.pttl(redisKey)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const exists = value !== null;
|
||||||
|
const ownedByMe =
|
||||||
|
exists &&
|
||||||
|
value!.startsWith(`${config.getRawConfig().gerbil.exit_node_name}:`);
|
||||||
|
const owner = exists ? value!.split(":")[0] : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
exists,
|
||||||
|
ownedByMe,
|
||||||
|
ttl: ttl > 0 ? ttl : 0,
|
||||||
|
owner
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to get lock info ${lockKey}:`, error);
|
||||||
|
return { exists: false, ownedByMe: false, ttl: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend the TTL of an existing lock owned by this worker
|
||||||
|
* @param lockKey - Unique identifier for the lock
|
||||||
|
* @param ttlMs - New TTL in milliseconds
|
||||||
|
* @returns Promise<boolean> - true if extended successfully
|
||||||
|
*/
|
||||||
|
async extendLock(lockKey: string, ttlMs: number): Promise<boolean> {
|
||||||
|
if (!redis || !redis.status || redis.status !== "ready") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const redisKey = `lock:${lockKey}`;
|
||||||
|
|
||||||
|
// Lua script to extend TTL only if lock is owned by this worker
|
||||||
|
const luaScript = `
|
||||||
|
local key = KEYS[1]
|
||||||
|
local worker_prefix = ARGV[1]
|
||||||
|
local ttl = tonumber(ARGV[2])
|
||||||
|
local current_value = redis.call('GET', key)
|
||||||
|
|
||||||
|
if current_value and string.find(current_value, worker_prefix, 1, true) == 1 then
|
||||||
|
return redis.call('PEXPIRE', key, ttl)
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = (await redis.eval(
|
||||||
|
luaScript,
|
||||||
|
1,
|
||||||
|
redisKey,
|
||||||
|
`${config.getRawConfig().gerbil.exit_node_name}:`,
|
||||||
|
ttlMs.toString()
|
||||||
|
)) as number;
|
||||||
|
|
||||||
|
if (result === 1) {
|
||||||
|
logger.debug(
|
||||||
|
`Lock extended: ${lockKey} by ${
|
||||||
|
config.getRawConfig().gerbil.exit_node_name
|
||||||
|
} for ${ttlMs}ms`
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to extend lock ${lockKey}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to acquire lock with retries and exponential backoff
|
||||||
|
* @param lockKey - Unique identifier for the lock
|
||||||
|
* @param ttlMs - Time to live in milliseconds
|
||||||
|
* @param maxRetries - Maximum number of retry attempts
|
||||||
|
* @param baseDelayMs - Base delay between retries in milliseconds
|
||||||
|
* @returns Promise<boolean> - true if lock acquired
|
||||||
|
*/
|
||||||
|
async acquireLockWithRetry(
|
||||||
|
lockKey: string,
|
||||||
|
ttlMs: number = 30000,
|
||||||
|
maxRetries: number = 5,
|
||||||
|
baseDelayMs: number = 100
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!redis || !redis.status || redis.status !== "ready") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
|
const acquired = await this.acquireLock(lockKey, ttlMs);
|
||||||
|
|
||||||
|
if (acquired) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
// Exponential backoff with jitter
|
||||||
|
const delay =
|
||||||
|
baseDelayMs * Math.pow(2, attempt) + Math.random() * 100;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
`Failed to acquire lock ${lockKey} after ${maxRetries + 1} attempts`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a function while holding a lock
|
||||||
|
* @param lockKey - Unique identifier for the lock
|
||||||
|
* @param fn - Function to execute while holding the lock
|
||||||
|
* @param ttlMs - Lock TTL in milliseconds
|
||||||
|
* @returns Promise<T> - Result of the executed function
|
||||||
|
*/
|
||||||
|
async withLock<T>(
|
||||||
|
lockKey: string,
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
ttlMs: number = 30000
|
||||||
|
): Promise<T> {
|
||||||
|
if (!redis || !redis.status || redis.status !== "ready") {
|
||||||
|
return await fn();
|
||||||
|
}
|
||||||
|
|
||||||
|
const acquired = await this.acquireLock(lockKey, ttlMs);
|
||||||
|
|
||||||
|
if (!acquired) {
|
||||||
|
throw new Error(`Failed to acquire lock: ${lockKey}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} finally {
|
||||||
|
await this.releaseLock(lockKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up expired locks - Redis handles this automatically, but this method
|
||||||
|
* can be used to get statistics about locks
|
||||||
|
* @returns Promise<{activeLocksCount: number, locksOwnedByMe: number}>
|
||||||
|
*/
|
||||||
|
async getLockStatistics(): Promise<{
|
||||||
|
activeLocksCount: number;
|
||||||
|
locksOwnedByMe: number;
|
||||||
|
}> {
|
||||||
|
if (!redis || !redis.status || redis.status !== "ready") {
|
||||||
|
return { activeLocksCount: 0, locksOwnedByMe: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keys = await redis.keys("lock:*");
|
||||||
|
let locksOwnedByMe = 0;
|
||||||
|
|
||||||
|
if (keys.length > 0) {
|
||||||
|
const values = await redis.mget(...keys);
|
||||||
|
locksOwnedByMe = values.filter(
|
||||||
|
(value) =>
|
||||||
|
value &&
|
||||||
|
value.startsWith(
|
||||||
|
`${config.getRawConfig().gerbil.exit_node_name}:`
|
||||||
|
)
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeLocksCount: keys.length,
|
||||||
|
locksOwnedByMe
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to get lock statistics:", error);
|
||||||
|
return { activeLocksCount: 0, locksOwnedByMe: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the Redis connection
|
||||||
|
*/
|
||||||
|
async disconnect(): Promise<void> {
|
||||||
|
if (!redis || !redis.status || redis.status !== "ready") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await redis.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const lockManager = new LockManager();
|
||||||
@@ -72,6 +72,43 @@ export class RateLimitService {
|
|||||||
return `ratelimit:${clientId}:${messageType}`;
|
return `ratelimit:${clientId}:${messageType}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to clean up old timestamp fields from a Redis hash
|
||||||
|
private async cleanupOldTimestamps(key: string, windowStart: number): Promise<void> {
|
||||||
|
if (!redisManager.isRedisEnabled()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = redisManager.getClient();
|
||||||
|
if (!client) return;
|
||||||
|
|
||||||
|
// Get all fields in the hash
|
||||||
|
const allData = await redisManager.hgetall(key);
|
||||||
|
if (!allData || Object.keys(allData).length === 0) return;
|
||||||
|
|
||||||
|
// Find fields that are older than the window
|
||||||
|
const fieldsToDelete: string[] = [];
|
||||||
|
for (const timestamp of Object.keys(allData)) {
|
||||||
|
const time = parseInt(timestamp);
|
||||||
|
if (time < windowStart) {
|
||||||
|
fieldsToDelete.push(timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete old fields in batches to avoid call stack size exceeded errors
|
||||||
|
// The spread operator can cause issues with very large arrays
|
||||||
|
if (fieldsToDelete.length > 0) {
|
||||||
|
const batchSize = 1000; // Process 1000 fields at a time
|
||||||
|
for (let i = 0; i < fieldsToDelete.length; i += batchSize) {
|
||||||
|
const batch = fieldsToDelete.slice(i, i + batchSize);
|
||||||
|
await client.hdel(key, ...batch);
|
||||||
|
}
|
||||||
|
logger.debug(`Cleaned up ${fieldsToDelete.length} old timestamp fields from ${key}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to cleanup old timestamps for key ${key}:`, error);
|
||||||
|
// Don't throw - cleanup failures shouldn't block rate limiting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to sync local rate limit data to Redis
|
// Helper function to sync local rate limit data to Redis
|
||||||
private async syncRateLimitToRedis(
|
private async syncRateLimitToRedis(
|
||||||
clientId: string,
|
clientId: string,
|
||||||
@@ -81,8 +118,12 @@ export class RateLimitService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const currentTime = Math.floor(Date.now() / 1000);
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
const windowStart = currentTime - RATE_LIMIT_WINDOW;
|
||||||
const globalKey = this.getRateLimitKey(clientId);
|
const globalKey = this.getRateLimitKey(clientId);
|
||||||
|
|
||||||
|
// Clean up old timestamp fields before writing
|
||||||
|
await this.cleanupOldTimestamps(globalKey, windowStart);
|
||||||
|
|
||||||
// Get current value and add pending count
|
// Get current value and add pending count
|
||||||
const currentValue = await redisManager.hget(
|
const currentValue = await redisManager.hget(
|
||||||
globalKey,
|
globalKey,
|
||||||
@@ -93,7 +134,7 @@ export class RateLimitService {
|
|||||||
).toString();
|
).toString();
|
||||||
await redisManager.hset(globalKey, currentTime.toString(), newValue);
|
await redisManager.hset(globalKey, currentTime.toString(), newValue);
|
||||||
|
|
||||||
// Set TTL using the client directly
|
// Set TTL using the client directly - this prevents the key from persisting forever
|
||||||
if (redisManager.getClient()) {
|
if (redisManager.getClient()) {
|
||||||
await redisManager
|
await redisManager
|
||||||
.getClient()
|
.getClient()
|
||||||
@@ -119,8 +160,12 @@ export class RateLimitService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const currentTime = Math.floor(Date.now() / 1000);
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
const windowStart = currentTime - RATE_LIMIT_WINDOW;
|
||||||
const messageTypeKey = this.getMessageTypeRateLimitKey(clientId, messageType);
|
const messageTypeKey = this.getMessageTypeRateLimitKey(clientId, messageType);
|
||||||
|
|
||||||
|
// Clean up old timestamp fields before writing
|
||||||
|
await this.cleanupOldTimestamps(messageTypeKey, windowStart);
|
||||||
|
|
||||||
// Get current value and add pending count
|
// Get current value and add pending count
|
||||||
const currentValue = await redisManager.hget(
|
const currentValue = await redisManager.hget(
|
||||||
messageTypeKey,
|
messageTypeKey,
|
||||||
@@ -135,7 +180,7 @@ export class RateLimitService {
|
|||||||
newValue
|
newValue
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set TTL using the client directly
|
// Set TTL using the client directly - this prevents the key from persisting forever
|
||||||
if (redisManager.getClient()) {
|
if (redisManager.getClient()) {
|
||||||
await redisManager
|
await redisManager
|
||||||
.getClient()
|
.getClient()
|
||||||
@@ -170,6 +215,10 @@ export class RateLimitService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const globalKey = this.getRateLimitKey(clientId);
|
const globalKey = this.getRateLimitKey(clientId);
|
||||||
|
|
||||||
|
// Clean up old timestamp fields before reading
|
||||||
|
await this.cleanupOldTimestamps(globalKey, windowStart);
|
||||||
|
|
||||||
const globalRateLimitData = await redisManager.hgetall(globalKey);
|
const globalRateLimitData = await redisManager.hgetall(globalKey);
|
||||||
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -215,6 +264,10 @@ export class RateLimitService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const messageTypeKey = this.getMessageTypeRateLimitKey(clientId, messageType);
|
const messageTypeKey = this.getMessageTypeRateLimitKey(clientId, messageType);
|
||||||
|
|
||||||
|
// Clean up old timestamp fields before reading
|
||||||
|
await this.cleanupOldTimestamps(messageTypeKey, windowStart);
|
||||||
|
|
||||||
const messageTypeRateLimitData = await redisManager.hgetall(messageTypeKey);
|
const messageTypeRateLimitData = await redisManager.hgetall(messageTypeKey);
|
||||||
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|||||||
@@ -50,14 +50,14 @@ export const privateConfigSchema = z.object({
|
|||||||
host: z.string(),
|
host: z.string(),
|
||||||
port: portSchema,
|
port: portSchema,
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
db: z.number().int().nonnegative().optional().default(0),
|
db: z.int().nonnegative().optional().default(0),
|
||||||
replicas: z
|
replicas: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
host: z.string(),
|
host: z.string(),
|
||||||
port: portSchema,
|
port: portSchema,
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
db: z.number().int().nonnegative().optional().default(0)
|
db: z.int().nonnegative().optional().default(0)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.optional()
|
.optional()
|
||||||
@@ -79,14 +79,14 @@ export const privateConfigSchema = z.object({
|
|||||||
.default("http://gerbil:3004")
|
.default("http://gerbil:3004")
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({}),
|
.prefault({}),
|
||||||
flags: z
|
flags: z
|
||||||
.object({
|
.object({
|
||||||
enable_redis: z.boolean().optional().default(false),
|
enable_redis: z.boolean().optional().default(false),
|
||||||
use_pangolin_dns: z.boolean().optional().default(false)
|
use_pangolin_dns: z.boolean().optional().default(false)
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({}),
|
.prefault({}),
|
||||||
branding: z
|
branding: z
|
||||||
.object({
|
.object({
|
||||||
app_name: z.string().optional(),
|
app_name: z.string().optional(),
|
||||||
@@ -124,6 +124,7 @@ export const privateConfigSchema = z.object({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
|
hide_auth_layout_footer: z.boolean().optional().default(false),
|
||||||
login_page: z
|
login_page: z
|
||||||
.object({
|
.object({
|
||||||
subtitle_text: z.string().optional(),
|
subtitle_text: z.string().optional(),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import privateConfig from "#private/lib/config";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
|
||||||
export enum AudienceIds {
|
export enum AudienceIds {
|
||||||
SignUps = "5cfbf99b-c592-40a9-9b8a-577a4681c158",
|
SignUps = "6c4e77b2-0851-4bd6-bac8-f51f91360f1a",
|
||||||
Subscribed = "870b43fd-387f-44de-8fc1-707335f30b20",
|
Subscribed = "870b43fd-387f-44de-8fc1-707335f30b20",
|
||||||
Churned = "f3ae92bd-2fdb-4d77-8746-2118afd62549",
|
Churned = "f3ae92bd-2fdb-4d77-8746-2118afd62549",
|
||||||
Newsletter = "5500c431-191c-42f0-a5d4-8b6d445b4ea0"
|
Newsletter = "5500c431-191c-42f0-a5d4-8b6d445b4ea0"
|
||||||
|
|||||||
@@ -434,9 +434,9 @@ export async function getTraefikConfig(
|
|||||||
routerMiddlewares.push(rewriteMiddlewareName);
|
routerMiddlewares.push(rewriteMiddlewareName);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(
|
// logger.debug(
|
||||||
`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`
|
// `Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`
|
||||||
);
|
// );
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`
|
`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ export * from "./verifyRemoteExitNodeAccess";
|
|||||||
export * from "./verifyIdpAccess";
|
export * from "./verifyIdpAccess";
|
||||||
export * from "./verifyLoginPageAccess";
|
export * from "./verifyLoginPageAccess";
|
||||||
export * from "./logActionAudit";
|
export * from "./logActionAudit";
|
||||||
export * from "./verifySubscription";
|
export * from "./verifySubscription";
|
||||||
|
export * from "./verifyValidLicense";
|
||||||
|
|||||||
@@ -30,17 +30,22 @@ export const queryAccessAuditLogsQuery = z.object({
|
|||||||
timeStart: z
|
timeStart: z
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => !isNaN(Date.parse(val)), {
|
.refine((val) => !isNaN(Date.parse(val)), {
|
||||||
message: "timeStart must be a valid ISO date string"
|
error: "timeStart must be a valid ISO date string"
|
||||||
})
|
})
|
||||||
.transform((val) => Math.floor(new Date(val).getTime() / 1000)),
|
.transform((val) => Math.floor(new Date(val).getTime() / 1000)),
|
||||||
timeEnd: z
|
timeEnd: z
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => !isNaN(Date.parse(val)), {
|
.refine((val) => !isNaN(Date.parse(val)), {
|
||||||
message: "timeEnd must be a valid ISO date string"
|
error: "timeEnd must be a valid ISO date string"
|
||||||
})
|
})
|
||||||
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
|
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
|
||||||
.optional()
|
.optional()
|
||||||
.default(new Date().toISOString()),
|
.prefault(new Date().toISOString())
|
||||||
|
.openapi({
|
||||||
|
type: "string",
|
||||||
|
format: "date-time",
|
||||||
|
description: "End time as ISO date string (defaults to current time)"
|
||||||
|
}),
|
||||||
action: z
|
action: z
|
||||||
.union([z.boolean(), z.string()])
|
.union([z.boolean(), z.string()])
|
||||||
.transform((val) => (typeof val === "string" ? val === "true" : val))
|
.transform((val) => (typeof val === "string" ? val === "true" : val))
|
||||||
@@ -51,7 +56,7 @@ export const queryAccessAuditLogsQuery = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.transform(Number)
|
.transform(Number)
|
||||||
.pipe(z.number().int().positive())
|
.pipe(z.int().positive())
|
||||||
.optional(),
|
.optional(),
|
||||||
actor: z.string().optional(),
|
actor: z.string().optional(),
|
||||||
type: z.string().optional(),
|
type: z.string().optional(),
|
||||||
@@ -61,13 +66,13 @@ export const queryAccessAuditLogsQuery = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
.default("1000")
|
.default("1000")
|
||||||
.transform(Number)
|
.transform(Number)
|
||||||
.pipe(z.number().int().positive()),
|
.pipe(z.int().positive()),
|
||||||
offset: z
|
offset: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.default("0")
|
.default("0")
|
||||||
.transform(Number)
|
.transform(Number)
|
||||||
.pipe(z.number().int().nonnegative())
|
.pipe(z.int().nonnegative())
|
||||||
});
|
});
|
||||||
|
|
||||||
export const queryAccessAuditLogsParams = z.object({
|
export const queryAccessAuditLogsParams = z.object({
|
||||||
|
|||||||
@@ -30,17 +30,22 @@ export const queryActionAuditLogsQuery = z.object({
|
|||||||
timeStart: z
|
timeStart: z
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => !isNaN(Date.parse(val)), {
|
.refine((val) => !isNaN(Date.parse(val)), {
|
||||||
message: "timeStart must be a valid ISO date string"
|
error: "timeStart must be a valid ISO date string"
|
||||||
})
|
})
|
||||||
.transform((val) => Math.floor(new Date(val).getTime() / 1000)),
|
.transform((val) => Math.floor(new Date(val).getTime() / 1000)),
|
||||||
timeEnd: z
|
timeEnd: z
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => !isNaN(Date.parse(val)), {
|
.refine((val) => !isNaN(Date.parse(val)), {
|
||||||
message: "timeEnd must be a valid ISO date string"
|
error: "timeEnd must be a valid ISO date string"
|
||||||
})
|
})
|
||||||
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
|
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
|
||||||
.optional()
|
.optional()
|
||||||
.default(new Date().toISOString()),
|
.prefault(new Date().toISOString())
|
||||||
|
.openapi({
|
||||||
|
type: "string",
|
||||||
|
format: "date-time",
|
||||||
|
description: "End time as ISO date string (defaults to current time)"
|
||||||
|
}),
|
||||||
action: z.string().optional(),
|
action: z.string().optional(),
|
||||||
actorType: z.string().optional(),
|
actorType: z.string().optional(),
|
||||||
actorId: z.string().optional(),
|
actorId: z.string().optional(),
|
||||||
@@ -50,13 +55,13 @@ export const queryActionAuditLogsQuery = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
.default("1000")
|
.default("1000")
|
||||||
.transform(Number)
|
.transform(Number)
|
||||||
.pipe(z.number().int().positive()),
|
.pipe(z.int().positive()),
|
||||||
offset: z
|
offset: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.default("0")
|
.default("0")
|
||||||
.transform(Number)
|
.transform(Number)
|
||||||
.pipe(z.number().int().nonnegative())
|
.pipe(z.int().nonnegative())
|
||||||
});
|
});
|
||||||
|
|
||||||
export const queryActionAuditLogsParams = z.object({
|
export const queryActionAuditLogsParams = z.object({
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { response } from "@server/lib/response";
|
|||||||
import { encrypt } from "@server/lib/crypto";
|
import { encrypt } from "@server/lib/crypto";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
|
|
||||||
const paramsSchema = z.object({}).strict();
|
const paramsSchema = z.strictObject({});
|
||||||
|
|
||||||
export type GetSessionTransferTokenRenponse = {
|
export type GetSessionTransferTokenRenponse = {
|
||||||
token: string;
|
token: string;
|
||||||
|
|||||||
@@ -62,10 +62,10 @@ import { isTargetValid } from "@server/lib/validators";
|
|||||||
import { listExitNodes } from "#private/lib/exitNodes";
|
import { listExitNodes } from "#private/lib/exitNodes";
|
||||||
|
|
||||||
const bodySchema = z.object({
|
const bodySchema = z.object({
|
||||||
email: z.string().toLowerCase().email(),
|
email: z.email().toLowerCase(),
|
||||||
ip: z.string().refine(isTargetValid),
|
ip: z.string().refine(isTargetValid),
|
||||||
method: z.enum(["http", "https"]),
|
method: z.enum(["http", "https"]),
|
||||||
port: z.number().int().min(1).max(65535),
|
port: z.int().min(1).max(65535),
|
||||||
pincode: z
|
pincode: z
|
||||||
.string()
|
.string()
|
||||||
.regex(/^\d{6}$/)
|
.regex(/^\d{6}$/)
|
||||||
|
|||||||
@@ -25,11 +25,9 @@ import stripe from "#private/lib/stripe";
|
|||||||
import { getLineItems, getStandardFeaturePriceSet } from "@server/lib/billing";
|
import { getLineItems, getStandardFeaturePriceSet } from "@server/lib/billing";
|
||||||
import { getTierPriceSet, TierId } from "@server/lib/billing/tiers";
|
import { getTierPriceSet, TierId } from "@server/lib/billing/tiers";
|
||||||
|
|
||||||
const createCheckoutSessionSchema = z
|
const createCheckoutSessionSchema = z.strictObject({
|
||||||
.object({
|
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
})
|
});
|
||||||
.strict();
|
|
||||||
|
|
||||||
export async function createCheckoutSession(
|
export async function createCheckoutSession(
|
||||||
req: Request,
|
req: Request,
|
||||||
|
|||||||
@@ -23,11 +23,9 @@ import config from "@server/lib/config";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import stripe from "#private/lib/stripe";
|
import stripe from "#private/lib/stripe";
|
||||||
|
|
||||||
const createPortalSessionSchema = z
|
const createPortalSessionSchema = z.strictObject({
|
||||||
.object({
|
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
})
|
});
|
||||||
.strict();
|
|
||||||
|
|
||||||
export async function createPortalSession(
|
export async function createPortalSession(
|
||||||
req: Request,
|
req: Request,
|
||||||
|
|||||||
@@ -33,11 +33,9 @@ import {
|
|||||||
SubscriptionItem
|
SubscriptionItem
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
|
|
||||||
const getOrgSchema = z
|
const getOrgSchema = z.strictObject({
|
||||||
.object({
|
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
})
|
});
|
||||||
.strict();
|
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|||||||
@@ -27,11 +27,9 @@ import { usageService } from "@server/lib/billing/usageService";
|
|||||||
import { FeatureId } from "@server/lib/billing";
|
import { FeatureId } from "@server/lib/billing";
|
||||||
import { GetOrgUsageResponse } from "@server/routers/billing/types";
|
import { GetOrgUsageResponse } from "@server/routers/billing/types";
|
||||||
|
|
||||||
const getOrgSchema = z
|
const getOrgSchema = z.strictObject({
|
||||||
.object({
|
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
})
|
});
|
||||||
.strict();
|
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|||||||
@@ -21,11 +21,9 @@ import { fromZodError } from "zod-validation-error";
|
|||||||
import { getOrgTierData } from "#private/lib/billing";
|
import { getOrgTierData } from "#private/lib/billing";
|
||||||
import { GetOrgTierResponse } from "@server/routers/billing/types";
|
import { GetOrgTierResponse } from "@server/routers/billing/types";
|
||||||
|
|
||||||
const getOrgSchema = z
|
const getOrgSchema = z.strictObject({
|
||||||
.object({
|
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
})
|
});
|
||||||
.strict();
|
|
||||||
|
|
||||||
export async function getOrgTier(
|
export async function getOrgTier(
|
||||||
req: Request,
|
req: Request,
|
||||||
|
|||||||
@@ -23,13 +23,11 @@ import { fromError } from "zod-validation-error";
|
|||||||
import { registry } from "@server/openApi";
|
import { registry } from "@server/openApi";
|
||||||
import { GetCertificateResponse } from "@server/routers/certificates/types";
|
import { GetCertificateResponse } from "@server/routers/certificates/types";
|
||||||
|
|
||||||
const getCertificateSchema = z
|
const getCertificateSchema = z.strictObject({
|
||||||
.object({
|
|
||||||
domainId: z.string(),
|
domainId: z.string(),
|
||||||
domain: z.string().min(1).max(255),
|
domain: z.string().min(1).max(255),
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
})
|
});
|
||||||
.strict();
|
|
||||||
|
|
||||||
async function query(domainId: string, domain: string) {
|
async function query(domainId: string, domain: string) {
|
||||||
const [domainRecord] = await db
|
const [domainRecord] = await db
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user