Compare commits

..

127 Commits

Author SHA1 Message Date
Owen Schwartz
603b66780d New translations en-us.json (Norwegian Bokmal) 2026-01-16 14:50:10 -08:00
Owen Schwartz
6bb72a7447 New translations en-us.json (Chinese Simplified) 2026-01-16 14:50:08 -08:00
Owen Schwartz
a679588d3b New translations en-us.json (Turkish) 2026-01-16 14:50:07 -08:00
Owen Schwartz
56114cb11a New translations en-us.json (Russian) 2026-01-16 14:50:06 -08:00
Owen Schwartz
ccc6a64332 New translations en-us.json (Portuguese) 2026-01-16 14:50:04 -08:00
Owen Schwartz
aee8d0131e New translations en-us.json (Polish) 2026-01-16 14:50:03 -08:00
Owen Schwartz
20fc347182 New translations en-us.json (Dutch) 2026-01-16 14:50:02 -08:00
Owen Schwartz
6462b3c482 New translations en-us.json (Korean) 2026-01-16 14:50:00 -08:00
Owen Schwartz
c3d620a9b2 New translations en-us.json (Italian) 2026-01-16 14:49:59 -08:00
Owen Schwartz
1cfab08262 New translations en-us.json (German) 2026-01-16 14:49:58 -08:00
Owen Schwartz
e70919b9e4 New translations en-us.json (Czech) 2026-01-16 14:49:57 -08:00
Owen Schwartz
157b673b64 New translations en-us.json (Bulgarian) 2026-01-16 14:49:55 -08:00
Owen Schwartz
2d71293a7c New translations en-us.json (Spanish) 2026-01-16 14:49:54 -08:00
Owen Schwartz
ac30d28d7e New translations en-us.json (French) 2026-01-16 14:49:52 -08:00
Owen Schwartz
23cf7bf745 New translations en-us.json (Norwegian Bokmal) 2026-01-15 21:54:41 -08:00
Owen Schwartz
d63de9ba40 New translations en-us.json (Chinese Simplified) 2026-01-15 21:54:40 -08:00
Owen Schwartz
e7ac5c34a2 New translations en-us.json (Turkish) 2026-01-15 21:54:38 -08:00
Owen Schwartz
1f88876e3c New translations en-us.json (Russian) 2026-01-15 21:54:37 -08:00
Owen Schwartz
e0425e2458 New translations en-us.json (Portuguese) 2026-01-15 21:54:36 -08:00
Owen Schwartz
10e9f017fb New translations en-us.json (Polish) 2026-01-15 21:54:34 -08:00
Owen Schwartz
6d2cb69e45 New translations en-us.json (Dutch) 2026-01-15 21:54:33 -08:00
Owen Schwartz
2fab9b65a8 New translations en-us.json (Korean) 2026-01-15 21:54:31 -08:00
Owen Schwartz
47a7a3f230 New translations en-us.json (Italian) 2026-01-15 21:54:30 -08:00
Owen Schwartz
61835ca0e6 New translations en-us.json (German) 2026-01-15 21:54:29 -08:00
Owen Schwartz
f4b22c5b31 New translations en-us.json (Czech) 2026-01-15 21:54:27 -08:00
Owen Schwartz
65afe23dd8 New translations en-us.json (Bulgarian) 2026-01-15 21:54:26 -08:00
Owen Schwartz
371e44e235 New translations en-us.json (Spanish) 2026-01-15 21:54:25 -08:00
Owen Schwartz
d349795995 New translations en-us.json (French) 2026-01-15 21:54:23 -08:00
Owen Schwartz
7d106294bc New translations en-us.json (Norwegian Bokmal) 2026-01-14 19:55:40 -08:00
Owen Schwartz
8c3e094534 New translations en-us.json (Chinese Simplified) 2026-01-14 19:55:38 -08:00
Owen Schwartz
b20a7231bc New translations en-us.json (Turkish) 2026-01-14 19:55:37 -08:00
Owen Schwartz
84998fdb6e New translations en-us.json (Russian) 2026-01-14 19:55:36 -08:00
Owen Schwartz
0d9ece1329 New translations en-us.json (Portuguese) 2026-01-14 19:55:34 -08:00
Owen Schwartz
eb7107016b New translations en-us.json (Polish) 2026-01-14 19:55:33 -08:00
Owen Schwartz
a7608424b7 New translations en-us.json (Dutch) 2026-01-14 19:55:31 -08:00
Owen Schwartz
6710657789 New translations en-us.json (Korean) 2026-01-14 19:55:30 -08:00
Owen Schwartz
b25069ab49 New translations en-us.json (Italian) 2026-01-14 19:55:29 -08:00
Owen Schwartz
6e8d1f9149 New translations en-us.json (German) 2026-01-14 19:55:27 -08:00
Owen Schwartz
825da82e56 New translations en-us.json (Czech) 2026-01-14 19:55:26 -08:00
Owen Schwartz
8d28063724 New translations en-us.json (Bulgarian) 2026-01-14 19:55:25 -08:00
Owen Schwartz
a7eed886ab New translations en-us.json (Spanish) 2026-01-14 19:55:23 -08:00
Owen Schwartz
4e965f1e83 New translations en-us.json (French) 2026-01-14 19:55:22 -08:00
Owen Schwartz
ea3011afca New translations en-us.json (Norwegian Bokmal) 2026-01-13 15:50:55 -08:00
Owen Schwartz
b20e5b4af1 New translations en-us.json (Chinese Simplified) 2026-01-13 15:50:53 -08:00
Owen Schwartz
577660c656 New translations en-us.json (Turkish) 2026-01-13 15:50:52 -08:00
Owen Schwartz
f9a9007ed9 New translations en-us.json (Russian) 2026-01-13 15:50:50 -08:00
Owen Schwartz
19873a0b3d New translations en-us.json (Portuguese) 2026-01-13 15:50:49 -08:00
Owen Schwartz
4f13267bc8 New translations en-us.json (Polish) 2026-01-13 15:50:47 -08:00
Owen Schwartz
c820fcc518 New translations en-us.json (Dutch) 2026-01-13 15:50:46 -08:00
Owen Schwartz
c612972f38 New translations en-us.json (Korean) 2026-01-13 15:50:45 -08:00
Owen Schwartz
395c4eff53 New translations en-us.json (Italian) 2026-01-13 15:50:44 -08:00
Owen Schwartz
6aa40bfd61 New translations en-us.json (German) 2026-01-13 15:50:42 -08:00
Owen Schwartz
f5744d0dcd New translations en-us.json (Czech) 2026-01-13 15:50:41 -08:00
Owen Schwartz
a975b33b7e New translations en-us.json (Bulgarian) 2026-01-13 15:50:40 -08:00
Owen Schwartz
92be668a55 New translations en-us.json (Spanish) 2026-01-13 15:50:38 -08:00
Owen Schwartz
15153d5e3a New translations en-us.json (French) 2026-01-13 15:50:37 -08:00
Owen Schwartz
212800d365 New translations en-us.json (Norwegian Bokmal) 2026-01-12 21:08:36 -08:00
Owen Schwartz
24ac9a1623 New translations en-us.json (Chinese Simplified) 2026-01-12 21:08:35 -08:00
Owen Schwartz
78bcfed668 New translations en-us.json (Turkish) 2026-01-12 21:08:33 -08:00
Owen Schwartz
bf8be14c3a New translations en-us.json (Russian) 2026-01-12 21:08:32 -08:00
Owen Schwartz
fe1f7bee60 New translations en-us.json (Portuguese) 2026-01-12 21:08:30 -08:00
Owen Schwartz
fbe14acdd1 New translations en-us.json (Polish) 2026-01-12 21:08:29 -08:00
Owen Schwartz
11c0afd9ab New translations en-us.json (Dutch) 2026-01-12 21:08:27 -08:00
Owen Schwartz
52e6b36c2d New translations en-us.json (Korean) 2026-01-12 21:08:26 -08:00
Owen Schwartz
dcce15036c New translations en-us.json (Italian) 2026-01-12 21:08:24 -08:00
Owen Schwartz
fd27682bbb New translations en-us.json (German) 2026-01-12 21:08:23 -08:00
Owen Schwartz
0cffeda5da New translations en-us.json (Czech) 2026-01-12 21:08:21 -08:00
Owen Schwartz
aff486ca57 New translations en-us.json (Bulgarian) 2026-01-12 21:08:20 -08:00
Owen Schwartz
d3eb950888 New translations en-us.json (Spanish) 2026-01-12 21:08:18 -08:00
Owen Schwartz
1b98f44588 New translations en-us.json (French) 2026-01-12 21:08:16 -08:00
Owen Schwartz
8a5a0c7c18 New translations en-us.json (Norwegian Bokmal) 2026-01-12 16:20:12 -08:00
Owen Schwartz
d2f0825498 New translations en-us.json (Chinese Simplified) 2026-01-12 16:20:11 -08:00
Owen Schwartz
ac88b79066 New translations en-us.json (Turkish) 2026-01-12 16:20:09 -08:00
Owen Schwartz
9160d94c7b New translations en-us.json (Russian) 2026-01-12 16:20:08 -08:00
Owen Schwartz
de6ce7aa10 New translations en-us.json (Portuguese) 2026-01-12 16:20:06 -08:00
Owen Schwartz
91a4b11632 New translations en-us.json (Polish) 2026-01-12 16:20:05 -08:00
Owen Schwartz
99ed4ea683 New translations en-us.json (Dutch) 2026-01-12 16:20:04 -08:00
Owen Schwartz
1a0d4870ed New translations en-us.json (Korean) 2026-01-12 16:20:02 -08:00
Owen Schwartz
f1bd315a96 New translations en-us.json (Italian) 2026-01-12 16:20:01 -08:00
Owen Schwartz
42d8e932ff New translations en-us.json (German) 2026-01-12 16:19:59 -08:00
Owen Schwartz
658ec87d1b New translations en-us.json (Czech) 2026-01-12 16:19:58 -08:00
Owen Schwartz
e9bbee13c0 New translations en-us.json (Bulgarian) 2026-01-12 16:19:57 -08:00
Owen Schwartz
99164bf7ab New translations en-us.json (Spanish) 2026-01-12 16:19:55 -08:00
Owen Schwartz
6a3cc578a7 New translations en-us.json (French) 2026-01-12 16:19:54 -08:00
Owen Schwartz
720c3d7c41 New translations en-us.json (German) 2026-01-11 17:43:02 -08:00
Owen Schwartz
72733a9b77 New translations en-us.json (Norwegian Bokmal) 2026-01-11 14:32:28 -08:00
Owen Schwartz
1295141eaf New translations en-us.json (Chinese Simplified) 2026-01-11 14:32:26 -08:00
Owen Schwartz
e8099795b3 New translations en-us.json (Turkish) 2026-01-11 14:32:25 -08:00
Owen Schwartz
d65fbcc28b New translations en-us.json (Russian) 2026-01-11 14:32:24 -08:00
Owen Schwartz
ad0a17f642 New translations en-us.json (Portuguese) 2026-01-11 14:32:23 -08:00
Owen Schwartz
c2ecabab33 New translations en-us.json (Polish) 2026-01-11 14:32:21 -08:00
Owen Schwartz
7170b14b1f New translations en-us.json (Dutch) 2026-01-11 14:32:20 -08:00
Owen Schwartz
343ba64eb4 New translations en-us.json (Korean) 2026-01-11 14:32:19 -08:00
Owen Schwartz
242c314e42 New translations en-us.json (Italian) 2026-01-11 14:32:17 -08:00
Owen Schwartz
16f9191bc5 New translations en-us.json (German) 2026-01-11 14:32:16 -08:00
Owen Schwartz
6ec51e50d1 New translations en-us.json (Czech) 2026-01-11 14:32:15 -08:00
Owen Schwartz
f1d73c5f8c New translations en-us.json (Bulgarian) 2026-01-11 14:32:13 -08:00
Owen Schwartz
6093add470 New translations en-us.json (Spanish) 2026-01-11 14:32:12 -08:00
Owen Schwartz
4af6bd43ef New translations en-us.json (French) 2026-01-11 14:32:11 -08:00
Owen Schwartz
99ad7c6bef New translations en-us.json (Norwegian Bokmal) 2026-01-09 18:42:36 -08:00
Owen Schwartz
fc192d955b New translations en-us.json (Chinese Simplified) 2026-01-09 18:42:35 -08:00
Owen Schwartz
914f1b41c6 New translations en-us.json (Turkish) 2026-01-09 18:42:34 -08:00
Owen Schwartz
0601eacfd5 New translations en-us.json (Russian) 2026-01-09 18:42:33 -08:00
Owen Schwartz
5481eae606 New translations en-us.json (Portuguese) 2026-01-09 18:42:31 -08:00
Owen Schwartz
d868c85ebe New translations en-us.json (Polish) 2026-01-09 18:42:30 -08:00
Owen Schwartz
ff0f0aa4e0 New translations en-us.json (Dutch) 2026-01-09 18:42:29 -08:00
Owen Schwartz
d4f9f20ef9 New translations en-us.json (Korean) 2026-01-09 18:42:27 -08:00
Owen Schwartz
30808ce2db New translations en-us.json (Italian) 2026-01-09 18:42:26 -08:00
Owen Schwartz
db5f7ed731 New translations en-us.json (German) 2026-01-09 18:42:25 -08:00
Owen Schwartz
6fd69f1293 New translations en-us.json (Czech) 2026-01-09 18:42:23 -08:00
Owen Schwartz
9f1dd42a85 New translations en-us.json (Bulgarian) 2026-01-09 18:42:22 -08:00
Owen Schwartz
7b55ef1cc8 New translations en-us.json (Spanish) 2026-01-09 18:42:20 -08:00
Owen Schwartz
b988f21b15 New translations en-us.json (French) 2026-01-09 18:42:19 -08:00
Owen Schwartz
a688757f5c New translations en-us.json (Norwegian Bokmal) 2026-01-09 15:04:27 -08:00
Owen Schwartz
0ed1852a35 New translations en-us.json (Chinese Simplified) 2026-01-09 15:04:26 -08:00
Owen Schwartz
a5c3702b8d New translations en-us.json (Turkish) 2026-01-09 15:04:25 -08:00
Owen Schwartz
b25f43da9a New translations en-us.json (Russian) 2026-01-09 15:04:23 -08:00
Owen Schwartz
ea0f0de802 New translations en-us.json (Portuguese) 2026-01-09 15:04:22 -08:00
Owen Schwartz
476131859d New translations en-us.json (Polish) 2026-01-09 15:04:21 -08:00
Owen Schwartz
8573c38332 New translations en-us.json (Dutch) 2026-01-09 15:04:19 -08:00
Owen Schwartz
bc9d2835be New translations en-us.json (Korean) 2026-01-09 15:04:18 -08:00
Owen Schwartz
df582b0409 New translations en-us.json (Italian) 2026-01-09 15:04:17 -08:00
Owen Schwartz
e3865dcd4d New translations en-us.json (German) 2026-01-09 15:04:15 -08:00
Owen Schwartz
0eba5cdb0b New translations en-us.json (Czech) 2026-01-09 15:04:14 -08:00
Owen Schwartz
2284f2cafc New translations en-us.json (Bulgarian) 2026-01-09 15:04:12 -08:00
Owen Schwartz
80215a7de0 New translations en-us.json (Spanish) 2026-01-09 15:04:11 -08:00
Owen Schwartz
ddd829064a New translations en-us.json (French) 2026-01-09 15:04:10 -08:00
160 changed files with 3005 additions and 8571 deletions

View File

@@ -329,89 +329,20 @@ jobs:
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
shell: bash
- name: Copy tags from Docker Hub to GHCR
# Mirror the already-built images (all architectures) to GHCR so we can sign them
- name: Copy tag from Docker Hub to GHCR
# Mirror the already-built image (all architectures) to GHCR so we can sign it
# Wait a bit for both architectures to be available in Docker Hub manifest
env:
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
run: |
set -euo pipefail
TAG=${{ env.TAG }}
MAJOR_TAG=$(echo $TAG | cut -d. -f1)
MINOR_TAG=$(echo $TAG | cut -d. -f1,2)
echo "Waiting for multi-arch manifests to be ready..."
echo "Waiting for multi-arch manifest to be ready..."
sleep 30
# Determine if this is an RC release
IS_RC="false"
if echo "$TAG" | grep -qE "rc[0-9]+$"; then
IS_RC="true"
fi
if [ "$IS_RC" = "true" ]; then
echo "RC release detected - copying version-specific tags only"
# SQLite OSS
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
skopeo copy --all --retry-times 3 \
docker://$DOCKERHUB_IMAGE:$TAG \
docker://$GHCR_IMAGE:$TAG
# PostgreSQL OSS
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:postgresql-${TAG} -> ${{ env.GHCR_IMAGE }}:postgresql-${TAG}"
skopeo copy --all --retry-times 3 \
docker://$DOCKERHUB_IMAGE:postgresql-$TAG \
docker://$GHCR_IMAGE:postgresql-$TAG
# SQLite Enterprise
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-${TAG} -> ${{ env.GHCR_IMAGE }}:ee-${TAG}"
skopeo copy --all --retry-times 3 \
docker://$DOCKERHUB_IMAGE:ee-$TAG \
docker://$GHCR_IMAGE:ee-$TAG
# PostgreSQL Enterprise
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-postgresql-${TAG} -> ${{ env.GHCR_IMAGE }}:ee-postgresql-${TAG}"
skopeo copy --all --retry-times 3 \
docker://$DOCKERHUB_IMAGE:ee-postgresql-$TAG \
docker://$GHCR_IMAGE:ee-postgresql-$TAG
else
echo "Regular release detected - copying all tags (latest, major, minor, full version)"
# SQLite OSS - all tags
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:${TAG_SUFFIX}"
skopeo copy --all --retry-times 3 \
docker://$DOCKERHUB_IMAGE:$TAG_SUFFIX \
docker://$GHCR_IMAGE:$TAG_SUFFIX
done
# PostgreSQL OSS - all tags
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:postgresql-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:postgresql-${TAG_SUFFIX}"
skopeo copy --all --retry-times 3 \
docker://$DOCKERHUB_IMAGE:postgresql-$TAG_SUFFIX \
docker://$GHCR_IMAGE:postgresql-$TAG_SUFFIX
done
# SQLite Enterprise - all tags
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:ee-${TAG_SUFFIX}"
skopeo copy --all --retry-times 3 \
docker://$DOCKERHUB_IMAGE:ee-$TAG_SUFFIX \
docker://$GHCR_IMAGE:ee-$TAG_SUFFIX
done
# PostgreSQL Enterprise - all tags
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-postgresql-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:ee-postgresql-${TAG_SUFFIX}"
skopeo copy --all --retry-times 3 \
docker://$DOCKERHUB_IMAGE:ee-postgresql-$TAG_SUFFIX \
docker://$GHCR_IMAGE:ee-postgresql-$TAG_SUFFIX
done
fi
echo "All images copied successfully to GHCR!"
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
skopeo copy --all --retry-times 3 \
docker://$DOCKERHUB_IMAGE:$TAG \
docker://$GHCR_IMAGE:$TAG
shell: bash
- name: Login to GitHub Container Registry (for cosign)
@@ -440,62 +371,28 @@ jobs:
issuer="https://token.actions.githubusercontent.com"
id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
# Determine if this is an RC release
IS_RC="false"
if echo "$TAG" | grep -qE "rc[0-9]+$"; then
IS_RC="true"
fi
for IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
echo "Processing ${IMAGE}:${TAG}"
# Define image variants to sign
if [ "$IS_RC" = "true" ]; then
echo "RC release - signing version-specific tags only"
IMAGE_TAGS=(
"${TAG}"
"postgresql-${TAG}"
"ee-${TAG}"
"ee-postgresql-${TAG}"
)
else
echo "Regular release - signing all tags"
MAJOR_TAG=$(echo $TAG | cut -d. -f1)
MINOR_TAG=$(echo $TAG | cut -d. -f1,2)
IMAGE_TAGS=(
"latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"
"postgresql-latest" "postgresql-$MAJOR_TAG" "postgresql-$MINOR_TAG" "postgresql-$TAG"
"ee-latest" "ee-$MAJOR_TAG" "ee-$MINOR_TAG" "ee-$TAG"
"ee-postgresql-latest" "ee-postgresql-$MAJOR_TAG" "ee-postgresql-$MINOR_TAG" "ee-postgresql-$TAG"
)
fi
DIGEST="$(skopeo inspect --retry-times 3 docker://${IMAGE}:${TAG} | jq -r '.Digest')"
REF="${IMAGE}@${DIGEST}"
echo "Resolved digest: ${REF}"
# Sign each image variant for both registries
for BASE_IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
for IMAGE_TAG in "${IMAGE_TAGS[@]}"; do
echo "Processing ${BASE_IMAGE}:${IMAGE_TAG}"
echo "==> cosign sign (keyless) --recursive ${REF}"
cosign sign --recursive "${REF}"
DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${IMAGE_TAG} | jq -r '.Digest')"
REF="${BASE_IMAGE}@${DIGEST}"
echo "Resolved digest: ${REF}"
echo "==> cosign sign (key) --recursive ${REF}"
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
echo "==> cosign sign (keyless) --recursive ${REF}"
cosign sign --recursive "${REF}"
echo "==> cosign verify (public key) ${REF}"
cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text
echo "==> cosign sign (key) --recursive ${REF}"
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
echo "==> cosign verify (public key) ${REF}"
cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text
echo "==> cosign verify (keyless policy) ${REF}"
cosign verify \
--certificate-oidc-issuer "${issuer}" \
--certificate-identity-regexp "${id_regex}" \
"${REF}" -o text
echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}"
done
echo "==> cosign verify (keyless policy) ${REF}"
cosign verify \
--certificate-oidc-issuer "${issuer}" \
--certificate-identity-regexp "${id_regex}" \
"${REF}" -o text
done
echo "All images signed and verified successfully!"
shell: bash
post-run:

View File

@@ -1,426 +0,0 @@
name: CI/CD Pipeline
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
permissions:
contents: read
packages: write # for GHCR push
id-token: write # for Cosign Keyless (OIDC) Signing
# Required secrets:
# - DOCKER_HUB_USERNAME / DOCKER_HUB_ACCESS_TOKEN: push to Docker Hub
# - GITHUB_TOKEN: used for GHCR login and OIDC keyless signing
# - COSIGN_PRIVATE_KEY / COSIGN_PASSWORD / COSIGN_PUBLIC_KEY: for key-based signing
on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+"
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
pre-run:
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
aws-region: ${{ secrets.AWS_REGION }}
- name: Verify AWS identity
run: aws sts get-caller-identity
- name: Start EC2 instances
run: |
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
echo "EC2 instances started"
release-arm:
name: Build and Release (ARM64)
runs-on: [self-hosted, linux, arm64, us-east-1]
needs: [pre-run]
if: >-
${{
needs.pre-run.result == 'success'
}}
# Job-level timeout to avoid runaway or stuck runs
timeout-minutes: 120
env:
# Target images
DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Monitor storage space
run: |
THRESHOLD=75
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
echo "Used space: $USED_SPACE%"
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
echo "Used space is below the threshold of 75% free. Running Docker system prune."
echo y | docker system prune -a
else
echo "Storage space is above the threshold. No action needed."
fi
- name: Log in to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
shell: bash
- name: Update version in package.json
run: |
TAG=${{ env.TAG }}
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
cat server/lib/consts.ts
shell: bash
- name: Check if release candidate
id: check-rc
run: |
TAG=${{ env.TAG }}
if [[ "$TAG" == *"-rc."* ]]; then
echo "IS_RC=true" >> $GITHUB_ENV
else
echo "IS_RC=false" >> $GITHUB_ENV
fi
shell: bash
- name: Build and push Docker images (Docker Hub - ARM64)
run: |
TAG=${{ env.TAG }}
if [ "$IS_RC" = "true" ]; then
make build-rc-arm tag=$TAG
else
make build-release-arm tag=$TAG
fi
echo "Built & pushed ARM64 images to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
shell: bash
release-amd:
name: Build and Release (AMD64)
runs-on: [self-hosted, linux, x64, us-east-1]
needs: [pre-run]
if: >-
${{
needs.pre-run.result == 'success'
}}
# Job-level timeout to avoid runaway or stuck runs
timeout-minutes: 120
env:
# Target images
DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Monitor storage space
run: |
THRESHOLD=75
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
echo "Used space: $USED_SPACE%"
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
echo "Used space is below the threshold of 75% free. Running Docker system prune."
echo y | docker system prune -a
else
echo "Storage space is above the threshold. No action needed."
fi
- name: Log in to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
shell: bash
- name: Update version in package.json
run: |
TAG=${{ env.TAG }}
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
cat server/lib/consts.ts
shell: bash
- name: Check if release candidate
id: check-rc
run: |
TAG=${{ env.TAG }}
if [[ "$TAG" == *"-rc."* ]]; then
echo "IS_RC=true" >> $GITHUB_ENV
else
echo "IS_RC=false" >> $GITHUB_ENV
fi
shell: bash
- name: Build and push Docker images (Docker Hub - AMD64)
run: |
TAG=${{ env.TAG }}
if [ "$IS_RC" = "true" ]; then
make build-rc-amd tag=$TAG
else
make build-release-amd tag=$TAG
fi
echo "Built & pushed AMD64 images to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
shell: bash
create-manifest:
name: Create Multi-Arch Manifests
runs-on: [self-hosted, linux, x64, us-east-1]
needs: [release-arm, release-amd]
if: >-
${{
needs.release-arm.result == 'success' &&
needs.release-amd.result == 'success'
}}
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Log in to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
shell: bash
- name: Check if release candidate
id: check-rc
run: |
TAG=${{ env.TAG }}
if [[ "$TAG" == *"-rc."* ]]; then
echo "IS_RC=true" >> $GITHUB_ENV
else
echo "IS_RC=false" >> $GITHUB_ENV
fi
shell: bash
- name: Create multi-arch manifests
run: |
TAG=${{ env.TAG }}
if [ "$IS_RC" = "true" ]; then
make create-manifests-rc tag=$TAG
else
make create-manifests tag=$TAG
fi
echo "Created multi-arch manifests for tag: ${TAG}"
shell: bash
sign-and-package:
name: Sign and Package
runs-on: [self-hosted, linux, x64, us-east-1]
needs: [release-arm, release-amd, create-manifest]
if: >-
${{
needs.release-arm.result == 'success' &&
needs.release-amd.result == 'success' &&
needs.create-manifest.result == 'success'
}}
# Job-level timeout to avoid runaway or stuck runs
timeout-minutes: 120
env:
# Target images
DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
shell: bash
- name: Install Go
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
with:
go-version: 1.24
- name: Update version in package.json
run: |
TAG=${{ env.TAG }}
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
cat server/lib/consts.ts
shell: bash
- name: Pull latest Gerbil version
id: get-gerbil-tag
run: |
LATEST_TAG=$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name')
echo "LATEST_GERBIL_TAG=$LATEST_TAG" >> $GITHUB_ENV
shell: bash
- name: Pull latest Badger version
id: get-badger-tag
run: |
LATEST_TAG=$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name')
echo "LATEST_BADGER_TAG=$LATEST_TAG" >> $GITHUB_ENV
shell: bash
- name: Update install/main.go
run: |
PANGOLIN_VERSION=${{ env.TAG }}
GERBIL_VERSION=${{ env.LATEST_GERBIL_TAG }}
BADGER_VERSION=${{ env.LATEST_BADGER_TAG }}
sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"$PANGOLIN_VERSION\"/" install/main.go
sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"$GERBIL_VERSION\"/" install/main.go
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$BADGER_VERSION\"/" install/main.go
echo "Updated install/main.go with Pangolin version $PANGOLIN_VERSION, Gerbil version $GERBIL_VERSION, and Badger version $BADGER_VERSION"
cat install/main.go
shell: bash
- name: Build installer
working-directory: install
run: |
make go-build-release
- name: Upload artifacts from /install/bin
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: install-bin
path: install/bin/
- name: Install skopeo + jq
# skopeo: copy/inspect images between registries
# jq: JSON parsing tool used to extract digest values
run: |
sudo apt-get update -y
sudo apt-get install -y skopeo jq
skopeo --version
shell: bash
- name: Login to GHCR
env:
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
run: |
mkdir -p "$(dirname "$REGISTRY_AUTH_FILE")"
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
shell: bash
- name: Copy tag from Docker Hub to GHCR
# Mirror the already-built image (all architectures) to GHCR so we can sign it
# Wait a bit for both architectures to be available in Docker Hub manifest
env:
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
run: |
set -euo pipefail
TAG=${{ env.TAG }}
echo "Waiting for multi-arch manifest to be ready..."
sleep 30
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
skopeo copy --all --retry-times 3 \
docker://$DOCKERHUB_IMAGE:$TAG \
docker://$GHCR_IMAGE:$TAG
shell: bash
- name: Login to GitHub Container Registry (for cosign)
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install cosign
# cosign is used to sign and verify container images (key and keyless)
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Dual-sign and verify (GHCR & Docker Hub)
# Sign each image by digest using keyless (OIDC) and key-based signing,
# then verify both the public key signature and the keyless OIDC signature.
env:
TAG: ${{ env.TAG }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }}
COSIGN_YES: "true"
run: |
set -euo pipefail
issuer="https://token.actions.githubusercontent.com"
id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
for IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
echo "Processing ${IMAGE}:${TAG}"
DIGEST="$(skopeo inspect --retry-times 3 docker://${IMAGE}:${TAG} | jq -r '.Digest')"
REF="${IMAGE}@${DIGEST}"
echo "Resolved digest: ${REF}"
echo "==> cosign sign (keyless) --recursive ${REF}"
cosign sign --recursive "${REF}"
echo "==> cosign sign (key) --recursive ${REF}"
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
echo "==> cosign verify (public key) ${REF}"
cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text
echo "==> cosign verify (keyless policy) ${REF}"
cosign verify \
--certificate-oidc-issuer "${issuer}" \
--certificate-identity-regexp "${id_regex}" \
"${REF}" -o text
done
shell: bash
post-run:
needs: [pre-run, release-arm, release-amd, create-manifest, sign-and-package]
if: >-
${{
always() &&
needs.pre-run.result == 'success' &&
(needs.release-arm.result == 'success' || needs.release-arm.result == 'skipped' || needs.release-arm.result == 'failure') &&
(needs.release-amd.result == 'success' || needs.release-amd.result == 'skipped' || needs.release-amd.result == 'failure') &&
(needs.create-manifest.result == 'success' || needs.create-manifest.result == 'skipped' || needs.create-manifest.result == 'failure') &&
(needs.sign-and-package.result == 'success' || needs.sign-and-package.result == 'skipped' || needs.sign-and-package.result == 'failure')
}}
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
aws-region: ${{ secrets.AWS_REGION }}
- name: Verify AWS identity
run: aws sts get-caller-identity
- name: Stop EC2 instances
run: |
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
echo "EC2 instances stopped"

View File

@@ -1,125 +0,0 @@
name: CI/CD Pipeline
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
permissions:
contents: read
packages: write # for GHCR push
id-token: write # for Cosign Keyless (OIDC) Signing
on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+-s.[0-9]+"
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
pre-run:
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
aws-region: ${{ secrets.AWS_REGION }}
- name: Verify AWS identity
run: aws sts get-caller-identity
- name: Start EC2 instances
run: |
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
echo "EC2 instances started"
release-arm:
name: Build and Release (ARM64)
runs-on: [self-hosted, linux, arm64, us-east-1]
needs: [pre-run]
if: >-
${{
needs.pre-run.result == 'success'
}}
# Job-level timeout to avoid runaway or stuck runs
timeout-minutes: 120
env:
# Target images
AWS_IMAGE: ${{ secrets.aws_account_id }}.dkr.ecr.us-east-1.amazonaws.com/${{ github.event.repository.name }}
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Monitor storage space
run: |
THRESHOLD=75
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
echo "Used space: $USED_SPACE%"
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
echo "Used space is below the threshold of 75% free. Running Docker system prune."
echo y | docker system prune -a
else
echo "Storage space is above the threshold. No action needed."
fi
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
aws-region: ${{ secrets.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
shell: bash
- name: Update version in package.json
run: |
TAG=${{ env.TAG }}
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
cat server/lib/consts.ts
shell: bash
- name: Build and push Docker images (Docker Hub - ARM64)
run: |
TAG=${{ env.TAG }}
make build-saas tag=$TAG
echo "Built & pushed ARM64 images to: ${{ env.AWS_IMAGE }}:${TAG}"
shell: bash
post-run:
needs: [pre-run, release-arm]
if: >-
${{
always() &&
needs.pre-run.result == 'success' &&
(needs.release-arm.result == 'success' || needs.release-arm.result == 'skipped' || needs.release-arm.result == 'failure')
}}
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
aws-region: ${{ secrets.AWS_REGION }}
- name: Verify AWS identity
run: aws sts get-caller-identity
- name: Stop EC2 instances
run: |
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
echo "EC2 instances stopped"

3
.gitignore vendored
View File

@@ -50,5 +50,4 @@ dynamic/
*.mmdb
scratch/
tsconfig.json
hydrateSaas.ts
CLAUDE.md
hydrateSaas.ts

View File

@@ -1,20 +1,10 @@
FROM node:24-alpine AS builder
# OCI Image Labels - Build Args for dynamic values
ARG VERSION="dev"
ARG REVISION=""
ARG CREATED=""
ARG LICENSE="AGPL-3.0"
WORKDIR /app
ARG BUILD=oss
ARG DATABASE=sqlite
# Derive title and description based on BUILD type
ARG IMAGE_TITLE="Pangolin"
ARG IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere"
RUN apk add --no-cache curl tzdata python3 make g++
# COPY package.json package-lock.json ./
@@ -79,17 +69,4 @@ RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
COPY server/db/names.json ./dist/names.json
COPY public ./public
# OCI Image Labels
# https://github.com/opencontainers/image-spec/blob/main/annotations.md
LABEL org.opencontainers.image.source="https://github.com/fosrl/pangolin" \
org.opencontainers.image.url="https://github.com/fosrl/pangolin" \
org.opencontainers.image.documentation="https://docs.pangolin.net" \
org.opencontainers.image.vendor="Fossorial" \
org.opencontainers.image.licenses="${LICENSE}" \
org.opencontainers.image.title="${IMAGE_TITLE}" \
org.opencontainers.image.description="${IMAGE_DESCRIPTION}" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${REVISION}" \
org.opencontainers.image.created="${CREATED}"
CMD ["npm", "run", "start"]

205
Makefile
View File

@@ -3,25 +3,6 @@
major_tag := $(shell echo $(tag) | cut -d. -f1)
minor_tag := $(shell echo $(tag) | cut -d. -f1,2)
# OCI label variables
CREATED := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
REVISION := $(shell git rev-parse HEAD 2>/dev/null || echo "unknown")
# Common OCI build args for OSS builds
OCI_ARGS_OSS = --build-arg VERSION=$(tag) \
--build-arg REVISION=$(REVISION) \
--build-arg CREATED=$(CREATED) \
--build-arg IMAGE_TITLE="Pangolin" \
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere"
# Common OCI build args for Enterprise builds
OCI_ARGS_EE = --build-arg VERSION=$(tag) \
--build-arg REVISION=$(REVISION) \
--build-arg CREATED=$(CREATED) \
--build-arg LICENSE="Fossorial Commercial" \
--build-arg IMAGE_TITLE="Pangolin EE" \
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere"
.PHONY: build-release build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
build-release: build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
@@ -34,7 +15,6 @@ build-sqlite:
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=sqlite \
$(OCI_ARGS_OSS) \
--platform linux/arm64,linux/amd64 \
--tag fosrl/pangolin:latest \
--tag fosrl/pangolin:$(major_tag) \
@@ -50,7 +30,6 @@ build-postgresql:
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=pg \
$(OCI_ARGS_OSS) \
--platform linux/arm64,linux/amd64 \
--tag fosrl/pangolin:postgresql-latest \
--tag fosrl/pangolin:postgresql-$(major_tag) \
@@ -66,7 +45,6 @@ build-ee-sqlite:
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=sqlite \
$(OCI_ARGS_EE) \
--platform linux/arm64,linux/amd64 \
--tag fosrl/pangolin:ee-latest \
--tag fosrl/pangolin:ee-$(major_tag) \
@@ -82,7 +60,6 @@ build-ee-postgresql:
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=pg \
$(OCI_ARGS_EE) \
--platform linux/arm64,linux/amd64 \
--tag fosrl/pangolin:ee-postgresql-latest \
--tag fosrl/pangolin:ee-postgresql-$(major_tag) \
@@ -90,18 +67,6 @@ build-ee-postgresql:
--tag fosrl/pangolin:ee-postgresql-$(tag) \
--push .
build-saas:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
exit 1; \
fi
docker buildx build \
--build-arg BUILD=saas \
--build-arg DATABASE=pg \
--platform linux/arm64 \
--tag $(AWS_IMAGE):$(tag) \
--push .
build-release-arm:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-release-arm tag=<tag>"; \
@@ -109,16 +74,9 @@ build-release-arm:
fi
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=sqlite \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg IMAGE_TITLE="Pangolin" \
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/arm64 \
--tag fosrl/pangolin:latest-arm64 \
--tag fosrl/pangolin:$$MAJOR_TAG-arm64 \
@@ -128,11 +86,6 @@ build-release-arm:
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=pg \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg IMAGE_TITLE="Pangolin" \
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/arm64 \
--tag fosrl/pangolin:postgresql-latest-arm64 \
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-arm64 \
@@ -142,12 +95,6 @@ build-release-arm:
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=sqlite \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg LICENSE="Fossorial Commercial" \
--build-arg IMAGE_TITLE="Pangolin EE" \
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/arm64 \
--tag fosrl/pangolin:ee-latest-arm64 \
--tag fosrl/pangolin:ee-$$MAJOR_TAG-arm64 \
@@ -157,12 +104,6 @@ build-release-arm:
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=pg \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg LICENSE="Fossorial Commercial" \
--build-arg IMAGE_TITLE="Pangolin EE" \
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/arm64 \
--tag fosrl/pangolin:ee-postgresql-latest-arm64 \
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-arm64 \
@@ -177,16 +118,9 @@ build-release-amd:
fi
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=sqlite \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg IMAGE_TITLE="Pangolin" \
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/amd64 \
--tag fosrl/pangolin:latest-amd64 \
--tag fosrl/pangolin:$$MAJOR_TAG-amd64 \
@@ -196,11 +130,6 @@ build-release-amd:
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=pg \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg IMAGE_TITLE="Pangolin" \
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/amd64 \
--tag fosrl/pangolin:postgresql-latest-amd64 \
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-amd64 \
@@ -210,12 +139,6 @@ build-release-amd:
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=sqlite \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg LICENSE="Fossorial Commercial" \
--build-arg IMAGE_TITLE="Pangolin EE" \
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/amd64 \
--tag fosrl/pangolin:ee-latest-amd64 \
--tag fosrl/pangolin:ee-$$MAJOR_TAG-amd64 \
@@ -225,12 +148,6 @@ build-release-amd:
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=pg \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg LICENSE="Fossorial Commercial" \
--build-arg IMAGE_TITLE="Pangolin EE" \
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/amd64 \
--tag fosrl/pangolin:ee-postgresql-latest-amd64 \
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-amd64 \
@@ -284,51 +201,27 @@ build-rc:
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
exit 1; \
fi
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=sqlite \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg IMAGE_TITLE="Pangolin" \
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/arm64,linux/amd64 \
--tag fosrl/pangolin:$(tag) \
--push . && \
--push .
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=pg \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg IMAGE_TITLE="Pangolin" \
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/arm64,linux/amd64 \
--tag fosrl/pangolin:postgresql-$(tag) \
--push . && \
--push .
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=sqlite \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg LICENSE="Fossorial Commercial" \
--build-arg IMAGE_TITLE="Pangolin EE" \
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/arm64,linux/amd64 \
--tag fosrl/pangolin:ee-$(tag) \
--push . && \
--push .
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=pg \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg LICENSE="Fossorial Commercial" \
--build-arg IMAGE_TITLE="Pangolin EE" \
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/arm64,linux/amd64 \
--tag fosrl/pangolin:ee-postgresql-$(tag) \
--push .
@@ -338,51 +231,27 @@ build-rc-arm:
echo "Error: tag is required. Usage: make build-rc-arm tag=<tag>"; \
exit 1; \
fi
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=sqlite \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg IMAGE_TITLE="Pangolin" \
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/arm64 \
--tag fosrl/pangolin:$(tag)-arm64 \
--push . && \
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=pg \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg IMAGE_TITLE="Pangolin" \
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/arm64 \
--tag fosrl/pangolin:postgresql-$(tag)-arm64 \
--push . && \
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=sqlite \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg LICENSE="Fossorial Commercial" \
--build-arg IMAGE_TITLE="Pangolin EE" \
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/arm64 \
--tag fosrl/pangolin:ee-$(tag)-arm64 \
--push . && \
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=pg \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg LICENSE="Fossorial Commercial" \
--build-arg IMAGE_TITLE="Pangolin EE" \
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/arm64 \
--tag fosrl/pangolin:ee-postgresql-$(tag)-arm64 \
--push .
@@ -392,51 +261,27 @@ build-rc-amd:
echo "Error: tag is required. Usage: make build-rc-amd tag=<tag>"; \
exit 1; \
fi
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=sqlite \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg IMAGE_TITLE="Pangolin" \
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/amd64 \
--tag fosrl/pangolin:$(tag)-amd64 \
--push . && \
docker buildx build \
--build-arg BUILD=oss \
--build-arg DATABASE=pg \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg IMAGE_TITLE="Pangolin" \
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/amd64 \
--tag fosrl/pangolin:postgresql-$(tag)-amd64 \
--push . && \
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=sqlite \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg LICENSE="Fossorial Commercial" \
--build-arg IMAGE_TITLE="Pangolin EE" \
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/amd64 \
--tag fosrl/pangolin:ee-$(tag)-amd64 \
--push . && \
docker buildx build \
--build-arg BUILD=enterprise \
--build-arg DATABASE=pg \
--build-arg VERSION=$(tag) \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg LICENSE="Fossorial Commercial" \
--build-arg IMAGE_TITLE="Pangolin EE" \
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/amd64 \
--tag fosrl/pangolin:ee-postgresql-$(tag)-amd64 \
--push .
@@ -469,52 +314,16 @@ create-manifests-rc:
echo "All RC multi-arch manifests created successfully!"
build-arm:
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
docker buildx build \
--build-arg VERSION=dev \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg IMAGE_TITLE="Pangolin" \
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/arm64 \
-t fosrl/pangolin:latest .
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
build-x86:
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
docker buildx build \
--build-arg VERSION=dev \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg IMAGE_TITLE="Pangolin" \
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
--platform linux/amd64 \
-t fosrl/pangolin:latest .
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
dev-build-sqlite:
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
docker build \
--build-arg DATABASE=sqlite \
--build-arg VERSION=dev \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg IMAGE_TITLE="Pangolin" \
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
-t fosrl/pangolin:latest .
docker build --build-arg DATABASE=sqlite -t fosrl/pangolin:latest .
dev-build-pg:
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
docker build \
--build-arg DATABASE=pg \
--build-arg VERSION=dev \
--build-arg REVISION=$$REVISION \
--build-arg CREATED=$$CREATED \
--build-arg IMAGE_TITLE="Pangolin" \
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
-t fosrl/pangolin:postgresql-latest .
docker build --build-arg DATABASE=pg -t fosrl/pangolin:postgresql-latest .
test:
docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest

View File

@@ -1,9 +1,5 @@
http:
middlewares:
badger:
plugin:
badger:
disableForwardAuth: true
redirect-to-https:
redirectScheme:
scheme: https
@@ -17,7 +13,6 @@ http:
- web
middlewares:
- redirect-to-https
- badger
# Next.js router (handles everything except API and WebSocket paths)
next-router:
@@ -26,8 +21,6 @@ http:
priority: 10
entryPoints:
- websecure
middlewares:
- badger
tls:
certResolver: letsencrypt
@@ -38,8 +31,6 @@ http:
priority: 100
entryPoints:
- websecure
middlewares:
- badger
tls:
certResolver: letsencrypt

View File

@@ -43,12 +43,9 @@ entryPoints:
http:
tls:
certResolver: "letsencrypt"
encodedCharacters:
allowEncodedSlash: true
allowEncodedQuestionMark: true
serversTransport:
insecureSkipVerify: true
ping:
entryPoint: "web"
entryPoint: "web"

View File

@@ -340,7 +340,7 @@ func collectUserInput(reader *bufio.Reader) Config {
// Basic configuration
fmt.Println("\n=== Basic Configuration ===")
config.IsEnterprise = readBoolNoDefault(reader, "Do you want to install the Enterprise version of Pangolin? The EE is free for personal use or for businesses making less than 100k USD annually.")
config.IsEnterprise = readBoolNoDefault(reader, "Do you want to install the Enterprise version of Pangolin? The EE is free for persoal use or for businesses making less than 100k USD annually.")
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")

View File

@@ -257,6 +257,8 @@
"accessRolesSearch": "Търсене на роли...",
"accessRolesAdd": "Добавете роля",
"accessRoleDelete": "Изтриване на роля",
"accessApprovalsManage": "Manage Approvals",
"accessApprovalsDescription": "Manage approval requests in the organization",
"description": "Описание",
"inviteTitle": "Отворени покани",
"inviteDescription": "Управлявайте покани за други потребители да се присъединят към организацията",
@@ -450,6 +452,18 @@
"selectDuration": "Изберете продължителност",
"selectResource": "Изберете Ресурс",
"filterByResource": "Филтрирай По Ресурс",
"selectApprovalState": "Select Approval State",
"filterByApprovalState": "Filter By Approval State",
"approvalListEmpty": "No approvals",
"approvalState": "Approval State",
"approve": "Approve",
"approved": "Approved",
"denied": "Denied",
"deniedApproval": "Denied Approval",
"all": "All",
"deny": "Deny",
"viewDetails": "View Details",
"requestingNewDeviceApproval": "requested a new device",
"resetFilters": "Нулиране на Филтрите",
"totalBlocked": "Заявки Блокирани От Pangolin",
"totalRequests": "Общо Заявки",
@@ -729,16 +743,28 @@
"countries": "Държави",
"accessRoleCreate": "Създайте роля",
"accessRoleCreateDescription": "Създайте нова роля за групиране на потребители и управление на техните разрешения.",
"accessRoleEdit": "Edit Role",
"accessRoleEditDescription": "Edit role information.",
"accessRoleCreateSubmit": "Създайте роля",
"accessRoleCreated": "Ролята е създадена",
"accessRoleCreatedDescription": "Ролята беше успешно създадена.",
"accessRoleErrorCreate": "Неуспешно създаване на роля",
"accessRoleErrorCreateDescription": "Възникна грешка при създаването на ролята.",
"accessRoleUpdateSubmit": "Update Role",
"accessRoleUpdated": "Role updated",
"accessRoleUpdatedDescription": "The role has been successfully updated.",
"accessApprovalUpdated": "Approval processed",
"accessApprovalApprovedDescription": "Set Approval Request decision to approved.",
"accessApprovalDeniedDescription": "Set Approval Request decision to denied.",
"accessRoleErrorUpdate": "Failed to update role",
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
"accessApprovalErrorUpdate": "Failed to process approval",
"accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.",
"accessRoleErrorNewRequired": "Нова роля е необходима",
"accessRoleErrorRemove": "Неуспешно премахване на роля",
"accessRoleErrorRemoveDescription": "Възникна грешка при премахването на роля.",
"accessRoleName": "Име на роля",
"accessRoleQuestionRemove": "Ще изтриете ролята {name}. Не можете да отмените това действие.",
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
"accessRoleRemove": "Премахни роля",
"accessRoleRemoveDescription": "Премахни роля от организацията",
"accessRoleRemoveSubmit": "Премахни роля",
@@ -874,7 +900,7 @@
"inviteAlready": "Изглежда, че сте били поканени!",
"inviteAlreadyDescription": "За да приемете поканата, трябва да влезете или да създадете акаунт.",
"signupQuestion": "Вече имате акаунт?",
"login": "Влизане",
"login": "Log In",
"resourceNotFound": "Ресурсът не е намерен",
"resourceNotFoundDescription": "Ресурсът, който се опитвате да достъпите, не съществува.",
"pincodeRequirementsLength": "ПИН трябва да бъде точно 6 цифри",
@@ -954,13 +980,13 @@
"passwordExpiryDescription": "Тази организация изисква да сменяте паролата си на всеки {maxDays} дни.",
"changePasswordNow": "Сменете паролата сега",
"pincodeAuth": "Код на удостоверителя",
"pincodeSubmit2": "Изпрати код",
"pincodeSubmit2": "Submit code",
"passwordResetSubmit": "Заявка за нулиране",
"passwordResetAlreadyHaveCode": "Въведете код.",
"passwordResetSmtpRequired": "Моля, свържете се с вашия администратор",
"passwordResetSmtpRequiredDescription": "Кодът за нулиране на парола е задължителен за нулиране на паролата ви. Моля, свържете се с вашия администратор за помощ.",
"passwordBack": "Назад към Парола",
"loginBack": "Връщане към вход",
"loginBack": "Go back to main login page",
"signup": "Регистрация",
"loginStart": "Влезте, за да започнете",
"idpOidcTokenValidating": "Валидиране на OIDC токен",
@@ -1118,6 +1144,10 @@
"actionUpdateIdpOrg": "Актуализиране на IdP организация",
"actionCreateClient": "Създаване на клиент",
"actionDeleteClient": "Изтриване на клиент",
"actionArchiveClient": "Archive Client",
"actionUnarchiveClient": "Unarchive Client",
"actionBlockClient": "Block Client",
"actionUnblockClient": "Unblock Client",
"actionUpdateClient": "Актуализиране на клиент",
"actionListClients": "Списък с клиенти",
"actionGetClient": "Получаване на клиент",
@@ -1134,14 +1164,14 @@
"searchProgress": "Търсене...",
"create": "Създаване",
"orgs": "Организации",
"loginError": "Възникна грешка при влизане",
"loginRequiredForDevice": "Необходим е вход за удостоверяване на вашето устройство.",
"loginError": "An unexpected error occurred. Please try again.",
"loginRequiredForDevice": "Login is required for your device.",
"passwordForgot": "Забравена парола?",
"otpAuth": "Двуфакторно удостоверяване",
"otpAuthDescription": "Въведете кода от приложението за удостоверяване или един от вашите резервни кодове за еднократна употреба.",
"otpAuthSubmit": "Изпрати код",
"idpContinue": "Или продължете със",
"otpAuthBack": "Назад към Вход",
"otpAuthBack": "Back to Password",
"navbar": "Навигационно меню",
"navbarDescription": "Главно навигационно меню за приложението",
"navbarDocsLink": "Документация",
@@ -1189,6 +1219,7 @@
"sidebarOverview": "Общ преглед",
"sidebarHome": "Начало",
"sidebarSites": "Сайтове",
"sidebarApprovals": "Approval Requests",
"sidebarResources": "Ресурси",
"sidebarProxyResources": "Публично",
"sidebarClientResources": "Частно",
@@ -1205,7 +1236,7 @@
"sidebarIdentityProviders": "Идентификационни доставчици",
"sidebarLicense": "Лиценз",
"sidebarClients": "Клиенти",
"sidebarUserDevices": "Потребители",
"sidebarUserDevices": "User Devices",
"sidebarMachineClients": "Машини",
"sidebarDomains": "Домейни",
"sidebarGeneral": "Управление.",
@@ -1277,6 +1308,7 @@
"setupErrorCreateAdmin": "Възникна грешка при създаване на админ акаунт.",
"certificateStatus": "Статус на сертификата",
"loading": "Зареждане",
"loadingAnalytics": "Loading Analytics",
"restart": "Рестарт",
"domains": "Домейни",
"domainsDescription": "Създайте и управлявайте наличните домейни в организацията",
@@ -1304,6 +1336,7 @@
"refreshError": "Неуспешно обновяване на данни",
"verified": "Потвърдено",
"pending": "Чакащо",
"pendingApproval": "Pending Approval",
"sidebarBilling": "Фактуриране",
"billing": "Фактуриране",
"orgBillingDescription": "Управлявайте информацията за плащане и абонаментите",
@@ -1420,7 +1453,7 @@
"securityKeyRemoveSuccess": "Ключът за защита е премахнат успешно",
"securityKeyRemoveError": "Неуспешно премахване на ключ за защита",
"securityKeyLoadError": "Неуспешно зареждане на ключове за защита",
"securityKeyLogin": "Продължете с ключа за сигурност",
"securityKeyLogin": "Use Security Key",
"securityKeyAuthError": "Неуспешно удостоверяване с ключ за сигурност",
"securityKeyRecommendation": "Регистрирайте резервен ключ за безопасност на друго устройство, за да сте сигурни, че винаги ще имате достъп до профила си",
"registering": "Регистрация...",
@@ -1547,6 +1580,8 @@
"IntervalSeconds": "Интервал за здраве",
"timeoutSeconds": "Време за изчакване (сек)",
"timeIsInSeconds": "Времето е в секунди",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need their devices approved by an admin before they can access resources",
"retryAttempts": "Опити за повторно",
"expectedResponseCodes": "Очаквани кодове за отговор",
"expectedResponseCodesDescription": "HTTP статус код, указващ здравословно състояние. Ако бъде оставено празно, между 200-300 се счита за здравословно.",
@@ -1876,7 +1911,7 @@
"orgAuthChooseIdpDescription": "Изберете своя доставчик на идентичност, за да продължите",
"orgAuthNoIdpConfigured": "Тази организация няма конфигурирани доставчици на идентичност. Можете да влезете с вашата Pangolin идентичност.",
"orgAuthSignInWithPangolin": "Впишете се с Pangolin",
"orgAuthSignInToOrg": "Влезте в организация.",
"orgAuthSignInToOrg": "Sign in to an organization",
"orgAuthSelectOrgTitle": "Вход в организация.",
"orgAuthSelectOrgDescription": "Въведете идентификатора на вашата организация, за да продължите.",
"orgAuthOrgIdPlaceholder": "вашата-организация",
@@ -2232,6 +2267,8 @@
"deviceCodeInvalidFormat": "Кодът трябва да бъде 9 символа (напр. A1AJ-N5JD)",
"deviceCodeInvalidOrExpired": "Невалиден или изтекъл код",
"deviceCodeVerifyFailed": "Неуспешна проверка на кода на устройството",
"deviceCodeValidating": "Validating device code...",
"deviceCodeVerifying": "Verifying device authorization...",
"signedInAs": "Вписан като",
"deviceCodeEnterPrompt": "Въведете кода, показан на устройството",
"continue": "Продължете",
@@ -2244,7 +2281,7 @@
"deviceOrganizationsAccess": "Достъп до всички организации, до които има достъп акаунтът ви",
"deviceAuthorize": "Разрешете {applicationName}",
"deviceConnected": "Устройството е свързано!",
"deviceAuthorizedMessage": "Устройството е разрешено да има достъп до вашия акаунт.",
"deviceAuthorizedMessage": "Device is authorized to access your account. Please return to the client application.",
"pangolinCloud": "Pangolin Cloud",
"viewDevices": "Преглед на устройствата",
"viewDevicesDescription": "Управлявайте свързаните си устройства",
@@ -2306,6 +2343,7 @@
"identifier": "Идентификатор",
"deviceLoginUseDifferentAccount": "Не сте вие? Използвайте друг акаунт.",
"deviceLoginDeviceRequestingAccessToAccount": "Устройство запитващо достъп до този акаунт.",
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
"noData": "Няма Данни",
"machineClients": "Машинни клиенти",
"install": "Инсталирай",
@@ -2394,5 +2432,56 @@
"maintenanceScreenTitle": "Услугата временно недостъпна.",
"maintenanceScreenMessage": "В момента срещаме технически затруднения. Моля, проверете отново скоро.",
"maintenanceScreenEstimatedCompletion": "Прогнозно завършване:",
"createInternalResourceDialogDestinationRequired": "Дестинацията е задължителна."
"createInternalResourceDialogDestinationRequired": "Дестинацията е задължителна.",
"available": "Available",
"archived": "Archived",
"noArchivedDevices": "No archived devices found",
"deviceArchived": "Device archived",
"deviceArchivedDescription": "The device has been successfully archived.",
"errorArchivingDevice": "Error archiving device",
"failedToArchiveDevice": "Failed to archive device",
"deviceQuestionArchive": "Are you sure you want to archive this device?",
"deviceMessageArchive": "The device will be archived and removed from your active devices list.",
"deviceArchiveConfirm": "Archive Device",
"archiveDevice": "Archive Device",
"archive": "Archive",
"deviceUnarchived": "Device unarchived",
"deviceUnarchivedDescription": "The device has been successfully unarchived.",
"errorUnarchivingDevice": "Error unarchiving device",
"failedToUnarchiveDevice": "Failed to unarchive device",
"unarchive": "Unarchive",
"archiveClient": "Archive Client",
"archiveClientQuestion": "Are you sure you want to archive this client?",
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
"archiveClientConfirm": "Archive Client",
"blockClient": "Block Client",
"blockClientQuestion": "Are you sure you want to block this client?",
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
"blockClientConfirm": "Block Client",
"active": "Active",
"usernameOrEmail": "Username or Email",
"selectYourOrganization": "Select your organization",
"signInTo": "Log in in to",
"signInWithPassword": "Continue with Password",
"noAuthMethodsAvailable": "No authentication methods available for this organization.",
"enterPassword": "Enter your password",
"enterMfaCode": "Enter the code from your authenticator app",
"securityKeyRequired": "Please use your security key to sign in.",
"needToUseAnotherAccount": "Need to use a different account?",
"loginLegalDisclaimer": "By clicking the buttons below, you acknowledge you have read, understand, and agree to the <termsOfService>Terms of Service</termsOfService> and <privacyPolicy>Privacy Policy</privacyPolicy>.",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"userNotFoundWithUsername": "No user found with that username.",
"verify": "Verify",
"signIn": "Sign In",
"forgotPassword": "Forgot password?",
"orgSignInTip": "If you've logged in before, you can enter your username or email above to authenticate with your organization's identity provider instead. It's easier!",
"continueAnyway": "Continue anyway",
"dontShowAgain": "Don't show again",
"orgSignInNotice": "Did you know?",
"signupOrgNotice": "Trying to sign in?",
"signupOrgTip": "Are you trying to sign in through your organization's identity provider?",
"signupOrgLink": "Sign in or sign up with your organization instead",
"verifyEmailLogInWithDifferentAccount": "Use a Different Account",
"logIn": "Log In"
}

View File

@@ -257,6 +257,8 @@
"accessRolesSearch": "Hledat role...",
"accessRolesAdd": "Přidat roli",
"accessRoleDelete": "Odstranit roli",
"accessApprovalsManage": "Manage Approvals",
"accessApprovalsDescription": "Manage approval requests in the organization",
"description": "L 343, 22.12.2009, s. 1).",
"inviteTitle": "Otevřít pozvánky",
"inviteDescription": "Spravovat pozvánky pro ostatní uživatele do organizace",
@@ -450,6 +452,18 @@
"selectDuration": "Vyberte dobu trvání",
"selectResource": "Vybrat dokument",
"filterByResource": "Filtrovat podle zdroje",
"selectApprovalState": "Select Approval State",
"filterByApprovalState": "Filter By Approval State",
"approvalListEmpty": "No approvals",
"approvalState": "Approval State",
"approve": "Approve",
"approved": "Approved",
"denied": "Denied",
"deniedApproval": "Denied Approval",
"all": "All",
"deny": "Deny",
"viewDetails": "View Details",
"requestingNewDeviceApproval": "requested a new device",
"resetFilters": "Resetovat filtry",
"totalBlocked": "Požadavky blokovány Pangolinem",
"totalRequests": "Celkem požadavků",
@@ -729,16 +743,28 @@
"countries": "Země",
"accessRoleCreate": "Vytvořit roli",
"accessRoleCreateDescription": "Vytvořte novou roli pro seskupení uživatelů a spravujte jejich oprávnění.",
"accessRoleEdit": "Edit Role",
"accessRoleEditDescription": "Edit role information.",
"accessRoleCreateSubmit": "Vytvořit roli",
"accessRoleCreated": "Role vytvořena",
"accessRoleCreatedDescription": "Role byla úspěšně vytvořena.",
"accessRoleErrorCreate": "Nepodařilo se vytvořit roli",
"accessRoleErrorCreateDescription": "Došlo k chybě při vytváření role.",
"accessRoleUpdateSubmit": "Update Role",
"accessRoleUpdated": "Role updated",
"accessRoleUpdatedDescription": "The role has been successfully updated.",
"accessApprovalUpdated": "Approval processed",
"accessApprovalApprovedDescription": "Set Approval Request decision to approved.",
"accessApprovalDeniedDescription": "Set Approval Request decision to denied.",
"accessRoleErrorUpdate": "Failed to update role",
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
"accessApprovalErrorUpdate": "Failed to process approval",
"accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.",
"accessRoleErrorNewRequired": "Je vyžadována nová role",
"accessRoleErrorRemove": "Nepodařilo se odstranit roli",
"accessRoleErrorRemoveDescription": "Došlo k chybě při odstraňování role.",
"accessRoleName": "Název role",
"accessRoleQuestionRemove": "Chystáte se odstranit {name} roli. Tuto akci nelze vrátit zpět.",
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
"accessRoleRemove": "Odstranit roli",
"accessRoleRemoveDescription": "Odebrat roli z organizace",
"accessRoleRemoveSubmit": "Odstranit roli",
@@ -874,7 +900,7 @@
"inviteAlready": "Vypadá to, že jste byli pozváni!",
"inviteAlreadyDescription": "Chcete-li přijmout pozvánku, musíte se přihlásit nebo vytvořit účet.",
"signupQuestion": "Již máte účet?",
"login": "Přihlásit se",
"login": "Log In",
"resourceNotFound": "Zdroj nebyl nalezen",
"resourceNotFoundDescription": "Dokument, ke kterému se snažíte přistupovat, neexistuje.",
"pincodeRequirementsLength": "PIN musí být přesně 6 číslic",
@@ -954,13 +980,13 @@
"passwordExpiryDescription": "Tato organizace vyžaduje změnu hesla každých {maxDays} dní.",
"changePasswordNow": "Změnit heslo",
"pincodeAuth": "Ověřovací kód",
"pincodeSubmit2": "Odeslat kód",
"pincodeSubmit2": "Submit code",
"passwordResetSubmit": "Žádost o obnovení",
"passwordResetAlreadyHaveCode": "Zadejte kód",
"passwordResetSmtpRequired": "Obraťte se na správce",
"passwordResetSmtpRequiredDescription": "Pro obnovení hesla je vyžadován kód pro obnovení hesla. Kontaktujte prosím svého administrátora.",
"passwordBack": "Zpět na heslo",
"loginBack": "Přejít zpět na přihlášení",
"loginBack": "Go back to main login page",
"signup": "Zaregistrovat se",
"loginStart": "Přihlaste se a začněte",
"idpOidcTokenValidating": "Ověřování OIDC tokenu",
@@ -1118,6 +1144,10 @@
"actionUpdateIdpOrg": "Aktualizovat IDP Org",
"actionCreateClient": "Vytvořit klienta",
"actionDeleteClient": "Odstranit klienta",
"actionArchiveClient": "Archive Client",
"actionUnarchiveClient": "Unarchive Client",
"actionBlockClient": "Block Client",
"actionUnblockClient": "Unblock Client",
"actionUpdateClient": "Aktualizovat klienta",
"actionListClients": "Seznam klientů",
"actionGetClient": "Získat klienta",
@@ -1134,14 +1164,14 @@
"searchProgress": "Hledat...",
"create": "Vytvořit",
"orgs": "Organizace",
"loginError": "Při přihlášení došlo k chybě",
"loginRequiredForDevice": "Pro ověření vašeho zařízení je nutné se přihlásit.",
"loginError": "An unexpected error occurred. Please try again.",
"loginRequiredForDevice": "Login is required for your device.",
"passwordForgot": "Zapomněli jste heslo?",
"otpAuth": "Dvoufaktorové ověření",
"otpAuthDescription": "Zadejte kód z vaší autentizační aplikace nebo jeden z vlastních záložních kódů.",
"otpAuthSubmit": "Odeslat kód",
"idpContinue": "Nebo pokračovat s",
"otpAuthBack": "Zpět na přihlášení",
"otpAuthBack": "Back to Password",
"navbar": "Navigation Menu",
"navbarDescription": "Hlavní navigační menu aplikace",
"navbarDocsLink": "Dokumentace",
@@ -1189,6 +1219,7 @@
"sidebarOverview": "Přehled",
"sidebarHome": "Domů",
"sidebarSites": "Stránky",
"sidebarApprovals": "Approval Requests",
"sidebarResources": "Zdroje",
"sidebarProxyResources": "Veřejnost",
"sidebarClientResources": "Soukromé",
@@ -1205,7 +1236,7 @@
"sidebarIdentityProviders": "Poskytovatelé identity",
"sidebarLicense": "Licence",
"sidebarClients": "Klienti",
"sidebarUserDevices": "Uživatelé",
"sidebarUserDevices": "User Devices",
"sidebarMachineClients": "Stroje a přístroje",
"sidebarDomains": "Domény",
"sidebarGeneral": "Spravovat",
@@ -1277,6 +1308,7 @@
"setupErrorCreateAdmin": "Došlo k chybě při vytváření účtu správce serveru.",
"certificateStatus": "Stav certifikátu",
"loading": "Načítání",
"loadingAnalytics": "Loading Analytics",
"restart": "Restartovat",
"domains": "Domény",
"domainsDescription": "Vytvořit a spravovat domény dostupné v organizaci",
@@ -1304,6 +1336,7 @@
"refreshError": "Obnovení dat se nezdařilo",
"verified": "Ověřeno",
"pending": "Nevyřízeno",
"pendingApproval": "Pending Approval",
"sidebarBilling": "Fakturace",
"billing": "Fakturace",
"orgBillingDescription": "Spravovat fakturační informace a předplatné",
@@ -1420,7 +1453,7 @@
"securityKeyRemoveSuccess": "Bezpečnostní klíč byl úspěšně odstraněn",
"securityKeyRemoveError": "Odstranění bezpečnostního klíče se nezdařilo",
"securityKeyLoadError": "Nepodařilo se načíst bezpečnostní klíče",
"securityKeyLogin": "Pokračovat s bezpečnostním klíčem",
"securityKeyLogin": "Use Security Key",
"securityKeyAuthError": "Ověření bezpečnostním klíčem se nezdařilo",
"securityKeyRecommendation": "Registrujte záložní bezpečnostní klíč na jiném zařízení, abyste zajistili, že budete mít vždy přístup ke svému účtu.",
"registering": "Registrace...",
@@ -1547,6 +1580,8 @@
"IntervalSeconds": "Interval zdraví",
"timeoutSeconds": "Časový limit (sek)",
"timeIsInSeconds": "Čas je v sekundách",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need their devices approved by an admin before they can access resources",
"retryAttempts": "Opakovat pokusy",
"expectedResponseCodes": "Očekávané kódy odezvy",
"expectedResponseCodesDescription": "HTTP kód stavu, který označuje zdravý stav. Ponecháte-li prázdné, 200-300 je považováno za zdravé.",
@@ -1876,7 +1911,7 @@
"orgAuthChooseIdpDescription": "Chcete-li pokračovat, vyberte svého poskytovatele identity",
"orgAuthNoIdpConfigured": "Tato organizace nemá nakonfigurovány žádné poskytovatele identity. Místo toho se můžete přihlásit s vaší Pangolinovou identitou.",
"orgAuthSignInWithPangolin": "Přihlásit se pomocí Pangolinu",
"orgAuthSignInToOrg": "Přihlaste se do organizace",
"orgAuthSignInToOrg": "Sign in to an organization",
"orgAuthSelectOrgTitle": "Přihlášení do organizace",
"orgAuthSelectOrgDescription": "Zadejte ID vaší organizace pro pokračování",
"orgAuthOrgIdPlaceholder": "vaše-organizace",
@@ -2232,6 +2267,8 @@
"deviceCodeInvalidFormat": "Kód musí být 9 znaků (např. A1AJ-N5JD)",
"deviceCodeInvalidOrExpired": "Neplatný nebo prošlý kód",
"deviceCodeVerifyFailed": "Ověření kódu zařízení se nezdařilo",
"deviceCodeValidating": "Validating device code...",
"deviceCodeVerifying": "Verifying device authorization...",
"signedInAs": "Přihlášen jako",
"deviceCodeEnterPrompt": "Zadejte kód zobrazený na zařízení",
"continue": "Pokračovat",
@@ -2244,7 +2281,7 @@
"deviceOrganizationsAccess": "Přístup ke všem organizacím má přístup k vašemu účtu",
"deviceAuthorize": "Autorizovat {applicationName}",
"deviceConnected": "Zařízení připojeno!",
"deviceAuthorizedMessage": "Zařízení má oprávnění k přístupu k vašemu účtu.",
"deviceAuthorizedMessage": "Device is authorized to access your account. Please return to the client application.",
"pangolinCloud": "Pangolin Cloud",
"viewDevices": "Zobrazit zařízení",
"viewDevicesDescription": "Spravovat připojená zařízení",
@@ -2306,6 +2343,7 @@
"identifier": "Identifier",
"deviceLoginUseDifferentAccount": "Nejste vy? Použijte jiný účet.",
"deviceLoginDeviceRequestingAccessToAccount": "Zařízení žádá o přístup k tomuto účtu.",
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
"noData": "Žádná data",
"machineClients": "Strojoví klienti",
"install": "Instalovat",
@@ -2394,5 +2432,56 @@
"maintenanceScreenTitle": "Služba dočasně nedostupná",
"maintenanceScreenMessage": "Momentálně máme technické potíže. Zkontrolujte později.",
"maintenanceScreenEstimatedCompletion": "Odhadované dokončení:",
"createInternalResourceDialogDestinationRequired": "Cíl je povinný"
"createInternalResourceDialogDestinationRequired": "Cíl je povinný",
"available": "Available",
"archived": "Archived",
"noArchivedDevices": "No archived devices found",
"deviceArchived": "Device archived",
"deviceArchivedDescription": "The device has been successfully archived.",
"errorArchivingDevice": "Error archiving device",
"failedToArchiveDevice": "Failed to archive device",
"deviceQuestionArchive": "Are you sure you want to archive this device?",
"deviceMessageArchive": "The device will be archived and removed from your active devices list.",
"deviceArchiveConfirm": "Archive Device",
"archiveDevice": "Archive Device",
"archive": "Archive",
"deviceUnarchived": "Device unarchived",
"deviceUnarchivedDescription": "The device has been successfully unarchived.",
"errorUnarchivingDevice": "Error unarchiving device",
"failedToUnarchiveDevice": "Failed to unarchive device",
"unarchive": "Unarchive",
"archiveClient": "Archive Client",
"archiveClientQuestion": "Are you sure you want to archive this client?",
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
"archiveClientConfirm": "Archive Client",
"blockClient": "Block Client",
"blockClientQuestion": "Are you sure you want to block this client?",
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
"blockClientConfirm": "Block Client",
"active": "Active",
"usernameOrEmail": "Username or Email",
"selectYourOrganization": "Select your organization",
"signInTo": "Log in in to",
"signInWithPassword": "Continue with Password",
"noAuthMethodsAvailable": "No authentication methods available for this organization.",
"enterPassword": "Enter your password",
"enterMfaCode": "Enter the code from your authenticator app",
"securityKeyRequired": "Please use your security key to sign in.",
"needToUseAnotherAccount": "Need to use a different account?",
"loginLegalDisclaimer": "By clicking the buttons below, you acknowledge you have read, understand, and agree to the <termsOfService>Terms of Service</termsOfService> and <privacyPolicy>Privacy Policy</privacyPolicy>.",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"userNotFoundWithUsername": "No user found with that username.",
"verify": "Verify",
"signIn": "Sign In",
"forgotPassword": "Forgot password?",
"orgSignInTip": "If you've logged in before, you can enter your username or email above to authenticate with your organization's identity provider instead. It's easier!",
"continueAnyway": "Continue anyway",
"dontShowAgain": "Don't show again",
"orgSignInNotice": "Did you know?",
"signupOrgNotice": "Trying to sign in?",
"signupOrgTip": "Are you trying to sign in through your organization's identity provider?",
"signupOrgLink": "Sign in or sign up with your organization instead",
"verifyEmailLogInWithDifferentAccount": "Use a Different Account",
"logIn": "Log In"
}

View File

@@ -257,6 +257,8 @@
"accessRolesSearch": "Rollen suchen...",
"accessRolesAdd": "Rolle hinzufügen",
"accessRoleDelete": "Rolle löschen",
"accessApprovalsManage": "Manage Approvals",
"accessApprovalsDescription": "Manage approval requests in the organization",
"description": "Beschreibung",
"inviteTitle": "Einladungen öffnen",
"inviteDescription": "Einladungen für andere Benutzer verwalten, der Organisation beizutreten",
@@ -450,6 +452,18 @@
"selectDuration": "Dauer auswählen",
"selectResource": "Ressource auswählen",
"filterByResource": "Nach Ressource filtern",
"selectApprovalState": "Select Approval State",
"filterByApprovalState": "Filter By Approval State",
"approvalListEmpty": "No approvals",
"approvalState": "Approval State",
"approve": "Approve",
"approved": "Approved",
"denied": "Denied",
"deniedApproval": "Denied Approval",
"all": "All",
"deny": "Deny",
"viewDetails": "View Details",
"requestingNewDeviceApproval": "requested a new device",
"resetFilters": "Filter zurücksetzen",
"totalBlocked": "Anfragen blockiert von Pangolin",
"totalRequests": "Gesamte Anfragen",
@@ -729,16 +743,28 @@
"countries": "Länder",
"accessRoleCreate": "Rolle erstellen",
"accessRoleCreateDescription": "Erstellen Sie eine neue Rolle, um Benutzer zu gruppieren und ihre Berechtigungen zu verwalten.",
"accessRoleEdit": "Edit Role",
"accessRoleEditDescription": "Edit role information.",
"accessRoleCreateSubmit": "Rolle erstellen",
"accessRoleCreated": "Rolle erstellt",
"accessRoleCreatedDescription": "Die Rolle wurde erfolgreich erstellt.",
"accessRoleErrorCreate": "Fehler beim Erstellen der Rolle",
"accessRoleErrorCreateDescription": "Beim Erstellen der Rolle ist ein Fehler aufgetreten.",
"accessRoleUpdateSubmit": "Update Role",
"accessRoleUpdated": "Role updated",
"accessRoleUpdatedDescription": "The role has been successfully updated.",
"accessApprovalUpdated": "Approval processed",
"accessApprovalApprovedDescription": "Set Approval Request decision to approved.",
"accessApprovalDeniedDescription": "Set Approval Request decision to denied.",
"accessRoleErrorUpdate": "Failed to update role",
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
"accessApprovalErrorUpdate": "Failed to process approval",
"accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.",
"accessRoleErrorNewRequired": "Neue Rolle ist erforderlich",
"accessRoleErrorRemove": "Fehler beim Entfernen der Rolle",
"accessRoleErrorRemoveDescription": "Beim Entfernen der Rolle ist ein Fehler aufgetreten.",
"accessRoleName": "Rollenname",
"accessRoleQuestionRemove": "Sie sind dabei, die Rolle {name} zu löschen. Diese Aktion kann nicht rückgängig gemacht werden.",
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
"accessRoleRemove": "Rolle entfernen",
"accessRoleRemoveDescription": "Eine Rolle aus der Organisation entfernen",
"accessRoleRemoveSubmit": "Rolle entfernen",
@@ -874,7 +900,7 @@
"inviteAlready": "Sieht aus, als wären Sie eingeladen worden!",
"inviteAlreadyDescription": "Um die Einladung anzunehmen, müssen Sie sich einloggen oder ein Konto erstellen.",
"signupQuestion": "Haben Sie bereits ein Konto?",
"login": "Anmelden",
"login": "Log In",
"resourceNotFound": "Ressource nicht gefunden",
"resourceNotFoundDescription": "Die Ressource, auf die Sie zugreifen möchten, existiert nicht.",
"pincodeRequirementsLength": "PIN muss genau 6 Ziffern lang sein",
@@ -954,13 +980,13 @@
"passwordExpiryDescription": "Diese Organisation erfordert, dass Sie Ihr Passwort alle {maxDays} Tage ändern.",
"changePasswordNow": "Passwort jetzt ändern",
"pincodeAuth": "Authentifizierungscode",
"pincodeSubmit2": "Code absenden",
"pincodeSubmit2": "Submit code",
"passwordResetSubmit": "Zurücksetzung anfordern",
"passwordResetAlreadyHaveCode": "Code eingeben",
"passwordResetSmtpRequired": "Bitte kontaktieren Sie Ihren Administrator",
"passwordResetSmtpRequiredDescription": "Zum Zurücksetzen Ihres Passworts ist ein Passwort erforderlich. Bitte wenden Sie sich an Ihren Administrator.",
"passwordBack": "Zurück zum Passwort",
"loginBack": "Zurück zur Anmeldung",
"loginBack": "Go back to main login page",
"signup": "Registrieren",
"loginStart": "Melden Sie sich an, um zu beginnen",
"idpOidcTokenValidating": "OIDC-Token wird validiert",
@@ -1118,6 +1144,10 @@
"actionUpdateIdpOrg": "IDP-Organisation aktualisieren",
"actionCreateClient": "Client erstellen",
"actionDeleteClient": "Client löschen",
"actionArchiveClient": "Archive Client",
"actionUnarchiveClient": "Unarchive Client",
"actionBlockClient": "Block Client",
"actionUnblockClient": "Unblock Client",
"actionUpdateClient": "Client aktualisieren",
"actionListClients": "Clients auflisten",
"actionGetClient": "Clients abrufen",
@@ -1134,14 +1164,14 @@
"searchProgress": "Suche...",
"create": "Erstellen",
"orgs": "Organisationen",
"loginError": "Beim Anmelden ist ein Fehler aufgetreten",
"loginRequiredForDevice": "Zur Authentifizierung Ihres Geräts ist eine Anmeldung erforderlich",
"loginError": "An unexpected error occurred. Please try again.",
"loginRequiredForDevice": "Anmeldung ist für Ihr Gerät erforderlich.",
"passwordForgot": "Passwort vergessen?",
"otpAuth": "Zwei-Faktor-Authentifizierung",
"otpAuthDescription": "Geben Sie den Code aus Ihrer Authenticator-App oder einen Ihrer einmaligen Backup-Codes ein.",
"otpAuthSubmit": "Code absenden",
"idpContinue": "Oder weiter mit",
"otpAuthBack": "Zurück zur Anmeldung",
"otpAuthBack": "Back to Password",
"navbar": "Navigationsmenü",
"navbarDescription": "Hauptnavigationsmenü für die Anwendung",
"navbarDocsLink": "Dokumentation",
@@ -1189,6 +1219,7 @@
"sidebarOverview": "Übersicht",
"sidebarHome": "Zuhause",
"sidebarSites": "Standorte",
"sidebarApprovals": "Approval Requests",
"sidebarResources": "Ressourcen",
"sidebarProxyResources": "Öffentlich",
"sidebarClientResources": "Privat",
@@ -1205,7 +1236,7 @@
"sidebarIdentityProviders": "Identitätsanbieter",
"sidebarLicense": "Lizenz",
"sidebarClients": "Clients",
"sidebarUserDevices": "Benutzergeräte",
"sidebarUserDevices": "User Devices",
"sidebarMachineClients": "Maschinen",
"sidebarDomains": "Domänen",
"sidebarGeneral": "Verwalten",
@@ -1277,6 +1308,7 @@
"setupErrorCreateAdmin": "Beim Erstellen des Server-Admin-Kontos ist ein Fehler aufgetreten.",
"certificateStatus": "Zertifikatsstatus",
"loading": "Laden",
"loadingAnalytics": "Loading Analytics",
"restart": "Neustart",
"domains": "Domänen",
"domainsDescription": "Erstellen und verwalten der in der Organisation verfügbaren Domänen",
@@ -1304,6 +1336,7 @@
"refreshError": "Datenaktualisierung fehlgeschlagen",
"verified": "Verifiziert",
"pending": "Ausstehend",
"pendingApproval": "Pending Approval",
"sidebarBilling": "Abrechnung",
"billing": "Abrechnung",
"orgBillingDescription": "Zahlungsinformationen und Abonnements verwalten",
@@ -1420,7 +1453,7 @@
"securityKeyRemoveSuccess": "Sicherheitsschlüssel erfolgreich entfernt",
"securityKeyRemoveError": "Fehler beim Entfernen des Sicherheitsschlüssels",
"securityKeyLoadError": "Fehler beim Laden der Sicherheitsschlüssel",
"securityKeyLogin": "Mit dem Sicherheitsschlüssel fortfahren",
"securityKeyLogin": "Use Security Key",
"securityKeyAuthError": "Fehler bei der Authentifizierung mit Sicherheitsschlüssel",
"securityKeyRecommendation": "Erwägen Sie die Registrierung eines weiteren Sicherheitsschlüssels auf einem anderen Gerät, um sicherzustellen, dass Sie sich nicht aus Ihrem Konto aussperren.",
"registering": "Registrierung...",
@@ -1547,6 +1580,8 @@
"IntervalSeconds": "Gesunder Intervall",
"timeoutSeconds": "Timeout (Sek.)",
"timeIsInSeconds": "Zeit ist in Sekunden",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need their devices approved by an admin before they can access resources",
"retryAttempts": "Wiederholungsversuche",
"expectedResponseCodes": "Erwartete Antwortcodes",
"expectedResponseCodesDescription": "HTTP-Statuscode, der einen gesunden Zustand anzeigt. Wenn leer gelassen, wird 200-300 als gesund angesehen.",
@@ -1876,7 +1911,7 @@
"orgAuthChooseIdpDescription": "Wähle deinen Identitätsanbieter um fortzufahren",
"orgAuthNoIdpConfigured": "Diese Organisation hat keine Identitätsanbieter konfiguriert. Sie können sich stattdessen mit Ihrer Pangolin-Identität anmelden.",
"orgAuthSignInWithPangolin": "Mit Pangolin anmelden",
"orgAuthSignInToOrg": "Bei einer Organisation anmelden",
"orgAuthSignInToOrg": "Sign in to an organization",
"orgAuthSelectOrgTitle": "Organisations-Anmeldung",
"orgAuthSelectOrgDescription": "Geben Sie Ihre Organisations-ID ein, um fortzufahren",
"orgAuthOrgIdPlaceholder": "Ihre Organisation",
@@ -2232,6 +2267,8 @@
"deviceCodeInvalidFormat": "Code muss 9 Zeichen lang sein (z.B. A1AJ-N5JD)",
"deviceCodeInvalidOrExpired": "Ungültiger oder abgelaufener Code",
"deviceCodeVerifyFailed": "Fehler beim Überprüfen des Gerätecodes",
"deviceCodeValidating": "Validating device code...",
"deviceCodeVerifying": "Verifying device authorization...",
"signedInAs": "Angemeldet als",
"deviceCodeEnterPrompt": "Geben Sie den auf dem Gerät angezeigten Code ein",
"continue": "Weiter",
@@ -2244,7 +2281,7 @@
"deviceOrganizationsAccess": "Zugriff auf alle Organisationen, auf die Ihr Konto Zugriff hat",
"deviceAuthorize": "{applicationName} autorisieren",
"deviceConnected": "Gerät verbunden!",
"deviceAuthorizedMessage": "Gerät ist berechtigt, auf Ihr Konto zuzugreifen.",
"deviceAuthorizedMessage": "Gerät ist berechtigt, auf Ihr Konto zuzugreifen. Bitte kehren Sie zur Client-Anwendung zurück.",
"pangolinCloud": "Pangolin Cloud",
"viewDevices": "Geräte anzeigen",
"viewDevicesDescription": "Verwalten Sie Ihre verbundenen Geräte",
@@ -2306,6 +2343,7 @@
"identifier": "Identifier",
"deviceLoginUseDifferentAccount": "Nicht du? Verwenden Sie ein anderes Konto.",
"deviceLoginDeviceRequestingAccessToAccount": "Ein Gerät fordert Zugriff auf dieses Konto an.",
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
"noData": "Keine Daten",
"machineClients": "Maschinen-Clients",
"install": "Installieren",
@@ -2394,5 +2432,56 @@
"maintenanceScreenTitle": "Dienst vorübergehend nicht verfügbar",
"maintenanceScreenMessage": "Wir haben derzeit technische Schwierigkeiten. Bitte schauen Sie bald noch einmal vorbei.",
"maintenanceScreenEstimatedCompletion": "Geschätzter Abschluss:",
"createInternalResourceDialogDestinationRequired": "Ziel ist erforderlich"
"createInternalResourceDialogDestinationRequired": "Ziel ist erforderlich",
"available": "Verfügbar",
"archived": "Archiviert",
"noArchivedDevices": "Keine archivierten Geräte gefunden",
"deviceArchived": "Gerät archiviert",
"deviceArchivedDescription": "Das Gerät wurde erfolgreich archiviert.",
"errorArchivingDevice": "Fehler beim Archivieren des Geräts",
"failedToArchiveDevice": "Archivierung des Geräts fehlgeschlagen",
"deviceQuestionArchive": "Sind Sie sicher, dass Sie dieses Gerät archivieren möchten?",
"deviceMessageArchive": "Das Gerät wird archiviert und aus Ihrer Liste der aktiven Geräte entfernt.",
"deviceArchiveConfirm": "Gerät archivieren",
"archiveDevice": "Gerät archivieren",
"archive": "Archiv",
"deviceUnarchived": "Device unarchived",
"deviceUnarchivedDescription": "The device has been successfully unarchived.",
"errorUnarchivingDevice": "Error unarchiving device",
"failedToUnarchiveDevice": "Failed to unarchive device",
"unarchive": "Unarchive",
"archiveClient": "Archive Client",
"archiveClientQuestion": "Are you sure you want to archive this client?",
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
"archiveClientConfirm": "Archive Client",
"blockClient": "Block Client",
"blockClientQuestion": "Are you sure you want to block this client?",
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
"blockClientConfirm": "Block Client",
"active": "Active",
"usernameOrEmail": "Username or Email",
"selectYourOrganization": "Select your organization",
"signInTo": "Log in in to",
"signInWithPassword": "Continue with Password",
"noAuthMethodsAvailable": "No authentication methods available for this organization.",
"enterPassword": "Enter your password",
"enterMfaCode": "Enter the code from your authenticator app",
"securityKeyRequired": "Please use your security key to sign in.",
"needToUseAnotherAccount": "Need to use a different account?",
"loginLegalDisclaimer": "By clicking the buttons below, you acknowledge you have read, understand, and agree to the <termsOfService>Terms of Service</termsOfService> and <privacyPolicy>Privacy Policy</privacyPolicy>.",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"userNotFoundWithUsername": "No user found with that username.",
"verify": "Verify",
"signIn": "Sign In",
"forgotPassword": "Forgot password?",
"orgSignInTip": "If you've logged in before, you can enter your username or email above to authenticate with your organization's identity provider instead. It's easier!",
"continueAnyway": "Continue anyway",
"dontShowAgain": "Don't show again",
"orgSignInNotice": "Did you know?",
"signupOrgNotice": "Trying to sign in?",
"signupOrgTip": "Are you trying to sign in through your organization's identity provider?",
"signupOrgLink": "Sign in or sign up with your organization instead",
"verifyEmailLogInWithDifferentAccount": "Use a Different Account",
"logIn": "Log In"
}

View File

@@ -257,8 +257,6 @@
"accessRolesSearch": "Search roles...",
"accessRolesAdd": "Add Role",
"accessRoleDelete": "Delete Role",
"accessApprovalsManage": "Manage Approvals",
"accessApprovalsDescription": "Manage approval requests in the organization",
"description": "Description",
"inviteTitle": "Open Invitations",
"inviteDescription": "Manage invitations for other users to join the organization",
@@ -452,18 +450,6 @@
"selectDuration": "Select duration",
"selectResource": "Select Resource",
"filterByResource": "Filter By Resource",
"selectApprovalState": "Select Approval State",
"filterByApprovalState": "Filter By Approval State",
"approvalListEmpty": "No approvals",
"approvalState": "Approval State",
"approve": "Approve",
"approved": "Approved",
"denied": "Denied",
"deniedApproval": "Denied Approval",
"all": "All",
"deny": "Deny",
"viewDetails": "View Details",
"requestingNewDeviceApproval": "requested a new device",
"resetFilters": "Reset Filters",
"totalBlocked": "Requests Blocked By Pangolin",
"totalRequests": "Total Requests",
@@ -743,28 +729,16 @@
"countries": "Countries",
"accessRoleCreate": "Create Role",
"accessRoleCreateDescription": "Create a new role to group users and manage their permissions.",
"accessRoleEdit": "Edit Role",
"accessRoleEditDescription": "Edit role information.",
"accessRoleCreateSubmit": "Create Role",
"accessRoleCreated": "Role created",
"accessRoleCreatedDescription": "The role has been successfully created.",
"accessRoleErrorCreate": "Failed to create role",
"accessRoleErrorCreateDescription": "An error occurred while creating the role.",
"accessRoleUpdateSubmit": "Update Role",
"accessRoleUpdated": "Role updated",
"accessRoleUpdatedDescription": "The role has been successfully updated.",
"accessApprovalUpdated": "Approval processed",
"accessApprovalApprovedDescription": "Set Approval Request decision to approved.",
"accessApprovalDeniedDescription": "Set Approval Request decision to denied.",
"accessRoleErrorUpdate": "Failed to update role",
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
"accessApprovalErrorUpdate": "Failed to process approval",
"accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.",
"accessRoleErrorNewRequired": "New role is required",
"accessRoleErrorRemove": "Failed to remove role",
"accessRoleErrorRemoveDescription": "An error occurred while removing the role.",
"accessRoleName": "Role Name",
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
"accessRoleQuestionRemove": "You're about to delete the {name} role. You cannot undo this action.",
"accessRoleRemove": "Remove Role",
"accessRoleRemoveDescription": "Remove a role from the organization",
"accessRoleRemoveSubmit": "Remove Role",
@@ -900,7 +874,7 @@
"inviteAlready": "Looks like you've been invited!",
"inviteAlreadyDescription": "To accept the invite, you must log in or create an account.",
"signupQuestion": "Already have an account?",
"login": "Log In",
"login": "Log in",
"resourceNotFound": "Resource Not Found",
"resourceNotFoundDescription": "The resource you're trying to access does not exist.",
"pincodeRequirementsLength": "PIN must be exactly 6 digits",
@@ -980,13 +954,13 @@
"passwordExpiryDescription": "This organization requires you to change your password every {maxDays} days.",
"changePasswordNow": "Change Password Now",
"pincodeAuth": "Authenticator Code",
"pincodeSubmit2": "Submit code",
"pincodeSubmit2": "Submit Code",
"passwordResetSubmit": "Request Reset",
"passwordResetAlreadyHaveCode": "Enter Code",
"passwordResetSmtpRequired": "Please contact your administrator",
"passwordResetSmtpRequiredDescription": "A password reset code is required to reset your password. Please contact your administrator for assistance.",
"passwordBack": "Back to Password",
"loginBack": "Go back to main login page",
"loginBack": "Go back to log in",
"signup": "Sign up",
"loginStart": "Log in to get started",
"idpOidcTokenValidating": "Validating OIDC token",
@@ -1144,10 +1118,6 @@
"actionUpdateIdpOrg": "Update IDP Org",
"actionCreateClient": "Create Client",
"actionDeleteClient": "Delete Client",
"actionArchiveClient": "Archive Client",
"actionUnarchiveClient": "Unarchive Client",
"actionBlockClient": "Block Client",
"actionUnblockClient": "Unblock Client",
"actionUpdateClient": "Update Client",
"actionListClients": "List Clients",
"actionGetClient": "Get Client",
@@ -1164,14 +1134,14 @@
"searchProgress": "Search...",
"create": "Create",
"orgs": "Organizations",
"loginError": "An unexpected error occurred. Please try again.",
"loginRequiredForDevice": "Login is required for your device.",
"loginError": "An error occurred while logging in",
"loginRequiredForDevice": "Login is required to authenticate your device.",
"passwordForgot": "Forgot your password?",
"otpAuth": "Two-Factor Authentication",
"otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.",
"otpAuthSubmit": "Submit Code",
"idpContinue": "Or continue with",
"otpAuthBack": "Back to Password",
"otpAuthBack": "Back to Log In",
"navbar": "Navigation Menu",
"navbarDescription": "Main navigation menu for the application",
"navbarDocsLink": "Documentation",
@@ -1219,7 +1189,6 @@
"sidebarOverview": "Overview",
"sidebarHome": "Home",
"sidebarSites": "Sites",
"sidebarApprovals": "Approval Requests",
"sidebarResources": "Resources",
"sidebarProxyResources": "Public",
"sidebarClientResources": "Private",
@@ -1236,7 +1205,7 @@
"sidebarIdentityProviders": "Identity Providers",
"sidebarLicense": "License",
"sidebarClients": "Clients",
"sidebarUserDevices": "User Devices",
"sidebarUserDevices": "Users",
"sidebarMachineClients": "Machines",
"sidebarDomains": "Domains",
"sidebarGeneral": "Manage",
@@ -1335,7 +1304,6 @@
"refreshError": "Failed to refresh data",
"verified": "Verified",
"pending": "Pending",
"pendingApproval": "Pending Approval",
"sidebarBilling": "Billing",
"billing": "Billing",
"orgBillingDescription": "Manage billing information and subscriptions",
@@ -1452,7 +1420,7 @@
"securityKeyRemoveSuccess": "Security key removed successfully",
"securityKeyRemoveError": "Failed to remove security key",
"securityKeyLoadError": "Failed to load security keys",
"securityKeyLogin": "Use Security Key",
"securityKeyLogin": "Continue with security key",
"securityKeyAuthError": "Failed to authenticate with security key",
"securityKeyRecommendation": "Register a backup security key on another device to ensure you always have access to your account.",
"registering": "Registering...",
@@ -1579,8 +1547,6 @@
"IntervalSeconds": "Healthy Interval",
"timeoutSeconds": "Timeout (sec)",
"timeIsInSeconds": "Time is in seconds",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need their devices approved by an admin before they can access resources",
"retryAttempts": "Retry Attempts",
"expectedResponseCodes": "Expected Response Codes",
"expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.",
@@ -2266,8 +2232,6 @@
"deviceCodeInvalidFormat": "Code must be 9 characters (e.g., A1AJ-N5JD)",
"deviceCodeInvalidOrExpired": "Invalid or expired code",
"deviceCodeVerifyFailed": "Failed to verify device code",
"deviceCodeValidating": "Validating device code...",
"deviceCodeVerifying": "Verifying device authorization...",
"signedInAs": "Signed in as",
"deviceCodeEnterPrompt": "Enter the code displayed on the device",
"continue": "Continue",
@@ -2280,7 +2244,7 @@
"deviceOrganizationsAccess": "Access to all organizations your account has access to",
"deviceAuthorize": "Authorize {applicationName}",
"deviceConnected": "Device Connected!",
"deviceAuthorizedMessage": "Device is authorized to access your account. Please return to the client application.",
"deviceAuthorizedMessage": "Device is authorized to access your account.",
"pangolinCloud": "Pangolin Cloud",
"viewDevices": "View Devices",
"viewDevicesDescription": "Manage your connected devices",
@@ -2342,7 +2306,6 @@
"identifier": "Identifier",
"deviceLoginUseDifferentAccount": "Not you? Use a different account.",
"deviceLoginDeviceRequestingAccessToAccount": "A device is requesting access to this account.",
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
"noData": "No Data",
"machineClients": "Machine Clients",
"install": "Install",
@@ -2431,56 +2394,5 @@
"maintenanceScreenTitle": "Service Temporarily Unavailable",
"maintenanceScreenMessage": "We are currently experiencing technical difficulties. Please check back soon.",
"maintenanceScreenEstimatedCompletion": "Estimated Completion:",
"createInternalResourceDialogDestinationRequired": "Destination is required",
"available": "Available",
"archived": "Archived",
"noArchivedDevices": "No archived devices found",
"deviceArchived": "Device archived",
"deviceArchivedDescription": "The device has been successfully archived.",
"errorArchivingDevice": "Error archiving device",
"failedToArchiveDevice": "Failed to archive device",
"deviceQuestionArchive": "Are you sure you want to archive this device?",
"deviceMessageArchive": "The device will be archived and removed from your active devices list.",
"deviceArchiveConfirm": "Archive Device",
"archiveDevice": "Archive Device",
"archive": "Archive",
"deviceUnarchived": "Device unarchived",
"deviceUnarchivedDescription": "The device has been successfully unarchived.",
"errorUnarchivingDevice": "Error unarchiving device",
"failedToUnarchiveDevice": "Failed to unarchive device",
"unarchive": "Unarchive",
"archiveClient": "Archive Client",
"archiveClientQuestion": "Are you sure you want to archive this client?",
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
"archiveClientConfirm": "Archive Client",
"blockClient": "Block Client",
"blockClientQuestion": "Are you sure you want to block this client?",
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
"blockClientConfirm": "Block Client",
"active": "Active",
"usernameOrEmail": "Username or Email",
"selectYourOrganization": "Select your organization",
"signInTo": "Log in in to",
"signInWithPassword": "Continue with Password",
"noAuthMethodsAvailable": "No authentication methods available for this organization.",
"enterPassword": "Enter your password",
"enterMfaCode": "Enter the code from your authenticator app",
"securityKeyRequired": "Please use your security key to sign in.",
"needToUseAnotherAccount": "Need to use a different account?",
"loginLegalDisclaimer": "By clicking the buttons below, you acknowledge you have read, understand, and agree to the <termsOfService>Terms of Service</termsOfService> and <privacyPolicy>Privacy Policy</privacyPolicy>.",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"userNotFoundWithUsername": "No user found with that username.",
"verify": "Verify",
"signIn": "Sign In",
"forgotPassword": "Forgot password?",
"orgSignInTip": "If you've logged in before, you can enter your username or email above to authenticate with your organization's identity provider instead. It's easier!",
"continueAnyway": "Continue anyway",
"dontShowAgain": "Don't show again",
"orgSignInNotice": "Did you know?",
"signupOrgNotice": "Trying to sign in?",
"signupOrgTip": "Are you trying to sign in through your organization's identity provider?",
"signupOrgLink": "Sign in or sign up with your organization instead",
"verifyEmailLogInWithDifferentAccount": "Use a Different Account",
"logIn": "Log In"
"createInternalResourceDialogDestinationRequired": "Destination is required"
}

View File

@@ -257,6 +257,8 @@
"accessRolesSearch": "Buscar roles...",
"accessRolesAdd": "Añadir rol",
"accessRoleDelete": "Eliminar rol",
"accessApprovalsManage": "Manage Approvals",
"accessApprovalsDescription": "Manage approval requests in the organization",
"description": "Descripción",
"inviteTitle": "Invitaciones abiertas",
"inviteDescription": "Administrar invitaciones para que otros usuarios se unan a la organización",
@@ -450,6 +452,18 @@
"selectDuration": "Seleccionar duración",
"selectResource": "Seleccionar Recurso",
"filterByResource": "Filtrar por Recurso",
"selectApprovalState": "Select Approval State",
"filterByApprovalState": "Filter By Approval State",
"approvalListEmpty": "No approvals",
"approvalState": "Approval State",
"approve": "Approve",
"approved": "Approved",
"denied": "Denied",
"deniedApproval": "Denied Approval",
"all": "All",
"deny": "Deny",
"viewDetails": "View Details",
"requestingNewDeviceApproval": "requested a new device",
"resetFilters": "Reiniciar filtros",
"totalBlocked": "Solicitudes bloqueadas por Pangolin",
"totalRequests": "Solicitudes totales",
@@ -729,16 +743,28 @@
"countries": "Países",
"accessRoleCreate": "Crear rol",
"accessRoleCreateDescription": "Crear un nuevo rol para agrupar usuarios y administrar sus permisos.",
"accessRoleEdit": "Edit Role",
"accessRoleEditDescription": "Edit role information.",
"accessRoleCreateSubmit": "Crear rol",
"accessRoleCreated": "Rol creado",
"accessRoleCreatedDescription": "El rol se ha creado correctamente.",
"accessRoleErrorCreate": "Error al crear el rol",
"accessRoleErrorCreateDescription": "Se ha producido un error al crear el rol.",
"accessRoleUpdateSubmit": "Update Role",
"accessRoleUpdated": "Role updated",
"accessRoleUpdatedDescription": "The role has been successfully updated.",
"accessApprovalUpdated": "Approval processed",
"accessApprovalApprovedDescription": "Set Approval Request decision to approved.",
"accessApprovalDeniedDescription": "Set Approval Request decision to denied.",
"accessRoleErrorUpdate": "Failed to update role",
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
"accessApprovalErrorUpdate": "Failed to process approval",
"accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.",
"accessRoleErrorNewRequired": "Se requiere un nuevo rol",
"accessRoleErrorRemove": "Error al eliminar el rol",
"accessRoleErrorRemoveDescription": "Ocurrió un error mientras se eliminaba el rol.",
"accessRoleName": "Nombre del Rol",
"accessRoleQuestionRemove": "Estás a punto de eliminar el rol {name} . No puedes deshacer esta acción.",
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
"accessRoleRemove": "Quitar rol",
"accessRoleRemoveDescription": "Eliminar un rol de la organización",
"accessRoleRemoveSubmit": "Quitar rol",
@@ -874,7 +900,7 @@
"inviteAlready": "¡Parece que has sido invitado!",
"inviteAlreadyDescription": "Para aceptar la invitación, debes iniciar sesión o crear una cuenta.",
"signupQuestion": "¿Ya tienes una cuenta?",
"login": "Iniciar sesión",
"login": "Log In",
"resourceNotFound": "Recurso no encontrado",
"resourceNotFoundDescription": "El recurso al que intentas acceder no existe.",
"pincodeRequirementsLength": "El PIN debe tener exactamente 6 dígitos",
@@ -954,13 +980,13 @@
"passwordExpiryDescription": "Esta organización requiere que cambies tu contraseña cada {maxDays} días.",
"changePasswordNow": "Cambiar Contraseña Ahora",
"pincodeAuth": "Código de autenticación",
"pincodeSubmit2": "Enviar código",
"pincodeSubmit2": "Submit code",
"passwordResetSubmit": "Reiniciar Solicitud",
"passwordResetAlreadyHaveCode": "Ingresar código",
"passwordResetSmtpRequired": "Póngase en contacto con su administrador",
"passwordResetSmtpRequiredDescription": "Se requiere un código de restablecimiento de contraseña para restablecer su contraseña. Póngase en contacto con su administrador para obtener asistencia.",
"passwordBack": "Volver a la contraseña",
"loginBack": "Volver a iniciar sesión",
"loginBack": "Go back to main login page",
"signup": "Regístrate",
"loginStart": "Inicia sesión para empezar",
"idpOidcTokenValidating": "Validando token OIDC",
@@ -1118,6 +1144,10 @@
"actionUpdateIdpOrg": "Actualizar IDP Org",
"actionCreateClient": "Crear cliente",
"actionDeleteClient": "Eliminar cliente",
"actionArchiveClient": "Archive Client",
"actionUnarchiveClient": "Unarchive Client",
"actionBlockClient": "Block Client",
"actionUnblockClient": "Unblock Client",
"actionUpdateClient": "Actualizar cliente",
"actionListClients": "Listar clientes",
"actionGetClient": "Obtener cliente",
@@ -1134,14 +1164,14 @@
"searchProgress": "Buscar...",
"create": "Crear",
"orgs": "Organizaciones",
"loginError": "Se ha producido un error al iniciar sesión",
"loginRequiredForDevice": "Es necesario iniciar sesión para autenticar tu dispositivo.",
"loginError": "An unexpected error occurred. Please try again.",
"loginRequiredForDevice": "Login is required for your device.",
"passwordForgot": "¿Olvidaste tu contraseña?",
"otpAuth": "Autenticación de dos factores",
"otpAuthDescription": "Introduzca el código de su aplicación de autenticación o uno de sus códigos de copia de seguridad de un solo uso.",
"otpAuthSubmit": "Enviar código",
"idpContinue": "O continuar con",
"otpAuthBack": "Volver a iniciar sesión",
"otpAuthBack": "Back to Password",
"navbar": "Menú de navegación",
"navbarDescription": "Menú de navegación principal para la aplicación",
"navbarDocsLink": "Documentación",
@@ -1189,6 +1219,7 @@
"sidebarOverview": "Resumen",
"sidebarHome": "Inicio",
"sidebarSites": "Sitios",
"sidebarApprovals": "Approval Requests",
"sidebarResources": "Recursos",
"sidebarProxyResources": "Público",
"sidebarClientResources": "Privado",
@@ -1205,7 +1236,7 @@
"sidebarIdentityProviders": "Proveedores de identidad",
"sidebarLicense": "Licencia",
"sidebarClients": "Clientes",
"sidebarUserDevices": "Usuarios",
"sidebarUserDevices": "User Devices",
"sidebarMachineClients": "Máquinas",
"sidebarDomains": "Dominios",
"sidebarGeneral": "Gestionar",
@@ -1277,6 +1308,7 @@
"setupErrorCreateAdmin": "Se produjo un error al crear la cuenta de administrador del servidor.",
"certificateStatus": "Estado del certificado",
"loading": "Cargando",
"loadingAnalytics": "Loading Analytics",
"restart": "Reiniciar",
"domains": "Dominios",
"domainsDescription": "Crear y administrar dominios disponibles en la organización",
@@ -1304,6 +1336,7 @@
"refreshError": "Error al actualizar datos",
"verified": "Verificado",
"pending": "Pendiente",
"pendingApproval": "Pending Approval",
"sidebarBilling": "Facturación",
"billing": "Facturación",
"orgBillingDescription": "Administrar información de facturación y suscripciones",
@@ -1420,7 +1453,7 @@
"securityKeyRemoveSuccess": "Llave de seguridad eliminada exitosamente",
"securityKeyRemoveError": "Error al eliminar la llave de seguridad",
"securityKeyLoadError": "Error al cargar las llaves de seguridad",
"securityKeyLogin": "Continuar con clave de seguridad",
"securityKeyLogin": "Use Security Key",
"securityKeyAuthError": "Error al autenticar con llave de seguridad",
"securityKeyRecommendation": "Considere registrar otra llave de seguridad en un dispositivo diferente para asegurarse de no quedar bloqueado de su cuenta.",
"registering": "Registrando...",
@@ -1547,6 +1580,8 @@
"IntervalSeconds": "Intervalo Saludable",
"timeoutSeconds": "Tiempo agotado (seg)",
"timeIsInSeconds": "El tiempo está en segundos",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need their devices approved by an admin before they can access resources",
"retryAttempts": "Intentos de Reintento",
"expectedResponseCodes": "Códigos de respuesta esperados",
"expectedResponseCodesDescription": "Código de estado HTTP que indica un estado saludable. Si se deja en blanco, se considera saludable de 200 a 300.",
@@ -1876,7 +1911,7 @@
"orgAuthChooseIdpDescription": "Elige tu proveedor de identidad para continuar",
"orgAuthNoIdpConfigured": "Esta organización no tiene ningún proveedor de identidad configurado. En su lugar puedes iniciar sesión con tu identidad de Pangolin.",
"orgAuthSignInWithPangolin": "Iniciar sesión con Pangolin",
"orgAuthSignInToOrg": "Iniciar sesión en una organización",
"orgAuthSignInToOrg": "Sign in to an organization",
"orgAuthSelectOrgTitle": "Inicio de sesión de organización",
"orgAuthSelectOrgDescription": "Ingrese el ID de su organización para continuar",
"orgAuthOrgIdPlaceholder": "tu-organización",
@@ -2232,6 +2267,8 @@
"deviceCodeInvalidFormat": "El código debe tener 9 caracteres (por ejemplo, A1AJ-N5JD)",
"deviceCodeInvalidOrExpired": "Código no válido o caducado",
"deviceCodeVerifyFailed": "Error al verificar el código del dispositivo",
"deviceCodeValidating": "Validating device code...",
"deviceCodeVerifying": "Verifying device authorization...",
"signedInAs": "Conectado como",
"deviceCodeEnterPrompt": "Introduzca el código mostrado en el dispositivo",
"continue": "Continuar",
@@ -2244,7 +2281,7 @@
"deviceOrganizationsAccess": "Acceso a todas las organizaciones a las que su cuenta tiene acceso",
"deviceAuthorize": "Autorizar a {applicationName}",
"deviceConnected": "¡Dispositivo conectado!",
"deviceAuthorizedMessage": "El dispositivo está autorizado para acceder a su cuenta.",
"deviceAuthorizedMessage": "Device is authorized to access your account. Please return to the client application.",
"pangolinCloud": "Nube de Pangolin",
"viewDevices": "Ver dispositivos",
"viewDevicesDescription": "Administra tus dispositivos conectados",
@@ -2306,6 +2343,7 @@
"identifier": "Identifier",
"deviceLoginUseDifferentAccount": "¿No tú? Utilice una cuenta diferente.",
"deviceLoginDeviceRequestingAccessToAccount": "Un dispositivo está solicitando acceso a esta cuenta.",
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
"noData": "Sin datos",
"machineClients": "Clientes de la máquina",
"install": "Instalar",
@@ -2394,5 +2432,56 @@
"maintenanceScreenTitle": "Servicio temporalmente no disponible",
"maintenanceScreenMessage": "Actualmente estamos experimentando dificultades técnicas. Por favor regrese pronto.",
"maintenanceScreenEstimatedCompletion": "Estimado completado:",
"createInternalResourceDialogDestinationRequired": "Se requiere destino"
"createInternalResourceDialogDestinationRequired": "Se requiere destino",
"available": "Available",
"archived": "Archived",
"noArchivedDevices": "No archived devices found",
"deviceArchived": "Device archived",
"deviceArchivedDescription": "The device has been successfully archived.",
"errorArchivingDevice": "Error archiving device",
"failedToArchiveDevice": "Failed to archive device",
"deviceQuestionArchive": "Are you sure you want to archive this device?",
"deviceMessageArchive": "The device will be archived and removed from your active devices list.",
"deviceArchiveConfirm": "Archive Device",
"archiveDevice": "Archive Device",
"archive": "Archive",
"deviceUnarchived": "Device unarchived",
"deviceUnarchivedDescription": "The device has been successfully unarchived.",
"errorUnarchivingDevice": "Error unarchiving device",
"failedToUnarchiveDevice": "Failed to unarchive device",
"unarchive": "Unarchive",
"archiveClient": "Archive Client",
"archiveClientQuestion": "Are you sure you want to archive this client?",
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
"archiveClientConfirm": "Archive Client",
"blockClient": "Block Client",
"blockClientQuestion": "Are you sure you want to block this client?",
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
"blockClientConfirm": "Block Client",
"active": "Active",
"usernameOrEmail": "Username or Email",
"selectYourOrganization": "Select your organization",
"signInTo": "Log in in to",
"signInWithPassword": "Continue with Password",
"noAuthMethodsAvailable": "No authentication methods available for this organization.",
"enterPassword": "Enter your password",
"enterMfaCode": "Enter the code from your authenticator app",
"securityKeyRequired": "Please use your security key to sign in.",
"needToUseAnotherAccount": "Need to use a different account?",
"loginLegalDisclaimer": "By clicking the buttons below, you acknowledge you have read, understand, and agree to the <termsOfService>Terms of Service</termsOfService> and <privacyPolicy>Privacy Policy</privacyPolicy>.",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"userNotFoundWithUsername": "No user found with that username.",
"verify": "Verify",
"signIn": "Sign In",
"forgotPassword": "Forgot password?",
"orgSignInTip": "If you've logged in before, you can enter your username or email above to authenticate with your organization's identity provider instead. It's easier!",
"continueAnyway": "Continue anyway",
"dontShowAgain": "Don't show again",
"orgSignInNotice": "Did you know?",
"signupOrgNotice": "Trying to sign in?",
"signupOrgTip": "Are you trying to sign in through your organization's identity provider?",
"signupOrgLink": "Sign in or sign up with your organization instead",
"verifyEmailLogInWithDifferentAccount": "Use a Different Account",
"logIn": "Log In"
}

View File

@@ -257,6 +257,8 @@
"accessRolesSearch": "Chercher des rôles...",
"accessRolesAdd": "Ajouter un rôle",
"accessRoleDelete": "Supprimer le rôle",
"accessApprovalsManage": "Manage Approvals",
"accessApprovalsDescription": "Manage approval requests in the organization",
"description": "Libellé",
"inviteTitle": "Invitations actives",
"inviteDescription": "Gérer les invitations des autres utilisateurs à rejoindre l'organisation",
@@ -450,6 +452,18 @@
"selectDuration": "Sélectionner la durée",
"selectResource": "Sélectionner une ressource",
"filterByResource": "Filtrer par ressource",
"selectApprovalState": "Select Approval State",
"filterByApprovalState": "Filter By Approval State",
"approvalListEmpty": "No approvals",
"approvalState": "Approval State",
"approve": "Approve",
"approved": "Approved",
"denied": "Denied",
"deniedApproval": "Denied Approval",
"all": "All",
"deny": "Deny",
"viewDetails": "View Details",
"requestingNewDeviceApproval": "requested a new device",
"resetFilters": "Réinitialiser les filtres",
"totalBlocked": "Demandes bloquées par le Pangolin",
"totalRequests": "Total des demandes",
@@ -729,16 +743,28 @@
"countries": "Pays",
"accessRoleCreate": "Créer un rôle",
"accessRoleCreateDescription": "Créer un nouveau rôle pour regrouper les utilisateurs et gérer leurs permissions.",
"accessRoleEdit": "Edit Role",
"accessRoleEditDescription": "Edit role information.",
"accessRoleCreateSubmit": "Créer un rôle",
"accessRoleCreated": "Rôle créé",
"accessRoleCreatedDescription": "Le rôle a été créé avec succès.",
"accessRoleErrorCreate": "Échec de la création du rôle",
"accessRoleErrorCreateDescription": "Une erreur s'est produite lors de la création du rôle.",
"accessRoleUpdateSubmit": "Update Role",
"accessRoleUpdated": "Role updated",
"accessRoleUpdatedDescription": "The role has been successfully updated.",
"accessApprovalUpdated": "Approval processed",
"accessApprovalApprovedDescription": "Set Approval Request decision to approved.",
"accessApprovalDeniedDescription": "Set Approval Request decision to denied.",
"accessRoleErrorUpdate": "Failed to update role",
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
"accessApprovalErrorUpdate": "Failed to process approval",
"accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.",
"accessRoleErrorNewRequired": "Un nouveau rôle est requis",
"accessRoleErrorRemove": "Échec de la suppression du rôle",
"accessRoleErrorRemoveDescription": "Une erreur s'est produite lors de la suppression du rôle.",
"accessRoleName": "Nom du rôle",
"accessRoleQuestionRemove": "Vous êtes sur le point de supprimer le rôle {name}. Cette action est irréversible.",
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
"accessRoleRemove": "Supprimer le rôle",
"accessRoleRemoveDescription": "Retirer un rôle de l'organisation",
"accessRoleRemoveSubmit": "Supprimer le rôle",
@@ -874,7 +900,7 @@
"inviteAlready": "On dirait que vous avez été invité !",
"inviteAlreadyDescription": "Pour accepter l'invitation, vous devez vous connecter ou créer un compte.",
"signupQuestion": "Vous avez déjà un compte ?",
"login": "Se connecter",
"login": "Log In",
"resourceNotFound": "Ressource introuvable",
"resourceNotFoundDescription": "La ressource que vous essayez d'accéder n'existe pas.",
"pincodeRequirementsLength": "Le code PIN doit comporter exactement 6 chiffres",
@@ -954,13 +980,13 @@
"passwordExpiryDescription": "Cette organisation vous demande de changer votre mot de passe tous les {maxDays} jours.",
"changePasswordNow": "Changer le mot de passe maintenant",
"pincodeAuth": "Code d'authentification",
"pincodeSubmit2": "Soumettre le code",
"pincodeSubmit2": "Submit code",
"passwordResetSubmit": "Demander la réinitialisation",
"passwordResetAlreadyHaveCode": "Entrer le code",
"passwordResetSmtpRequired": "Veuillez contacter votre administrateur",
"passwordResetSmtpRequiredDescription": "Un code de réinitialisation du mot de passe est requis pour réinitialiser votre mot de passe. Veuillez contacter votre administrateur pour obtenir de l'aide.",
"passwordBack": "Retour au mot de passe",
"loginBack": "Retour à la connexion",
"loginBack": "Go back to main login page",
"signup": "S'inscrire",
"loginStart": "Connectez-vous pour commencer",
"idpOidcTokenValidating": "Validation du jeton OIDC",
@@ -1118,6 +1144,10 @@
"actionUpdateIdpOrg": "Mettre à jour une organisation IDP",
"actionCreateClient": "Créer un client",
"actionDeleteClient": "Supprimer le client",
"actionArchiveClient": "Archive Client",
"actionUnarchiveClient": "Unarchive Client",
"actionBlockClient": "Block Client",
"actionUnblockClient": "Unblock Client",
"actionUpdateClient": "Mettre à jour le client",
"actionListClients": "Liste des clients",
"actionGetClient": "Obtenir le client",
@@ -1134,14 +1164,14 @@
"searchProgress": "Rechercher...",
"create": "Créer",
"orgs": "Organisations",
"loginError": "Une erreur s'est produite lors de la connexion",
"loginRequiredForDevice": "La connexion est requise pour authentifier votre appareil.",
"loginError": "An unexpected error occurred. Please try again.",
"loginRequiredForDevice": "Login is required for your device.",
"passwordForgot": "Mot de passe oublié ?",
"otpAuth": "Authentification à deux facteurs",
"otpAuthDescription": "Entrez le code de votre application d'authentification ou l'un de vos codes de secours à usage unique.",
"otpAuthSubmit": "Soumettre le code",
"idpContinue": "Ou continuer avec",
"otpAuthBack": "Retour à la connexion",
"otpAuthBack": "Back to Password",
"navbar": "Menu de navigation",
"navbarDescription": "Menu de navigation principal de l'application",
"navbarDocsLink": "Documentation",
@@ -1189,6 +1219,7 @@
"sidebarOverview": "Aperçu",
"sidebarHome": "Domicile",
"sidebarSites": "Nœuds",
"sidebarApprovals": "Approval Requests",
"sidebarResources": "Ressource",
"sidebarProxyResources": "Publique",
"sidebarClientResources": "Privé",
@@ -1205,7 +1236,7 @@
"sidebarIdentityProviders": "Fournisseurs d'identité",
"sidebarLicense": "Licence",
"sidebarClients": "Clients",
"sidebarUserDevices": "Utilisateurs",
"sidebarUserDevices": "User Devices",
"sidebarMachineClients": "Machines",
"sidebarDomains": "Domaines",
"sidebarGeneral": "Gérer",
@@ -1277,6 +1308,7 @@
"setupErrorCreateAdmin": "Une erreur s'est produite lors de la création du compte administrateur du serveur.",
"certificateStatus": "Statut du certificat",
"loading": "Chargement",
"loadingAnalytics": "Loading Analytics",
"restart": "Redémarrer",
"domains": "Domaines",
"domainsDescription": "Créer et gérer les domaines disponibles dans l'organisation",
@@ -1304,6 +1336,7 @@
"refreshError": "Échec de l'actualisation des données",
"verified": "Vérifié",
"pending": "En attente",
"pendingApproval": "Pending Approval",
"sidebarBilling": "Facturation",
"billing": "Facturation",
"orgBillingDescription": "Gérer les informations de facturation et les abonnements",
@@ -1420,7 +1453,7 @@
"securityKeyRemoveSuccess": "Clé de sécurité supprimée avec succès",
"securityKeyRemoveError": "Échec de la suppression de la clé de sécurité",
"securityKeyLoadError": "Échec du chargement des clés de sécurité",
"securityKeyLogin": "Continuer avec une clé de sécurité",
"securityKeyLogin": "Use Security Key",
"securityKeyAuthError": "Échec de l'authentification avec la clé de sécurité",
"securityKeyRecommendation": "Envisagez d'enregistrer une autre clé de sécurité sur un appareil différent pour vous assurer de ne pas être bloqué de votre compte.",
"registering": "Enregistrement...",
@@ -1547,6 +1580,8 @@
"IntervalSeconds": "Intervalle sain",
"timeoutSeconds": "Délai d'attente (sec)",
"timeIsInSeconds": "Le temps est exprimé en secondes",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need their devices approved by an admin before they can access resources",
"retryAttempts": "Tentatives de réessai",
"expectedResponseCodes": "Codes de réponse attendus",
"expectedResponseCodesDescription": "Code de statut HTTP indiquant un état de santé satisfaisant. Si non renseigné, 200-300 est considéré comme satisfaisant.",
@@ -1876,7 +1911,7 @@
"orgAuthChooseIdpDescription": "Choisissez votre fournisseur d'identité pour continuer",
"orgAuthNoIdpConfigured": "Cette organisation n'a aucun fournisseur d'identité configuré. Vous pouvez vous connecter avec votre identité Pangolin à la place.",
"orgAuthSignInWithPangolin": "Se connecter avec Pangolin",
"orgAuthSignInToOrg": "Connectez-vous à une organisation",
"orgAuthSignInToOrg": "Sign in to an organization",
"orgAuthSelectOrgTitle": "Connexion à l'organisation",
"orgAuthSelectOrgDescription": "Entrez votre identifiant d'organisation pour continuer",
"orgAuthOrgIdPlaceholder": "votre-organisation",
@@ -2232,6 +2267,8 @@
"deviceCodeInvalidFormat": "Le code doit contenir 9 caractères (par exemple, A1AJ-N5JD)",
"deviceCodeInvalidOrExpired": "Code invalide ou expiré",
"deviceCodeVerifyFailed": "Impossible de vérifier le code de l'appareil",
"deviceCodeValidating": "Validating device code...",
"deviceCodeVerifying": "Verifying device authorization...",
"signedInAs": "Connecté en tant que",
"deviceCodeEnterPrompt": "Entrez le code affiché sur l'appareil",
"continue": "Continuer",
@@ -2244,7 +2281,7 @@
"deviceOrganizationsAccess": "Accès à toutes les organisations auxquelles votre compte a accès",
"deviceAuthorize": "Autoriser {applicationName}",
"deviceConnected": "Appareil connecté !",
"deviceAuthorizedMessage": "L'appareil est autorisé à accéder à votre compte.",
"deviceAuthorizedMessage": "Device is authorized to access your account. Please return to the client application.",
"pangolinCloud": "Nuage de Pangolin",
"viewDevices": "Voir les appareils",
"viewDevicesDescription": "Gérer vos appareils connectés",
@@ -2306,6 +2343,7 @@
"identifier": "Identifiant",
"deviceLoginUseDifferentAccount": "Pas vous ? Utilisez un autre compte.",
"deviceLoginDeviceRequestingAccessToAccount": "Un appareil demande l'accès à ce compte.",
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
"noData": "Aucune donnée",
"machineClients": "Clients Machines",
"install": "Installer",
@@ -2394,5 +2432,56 @@
"maintenanceScreenTitle": "Service temporairement indisponible",
"maintenanceScreenMessage": "Nous rencontrons actuellement des difficultés techniques. Veuillez vérifier ultérieurement.",
"maintenanceScreenEstimatedCompletion": "Achèvement estimé :",
"createInternalResourceDialogDestinationRequired": "La destination est requise"
"createInternalResourceDialogDestinationRequired": "La destination est requise",
"available": "Available",
"archived": "Archived",
"noArchivedDevices": "No archived devices found",
"deviceArchived": "Device archived",
"deviceArchivedDescription": "The device has been successfully archived.",
"errorArchivingDevice": "Error archiving device",
"failedToArchiveDevice": "Failed to archive device",
"deviceQuestionArchive": "Are you sure you want to archive this device?",
"deviceMessageArchive": "The device will be archived and removed from your active devices list.",
"deviceArchiveConfirm": "Archive Device",
"archiveDevice": "Archive Device",
"archive": "Archive",
"deviceUnarchived": "Device unarchived",
"deviceUnarchivedDescription": "The device has been successfully unarchived.",
"errorUnarchivingDevice": "Error unarchiving device",
"failedToUnarchiveDevice": "Failed to unarchive device",
"unarchive": "Unarchive",
"archiveClient": "Archive Client",
"archiveClientQuestion": "Are you sure you want to archive this client?",
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
"archiveClientConfirm": "Archive Client",
"blockClient": "Block Client",
"blockClientQuestion": "Are you sure you want to block this client?",
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
"blockClientConfirm": "Block Client",
"active": "Active",
"usernameOrEmail": "Username or Email",
"selectYourOrganization": "Select your organization",
"signInTo": "Log in in to",
"signInWithPassword": "Continue with Password",
"noAuthMethodsAvailable": "No authentication methods available for this organization.",
"enterPassword": "Enter your password",
"enterMfaCode": "Enter the code from your authenticator app",
"securityKeyRequired": "Please use your security key to sign in.",
"needToUseAnotherAccount": "Need to use a different account?",
"loginLegalDisclaimer": "By clicking the buttons below, you acknowledge you have read, understand, and agree to the <termsOfService>Terms of Service</termsOfService> and <privacyPolicy>Privacy Policy</privacyPolicy>.",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"userNotFoundWithUsername": "No user found with that username.",
"verify": "Verify",
"signIn": "Sign In",
"forgotPassword": "Forgot password?",
"orgSignInTip": "If you've logged in before, you can enter your username or email above to authenticate with your organization's identity provider instead. It's easier!",
"continueAnyway": "Continue anyway",
"dontShowAgain": "Don't show again",
"orgSignInNotice": "Did you know?",
"signupOrgNotice": "Trying to sign in?",
"signupOrgTip": "Are you trying to sign in through your organization's identity provider?",
"signupOrgLink": "Sign in or sign up with your organization instead",
"verifyEmailLogInWithDifferentAccount": "Use a Different Account",
"logIn": "Log In"
}

View File

@@ -257,6 +257,8 @@
"accessRolesSearch": "Ricerca ruoli...",
"accessRolesAdd": "Aggiungi Ruolo",
"accessRoleDelete": "Elimina Ruolo",
"accessApprovalsManage": "Manage Approvals",
"accessApprovalsDescription": "Manage approval requests in the organization",
"description": "Descrizione",
"inviteTitle": "Inviti Aperti",
"inviteDescription": "Gestisci gli inviti per gli altri utenti a unirsi all'organizzazione",
@@ -450,6 +452,18 @@
"selectDuration": "Seleziona durata",
"selectResource": "Seleziona Risorsa",
"filterByResource": "Filtra Per Risorsa",
"selectApprovalState": "Select Approval State",
"filterByApprovalState": "Filter By Approval State",
"approvalListEmpty": "No approvals",
"approvalState": "Approval State",
"approve": "Approve",
"approved": "Approved",
"denied": "Denied",
"deniedApproval": "Denied Approval",
"all": "All",
"deny": "Deny",
"viewDetails": "View Details",
"requestingNewDeviceApproval": "requested a new device",
"resetFilters": "Ripristina Filtri",
"totalBlocked": "Richieste Bloccate Da Pangolino",
"totalRequests": "Totale Richieste",
@@ -729,16 +743,28 @@
"countries": "Paesi",
"accessRoleCreate": "Crea Ruolo",
"accessRoleCreateDescription": "Crea un nuovo ruolo per raggruppare gli utenti e gestire i loro permessi.",
"accessRoleEdit": "Edit Role",
"accessRoleEditDescription": "Edit role information.",
"accessRoleCreateSubmit": "Crea Ruolo",
"accessRoleCreated": "Ruolo creato",
"accessRoleCreatedDescription": "Il ruolo è stato creato con successo.",
"accessRoleErrorCreate": "Impossibile creare il ruolo",
"accessRoleErrorCreateDescription": "Si è verificato un errore durante la creazione del ruolo.",
"accessRoleUpdateSubmit": "Update Role",
"accessRoleUpdated": "Role updated",
"accessRoleUpdatedDescription": "The role has been successfully updated.",
"accessApprovalUpdated": "Approval processed",
"accessApprovalApprovedDescription": "Set Approval Request decision to approved.",
"accessApprovalDeniedDescription": "Set Approval Request decision to denied.",
"accessRoleErrorUpdate": "Failed to update role",
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
"accessApprovalErrorUpdate": "Failed to process approval",
"accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.",
"accessRoleErrorNewRequired": "Nuovo ruolo richiesto",
"accessRoleErrorRemove": "Impossibile rimuovere il ruolo",
"accessRoleErrorRemoveDescription": "Si è verificato un errore durante la rimozione del ruolo.",
"accessRoleName": "Nome Del Ruolo",
"accessRoleQuestionRemove": "Stai per eliminare il ruolo {name}. Non puoi annullare questa azione.",
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
"accessRoleRemove": "Rimuovi Ruolo",
"accessRoleRemoveDescription": "Rimuovi un ruolo dall'organizzazione",
"accessRoleRemoveSubmit": "Rimuovi Ruolo",
@@ -874,7 +900,7 @@
"inviteAlready": "Sembra che sei stato invitato!",
"inviteAlreadyDescription": "Per accettare l'invito, devi accedere o creare un account.",
"signupQuestion": "Hai già un account?",
"login": "Accedi",
"login": "Log In",
"resourceNotFound": "Risorsa Non Trovata",
"resourceNotFoundDescription": "La risorsa che stai cercando di accedere non esiste.",
"pincodeRequirementsLength": "Il PIN deve essere esattamente di 6 cifre",
@@ -954,13 +980,13 @@
"passwordExpiryDescription": "Questa organizzazione richiede di cambiare la password ogni {maxDays} giorni.",
"changePasswordNow": "Cambia Password Ora",
"pincodeAuth": "Codice Autenticatore",
"pincodeSubmit2": "Invia Codice",
"pincodeSubmit2": "Submit code",
"passwordResetSubmit": "Richiedi Reset",
"passwordResetAlreadyHaveCode": "Inserisci Codice",
"passwordResetSmtpRequired": "Si prega di contattare l'amministratore",
"passwordResetSmtpRequiredDescription": "Per reimpostare la password è necessario un codice di reimpostazione della password. Si prega di contattare l'amministratore per assistenza.",
"passwordBack": "Torna alla Password",
"loginBack": "Torna al login",
"loginBack": "Go back to main login page",
"signup": "Registrati",
"loginStart": "Accedi per iniziare",
"idpOidcTokenValidating": "Convalida token OIDC",
@@ -1118,6 +1144,10 @@
"actionUpdateIdpOrg": "Aggiorna Org IDP",
"actionCreateClient": "Crea Client",
"actionDeleteClient": "Elimina Client",
"actionArchiveClient": "Archive Client",
"actionUnarchiveClient": "Unarchive Client",
"actionBlockClient": "Block Client",
"actionUnblockClient": "Unblock Client",
"actionUpdateClient": "Aggiorna Client",
"actionListClients": "Elenco Clienti",
"actionGetClient": "Ottieni Client",
@@ -1134,14 +1164,14 @@
"searchProgress": "Ricerca...",
"create": "Crea",
"orgs": "Organizzazioni",
"loginError": "Si è verificato un errore durante l'accesso",
"loginRequiredForDevice": "È richiesto il login per autenticare il dispositivo.",
"loginError": "An unexpected error occurred. Please try again.",
"loginRequiredForDevice": "Login is required for your device.",
"passwordForgot": "Password dimenticata?",
"otpAuth": "Autenticazione a Due Fattori",
"otpAuthDescription": "Inserisci il codice dalla tua app di autenticazione o uno dei tuoi codici di backup monouso.",
"otpAuthSubmit": "Invia Codice",
"idpContinue": "O continua con",
"otpAuthBack": "Torna al Login",
"otpAuthBack": "Back to Password",
"navbar": "Menu di Navigazione",
"navbarDescription": "Menu di navigazione principale dell'applicazione",
"navbarDocsLink": "Documentazione",
@@ -1189,6 +1219,7 @@
"sidebarOverview": "Panoramica",
"sidebarHome": "Home",
"sidebarSites": "Siti",
"sidebarApprovals": "Approval Requests",
"sidebarResources": "Risorse",
"sidebarProxyResources": "Pubblico",
"sidebarClientResources": "Privato",
@@ -1205,7 +1236,7 @@
"sidebarIdentityProviders": "Fornitori Di Identità",
"sidebarLicense": "Licenza",
"sidebarClients": "Client",
"sidebarUserDevices": "Utenti",
"sidebarUserDevices": "User Devices",
"sidebarMachineClients": "Macchine",
"sidebarDomains": "Domini",
"sidebarGeneral": "Gestisci",
@@ -1277,6 +1308,7 @@
"setupErrorCreateAdmin": "Si è verificato un errore durante la creazione dell'account amministratore del server.",
"certificateStatus": "Stato del Certificato",
"loading": "Caricamento",
"loadingAnalytics": "Loading Analytics",
"restart": "Riavvia",
"domains": "Domini",
"domainsDescription": "Creare e gestire i domini disponibili nell'organizzazione",
@@ -1304,6 +1336,7 @@
"refreshError": "Impossibile aggiornare i dati",
"verified": "Verificato",
"pending": "In attesa",
"pendingApproval": "Pending Approval",
"sidebarBilling": "Fatturazione",
"billing": "Fatturazione",
"orgBillingDescription": "Gestisci le informazioni di fatturazione e gli abbonamenti",
@@ -1420,7 +1453,7 @@
"securityKeyRemoveSuccess": "Chiave di sicurezza rimossa con successo",
"securityKeyRemoveError": "Errore durante la rimozione della chiave di sicurezza",
"securityKeyLoadError": "Errore durante il caricamento delle chiavi di sicurezza",
"securityKeyLogin": "Continua con la chiave di sicurezza",
"securityKeyLogin": "Use Security Key",
"securityKeyAuthError": "Errore durante l'autenticazione con chiave di sicurezza",
"securityKeyRecommendation": "Considera di registrare un'altra chiave di sicurezza su un dispositivo diverso per assicurarti di non rimanere bloccato fuori dal tuo account.",
"registering": "Registrazione in corso...",
@@ -1547,6 +1580,8 @@
"IntervalSeconds": "Intervallo Sano",
"timeoutSeconds": "Timeout (sec)",
"timeIsInSeconds": "Il tempo è in secondi",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need their devices approved by an admin before they can access resources",
"retryAttempts": "Tentativi di Riprova",
"expectedResponseCodes": "Codici di Risposta Attesi",
"expectedResponseCodesDescription": "Codice di stato HTTP che indica lo stato di salute. Se lasciato vuoto, considerato sano è compreso tra 200-300.",
@@ -1876,7 +1911,7 @@
"orgAuthChooseIdpDescription": "Scegli il tuo provider di identità per continuare",
"orgAuthNoIdpConfigured": "Questa organizzazione non ha nessun provider di identità configurato. Puoi accedere con la tua identità Pangolin.",
"orgAuthSignInWithPangolin": "Accedi con Pangolino",
"orgAuthSignInToOrg": "Accedi a un'organizzazione",
"orgAuthSignInToOrg": "Sign in to an organization",
"orgAuthSelectOrgTitle": "Accesso Organizzazione",
"orgAuthSelectOrgDescription": "Inserisci l'ID dell'organizzazione per continuare",
"orgAuthOrgIdPlaceholder": "la-tua-organizzazione",
@@ -2232,6 +2267,8 @@
"deviceCodeInvalidFormat": "Il codice deve contenere 9 caratteri (es. A1AJ-N5JD)",
"deviceCodeInvalidOrExpired": "Codice non valido o scaduto",
"deviceCodeVerifyFailed": "Impossibile verificare il codice del dispositivo",
"deviceCodeValidating": "Validating device code...",
"deviceCodeVerifying": "Verifying device authorization...",
"signedInAs": "Accesso come",
"deviceCodeEnterPrompt": "Inserisci il codice visualizzato sul dispositivo",
"continue": "Continua",
@@ -2244,7 +2281,7 @@
"deviceOrganizationsAccess": "Accesso a tutte le organizzazioni a cui il tuo account ha accesso",
"deviceAuthorize": "Autorizza {applicationName}",
"deviceConnected": "Dispositivo Connesso!",
"deviceAuthorizedMessage": "Il dispositivo è autorizzato ad accedere al tuo account.",
"deviceAuthorizedMessage": "Device is authorized to access your account. Please return to the client application.",
"pangolinCloud": "Pangolin Cloud",
"viewDevices": "Visualizza Dispositivi",
"viewDevicesDescription": "Gestisci i tuoi dispositivi connessi",
@@ -2306,6 +2343,7 @@
"identifier": "Identifier",
"deviceLoginUseDifferentAccount": "Non tu? Usa un account diverso.",
"deviceLoginDeviceRequestingAccessToAccount": "Un dispositivo sta richiedendo l'accesso a questo account.",
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
"noData": "Nessun Dato",
"machineClients": "Machine Clients",
"install": "Installa",
@@ -2394,5 +2432,56 @@
"maintenanceScreenTitle": "Servizio Temporaneamente Non Disponibile",
"maintenanceScreenMessage": "Stiamo attualmente riscontrando difficoltà tecniche. Si prega di ricontrollare a breve.",
"maintenanceScreenEstimatedCompletion": "Completamento Stimato:",
"createInternalResourceDialogDestinationRequired": "Destinazione richiesta"
"createInternalResourceDialogDestinationRequired": "Destinazione richiesta",
"available": "Available",
"archived": "Archived",
"noArchivedDevices": "No archived devices found",
"deviceArchived": "Device archived",
"deviceArchivedDescription": "The device has been successfully archived.",
"errorArchivingDevice": "Error archiving device",
"failedToArchiveDevice": "Failed to archive device",
"deviceQuestionArchive": "Are you sure you want to archive this device?",
"deviceMessageArchive": "The device will be archived and removed from your active devices list.",
"deviceArchiveConfirm": "Archive Device",
"archiveDevice": "Archive Device",
"archive": "Archive",
"deviceUnarchived": "Device unarchived",
"deviceUnarchivedDescription": "The device has been successfully unarchived.",
"errorUnarchivingDevice": "Error unarchiving device",
"failedToUnarchiveDevice": "Failed to unarchive device",
"unarchive": "Unarchive",
"archiveClient": "Archive Client",
"archiveClientQuestion": "Are you sure you want to archive this client?",
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
"archiveClientConfirm": "Archive Client",
"blockClient": "Block Client",
"blockClientQuestion": "Are you sure you want to block this client?",
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
"blockClientConfirm": "Block Client",
"active": "Active",
"usernameOrEmail": "Username or Email",
"selectYourOrganization": "Select your organization",
"signInTo": "Log in in to",
"signInWithPassword": "Continue with Password",
"noAuthMethodsAvailable": "No authentication methods available for this organization.",
"enterPassword": "Enter your password",
"enterMfaCode": "Enter the code from your authenticator app",
"securityKeyRequired": "Please use your security key to sign in.",
"needToUseAnotherAccount": "Need to use a different account?",
"loginLegalDisclaimer": "By clicking the buttons below, you acknowledge you have read, understand, and agree to the <termsOfService>Terms of Service</termsOfService> and <privacyPolicy>Privacy Policy</privacyPolicy>.",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"userNotFoundWithUsername": "No user found with that username.",
"verify": "Verify",
"signIn": "Sign In",
"forgotPassword": "Forgot password?",
"orgSignInTip": "If you've logged in before, you can enter your username or email above to authenticate with your organization's identity provider instead. It's easier!",
"continueAnyway": "Continue anyway",
"dontShowAgain": "Don't show again",
"orgSignInNotice": "Did you know?",
"signupOrgNotice": "Trying to sign in?",
"signupOrgTip": "Are you trying to sign in through your organization's identity provider?",
"signupOrgLink": "Sign in or sign up with your organization instead",
"verifyEmailLogInWithDifferentAccount": "Use a Different Account",
"logIn": "Log In"
}

View File

@@ -257,6 +257,8 @@
"accessRolesSearch": "역할 검색...",
"accessRolesAdd": "역할 추가",
"accessRoleDelete": "역할 삭제",
"accessApprovalsManage": "Manage Approvals",
"accessApprovalsDescription": "Manage approval requests in the organization",
"description": "설명",
"inviteTitle": "열린 초대",
"inviteDescription": "다른 사용자가 조직에 참여하도록 초대장을 관리합니다.",
@@ -450,6 +452,18 @@
"selectDuration": "지속 시간 선택",
"selectResource": "리소스 선택",
"filterByResource": "리소스별 필터",
"selectApprovalState": "Select Approval State",
"filterByApprovalState": "Filter By Approval State",
"approvalListEmpty": "No approvals",
"approvalState": "Approval State",
"approve": "Approve",
"approved": "Approved",
"denied": "Denied",
"deniedApproval": "Denied Approval",
"all": "All",
"deny": "Deny",
"viewDetails": "View Details",
"requestingNewDeviceApproval": "requested a new device",
"resetFilters": "필터 재설정",
"totalBlocked": "Pangolin으로 차단된 요청",
"totalRequests": "총 요청 수",
@@ -729,16 +743,28 @@
"countries": "국가",
"accessRoleCreate": "역할 생성",
"accessRoleCreateDescription": "사용자를 그룹화하고 권한을 관리하기 위해 새 역할을 생성하세요.",
"accessRoleEdit": "Edit Role",
"accessRoleEditDescription": "Edit role information.",
"accessRoleCreateSubmit": "역할 생성",
"accessRoleCreated": "역할이 생성되었습니다.",
"accessRoleCreatedDescription": "역할이 성공적으로 생성되었습니다.",
"accessRoleErrorCreate": "역할 생성 실패",
"accessRoleErrorCreateDescription": "역할 생성 중 오류가 발생했습니다.",
"accessRoleUpdateSubmit": "Update Role",
"accessRoleUpdated": "Role updated",
"accessRoleUpdatedDescription": "The role has been successfully updated.",
"accessApprovalUpdated": "Approval processed",
"accessApprovalApprovedDescription": "Set Approval Request decision to approved.",
"accessApprovalDeniedDescription": "Set Approval Request decision to denied.",
"accessRoleErrorUpdate": "Failed to update role",
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
"accessApprovalErrorUpdate": "Failed to process approval",
"accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.",
"accessRoleErrorNewRequired": "새 역할이 필요합니다.",
"accessRoleErrorRemove": "역할 제거에 실패했습니다.",
"accessRoleErrorRemoveDescription": "역할을 제거하는 동안 오류가 발생했습니다.",
"accessRoleName": "역할 이름",
"accessRoleQuestionRemove": "{name} 역할을 삭제하려고 합니다. 이 작업은 취소할 수 없습니다.",
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
"accessRoleRemove": "역할 제거",
"accessRoleRemoveDescription": "조직에서 역할 제거",
"accessRoleRemoveSubmit": "역할 제거",
@@ -874,7 +900,7 @@
"inviteAlready": "초대받은 것 같습니다!",
"inviteAlreadyDescription": "초대를 수락하려면 로그인하거나 계정을 생성해야 합니다.",
"signupQuestion": "이미 계정이 있습니까?",
"login": "로그인",
"login": "Log In",
"resourceNotFound": "리소스를 찾을 수 없습니다",
"resourceNotFoundDescription": "접근하려는 리소스가 존재하지 않습니다.",
"pincodeRequirementsLength": "PIN은 정확히 6자리여야 합니다",
@@ -954,13 +980,13 @@
"passwordExpiryDescription": "이 조직은 {maxDays}일마다 비밀번호 변경을 요구합니다.",
"changePasswordNow": "지금 비밀번호 변경",
"pincodeAuth": "인증 코드",
"pincodeSubmit2": "코드 제출",
"pincodeSubmit2": "Submit code",
"passwordResetSubmit": "재설정 요청",
"passwordResetAlreadyHaveCode": "코드를 입력하십시오.",
"passwordResetSmtpRequired": "관리자에게 문의하십시오",
"passwordResetSmtpRequiredDescription": "비밀번호를 재설정하려면 비밀번호 초기화 코드가 필요합니다. 지원을 받으려면 관리자에게 문의하십시오.",
"passwordBack": "비밀번호로 돌아가기",
"loginBack": "로그인으로 돌아가기",
"loginBack": "Go back to main login page",
"signup": "가입하기",
"loginStart": "시작하려면 로그인하세요.",
"idpOidcTokenValidating": "OIDC 토큰 검증 중",
@@ -1118,6 +1144,10 @@
"actionUpdateIdpOrg": "IDP 조직 업데이트",
"actionCreateClient": "클라이언트 생성",
"actionDeleteClient": "클라이언트 삭제",
"actionArchiveClient": "Archive Client",
"actionUnarchiveClient": "Unarchive Client",
"actionBlockClient": "Block Client",
"actionUnblockClient": "Unblock Client",
"actionUpdateClient": "클라이언트 업데이트",
"actionListClients": "클라이언트 목록",
"actionGetClient": "클라이언트 가져오기",
@@ -1134,14 +1164,14 @@
"searchProgress": "검색...",
"create": "생성",
"orgs": "조직",
"loginError": "로그인 중 오류가 발생했습니다",
"loginRequiredForDevice": "장치를 인증하려면 로그인이 필요합니다.",
"loginError": "An unexpected error occurred. Please try again.",
"loginRequiredForDevice": "Login is required for your device.",
"passwordForgot": "비밀번호를 잊으셨나요?",
"otpAuth": "이중 인증",
"otpAuthDescription": "인증 앱에서 코드를 입력하거나 단일 사용 백업 코드 중 하나를 입력하세요.",
"otpAuthSubmit": "코드 제출",
"idpContinue": "또는 계속 진행하십시오.",
"otpAuthBack": "로그인으로 돌아가기",
"otpAuthBack": "Back to Password",
"navbar": "탐색 메뉴",
"navbarDescription": "애플리케이션의 주요 탐색 메뉴",
"navbarDocsLink": "문서",
@@ -1189,6 +1219,7 @@
"sidebarOverview": "개요",
"sidebarHome": "홈",
"sidebarSites": "사이트",
"sidebarApprovals": "Approval Requests",
"sidebarResources": "리소스",
"sidebarProxyResources": "공유",
"sidebarClientResources": "비공개",
@@ -1205,7 +1236,7 @@
"sidebarIdentityProviders": "신원 공급자",
"sidebarLicense": "라이선스",
"sidebarClients": "클라이언트",
"sidebarUserDevices": "사용자",
"sidebarUserDevices": "User Devices",
"sidebarMachineClients": "기계",
"sidebarDomains": "도메인",
"sidebarGeneral": "관리",
@@ -1277,6 +1308,7 @@
"setupErrorCreateAdmin": "서버 관리자 계정을 생성하는 동안 오류가 발생했습니다.",
"certificateStatus": "인증서 상태",
"loading": "로딩 중",
"loadingAnalytics": "Loading Analytics",
"restart": "재시작",
"domains": "도메인",
"domainsDescription": "조직에서 사용 가능한 도메인 생성 및 관리",
@@ -1304,6 +1336,7 @@
"refreshError": "데이터 새로고침 실패",
"verified": "검증됨",
"pending": "대기 중",
"pendingApproval": "Pending Approval",
"sidebarBilling": "청구",
"billing": "청구",
"orgBillingDescription": "청구 정보 및 구독을 관리하세요",
@@ -1420,7 +1453,7 @@
"securityKeyRemoveSuccess": "보안 키가 성공적으로 제거되었습니다",
"securityKeyRemoveError": "보안 키 제거 실패",
"securityKeyLoadError": "보안 키를 불러오는 데 실패했습니다",
"securityKeyLogin": "보안 키로 계속하기",
"securityKeyLogin": "Use Security Key",
"securityKeyAuthError": "보안 키를 사용한 인증 실패",
"securityKeyRecommendation": "항상 계정에 액세스할 수 있도록 다른 장치에 백업 보안 키를 등록하세요.",
"registering": "등록 중...",
@@ -1547,6 +1580,8 @@
"IntervalSeconds": "정상 간격",
"timeoutSeconds": "타임아웃(초)",
"timeIsInSeconds": "시간은 초 단위입니다",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need their devices approved by an admin before they can access resources",
"retryAttempts": "재시도 횟수",
"expectedResponseCodes": "예상 응답 코드",
"expectedResponseCodesDescription": "정상 상태를 나타내는 HTTP 상태 코드입니다. 비워 두면 200-300이 정상으로 간주됩니다.",
@@ -1876,7 +1911,7 @@
"orgAuthChooseIdpDescription": "계속하려면 신원 공급자를 선택하세요.",
"orgAuthNoIdpConfigured": "이 조직은 구성된 신원 공급자가 없습니다. 대신 Pangolin 아이덴티티로 로그인할 수 있습니다.",
"orgAuthSignInWithPangolin": "Pangolin으로 로그인",
"orgAuthSignInToOrg": "조직에 로그인합니다.",
"orgAuthSignInToOrg": "Sign in to an organization",
"orgAuthSelectOrgTitle": "조직 로그인",
"orgAuthSelectOrgDescription": "계속하려면 조직 ID를 입력하십시오.",
"orgAuthOrgIdPlaceholder": "your-organization",
@@ -2232,6 +2267,8 @@
"deviceCodeInvalidFormat": "코드는 9자리여야 합니다 (예: A1AJ-N5JD)",
"deviceCodeInvalidOrExpired": "무효하거나 만료된 코드",
"deviceCodeVerifyFailed": "이메일 확인에 실패했습니다:",
"deviceCodeValidating": "Validating device code...",
"deviceCodeVerifying": "Verifying device authorization...",
"signedInAs": "로그인한 사용자",
"deviceCodeEnterPrompt": "기기에 표시된 코드를 입력하세요",
"continue": "계속 진행하기",
@@ -2244,7 +2281,7 @@
"deviceOrganizationsAccess": "계정이 접근할 수 있는 모든 조직에 대한 접근",
"deviceAuthorize": "{applicationName} 권한 부여",
"deviceConnected": "장치가 연결되었습니다!",
"deviceAuthorizedMessage": "장치가 계정에 액세스할 수 있도록 승인되었습니다.",
"deviceAuthorizedMessage": "Device is authorized to access your account. Please return to the client application.",
"pangolinCloud": "판골린 클라우드",
"viewDevices": "장치 보기",
"viewDevicesDescription": "연결된 장치를 관리하십시오",
@@ -2306,6 +2343,7 @@
"identifier": "식별자",
"deviceLoginUseDifferentAccount": "본인이 아닙니까? 다른 계정을 사용하세요.",
"deviceLoginDeviceRequestingAccessToAccount": "장치가 이 계정에 접근하려고 합니다.",
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
"noData": "데이터 없음",
"machineClients": "기계 클라이언트",
"install": "설치",
@@ -2394,5 +2432,56 @@
"maintenanceScreenTitle": "서비스 일시 중단",
"maintenanceScreenMessage": "현재 기술적 문제를 겪고 있습니다. 곧 다시 확인하십시오.",
"maintenanceScreenEstimatedCompletion": "예상 완료:",
"createInternalResourceDialogDestinationRequired": "목적지가 필요합니다."
"createInternalResourceDialogDestinationRequired": "목적지가 필요합니다.",
"available": "Available",
"archived": "Archived",
"noArchivedDevices": "No archived devices found",
"deviceArchived": "Device archived",
"deviceArchivedDescription": "The device has been successfully archived.",
"errorArchivingDevice": "Error archiving device",
"failedToArchiveDevice": "Failed to archive device",
"deviceQuestionArchive": "Are you sure you want to archive this device?",
"deviceMessageArchive": "The device will be archived and removed from your active devices list.",
"deviceArchiveConfirm": "Archive Device",
"archiveDevice": "Archive Device",
"archive": "Archive",
"deviceUnarchived": "Device unarchived",
"deviceUnarchivedDescription": "The device has been successfully unarchived.",
"errorUnarchivingDevice": "Error unarchiving device",
"failedToUnarchiveDevice": "Failed to unarchive device",
"unarchive": "Unarchive",
"archiveClient": "Archive Client",
"archiveClientQuestion": "Are you sure you want to archive this client?",
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
"archiveClientConfirm": "Archive Client",
"blockClient": "Block Client",
"blockClientQuestion": "Are you sure you want to block this client?",
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
"blockClientConfirm": "Block Client",
"active": "Active",
"usernameOrEmail": "Username or Email",
"selectYourOrganization": "Select your organization",
"signInTo": "Log in in to",
"signInWithPassword": "Continue with Password",
"noAuthMethodsAvailable": "No authentication methods available for this organization.",
"enterPassword": "Enter your password",
"enterMfaCode": "Enter the code from your authenticator app",
"securityKeyRequired": "Please use your security key to sign in.",
"needToUseAnotherAccount": "Need to use a different account?",
"loginLegalDisclaimer": "By clicking the buttons below, you acknowledge you have read, understand, and agree to the <termsOfService>Terms of Service</termsOfService> and <privacyPolicy>Privacy Policy</privacyPolicy>.",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"userNotFoundWithUsername": "No user found with that username.",
"verify": "Verify",
"signIn": "Sign In",
"forgotPassword": "Forgot password?",
"orgSignInTip": "If you've logged in before, you can enter your username or email above to authenticate with your organization's identity provider instead. It's easier!",
"continueAnyway": "Continue anyway",
"dontShowAgain": "Don't show again",
"orgSignInNotice": "Did you know?",
"signupOrgNotice": "Trying to sign in?",
"signupOrgTip": "Are you trying to sign in through your organization's identity provider?",
"signupOrgLink": "Sign in or sign up with your organization instead",
"verifyEmailLogInWithDifferentAccount": "Use a Different Account",
"logIn": "Log In"
}

View File

@@ -257,6 +257,8 @@
"accessRolesSearch": "Søk etter roller...",
"accessRolesAdd": "Legg til rolle",
"accessRoleDelete": "Slett rolle",
"accessApprovalsManage": "Manage Approvals",
"accessApprovalsDescription": "Manage approval requests in the organization",
"description": "Beskrivelse",
"inviteTitle": "Åpne invitasjoner",
"inviteDescription": "Administrer invitasjoner til andre brukere for å bli med i organisasjonen",
@@ -450,6 +452,18 @@
"selectDuration": "Velg varighet",
"selectResource": "Velg ressurs",
"filterByResource": "Filtrer etter ressurser",
"selectApprovalState": "Select Approval State",
"filterByApprovalState": "Filter By Approval State",
"approvalListEmpty": "No approvals",
"approvalState": "Approval State",
"approve": "Approve",
"approved": "Approved",
"denied": "Denied",
"deniedApproval": "Denied Approval",
"all": "All",
"deny": "Deny",
"viewDetails": "View Details",
"requestingNewDeviceApproval": "requested a new device",
"resetFilters": "Tilbakestill filtre",
"totalBlocked": "Forespørsler blokkert av Pangolin",
"totalRequests": "Totalt antall forespørsler",
@@ -729,16 +743,28 @@
"countries": "Land",
"accessRoleCreate": "Opprett rolle",
"accessRoleCreateDescription": "Opprett en ny rolle for å gruppere brukere og administrere deres tillatelser.",
"accessRoleEdit": "Edit Role",
"accessRoleEditDescription": "Edit role information.",
"accessRoleCreateSubmit": "Opprett rolle",
"accessRoleCreated": "Rolle opprettet",
"accessRoleCreatedDescription": "Rollen er vellykket opprettet.",
"accessRoleErrorCreate": "Klarte ikke å opprette rolle",
"accessRoleErrorCreateDescription": "Det oppstod en feil under opprettelse av rollen.",
"accessRoleUpdateSubmit": "Update Role",
"accessRoleUpdated": "Role updated",
"accessRoleUpdatedDescription": "The role has been successfully updated.",
"accessApprovalUpdated": "Approval processed",
"accessApprovalApprovedDescription": "Set Approval Request decision to approved.",
"accessApprovalDeniedDescription": "Set Approval Request decision to denied.",
"accessRoleErrorUpdate": "Failed to update role",
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
"accessApprovalErrorUpdate": "Failed to process approval",
"accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.",
"accessRoleErrorNewRequired": "Ny rolle kreves",
"accessRoleErrorRemove": "Kunne ikke fjerne rolle",
"accessRoleErrorRemoveDescription": "Det oppstod en feil under fjerning av rollen.",
"accessRoleName": "Rollenavn",
"accessRoleQuestionRemove": "Du er i ferd med å slette rollen {name}. Du kan ikke angre denne handlingen.",
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
"accessRoleRemove": "Fjern Rolle",
"accessRoleRemoveDescription": "Fjern en rolle fra organisasjonen",
"accessRoleRemoveSubmit": "Fjern Rolle",
@@ -874,7 +900,7 @@
"inviteAlready": "Ser ut til at du har blitt invitert!",
"inviteAlreadyDescription": "For å godta invitasjonen, må du logge inn eller opprette en konto.",
"signupQuestion": "Har du allerede en konto?",
"login": "Logg inn",
"login": "Log In",
"resourceNotFound": "Ressurs ikke funnet",
"resourceNotFoundDescription": "Ressursen du prøver å få tilgang til eksisterer ikke.",
"pincodeRequirementsLength": "PIN må være nøyaktig 6 siffer",
@@ -954,13 +980,13 @@
"passwordExpiryDescription": "Denne organisasjonen krever at du bytter passord hver {maxDays} dag.",
"changePasswordNow": "Bytt passord nå",
"pincodeAuth": "Autentiseringskode",
"pincodeSubmit2": "Send inn kode",
"pincodeSubmit2": "Submit code",
"passwordResetSubmit": "Be om tilbakestilling",
"passwordResetAlreadyHaveCode": "Skriv inn koden",
"passwordResetSmtpRequired": "Kontakt din administrator",
"passwordResetSmtpRequiredDescription": "En passord tilbakestillingskode kreves for å tilbakestille passordet. Kontakt systemansvarlig for assistanse.",
"passwordBack": "Tilbake til passord",
"loginBack": "Gå tilbake til innlogging",
"loginBack": "Go back to main login page",
"signup": "Registrer deg",
"loginStart": "Logg inn for å komme i gang",
"idpOidcTokenValidating": "Validerer OIDC-token",
@@ -1118,6 +1144,10 @@
"actionUpdateIdpOrg": "Oppdater IDP-organisasjon",
"actionCreateClient": "Opprett Klient",
"actionDeleteClient": "Slett klient",
"actionArchiveClient": "Archive Client",
"actionUnarchiveClient": "Unarchive Client",
"actionBlockClient": "Block Client",
"actionUnblockClient": "Unblock Client",
"actionUpdateClient": "Oppdater klient",
"actionListClients": "List klienter",
"actionGetClient": "Hent klient",
@@ -1134,14 +1164,14 @@
"searchProgress": "Søker...",
"create": "Opprett",
"orgs": "Organisasjoner",
"loginError": "En feil oppstod under innlogging",
"loginRequiredForDevice": "Innlogging kreves for å godkjenne enheten.",
"loginError": "An unexpected error occurred. Please try again.",
"loginRequiredForDevice": "Login is required for your device.",
"passwordForgot": "Glemt passordet ditt?",
"otpAuth": "Tofaktorautentisering",
"otpAuthDescription": "Skriv inn koden fra autentiseringsappen din eller en av dine engangs reservekoder.",
"otpAuthSubmit": "Send inn kode",
"idpContinue": "Eller fortsett med",
"otpAuthBack": "Tilbake til innlogging",
"otpAuthBack": "Back to Password",
"navbar": "Navigasjonsmeny",
"navbarDescription": "Hovednavigasjonsmeny for applikasjonen",
"navbarDocsLink": "Dokumentasjon",
@@ -1189,6 +1219,7 @@
"sidebarOverview": "Oversikt",
"sidebarHome": "Hjem",
"sidebarSites": "Områder",
"sidebarApprovals": "Approval Requests",
"sidebarResources": "Ressurser",
"sidebarProxyResources": "Offentlig",
"sidebarClientResources": "Privat",
@@ -1205,7 +1236,7 @@
"sidebarIdentityProviders": "Identitetsleverandører",
"sidebarLicense": "Lisens",
"sidebarClients": "Klienter",
"sidebarUserDevices": "Brukere",
"sidebarUserDevices": "User Devices",
"sidebarMachineClients": "Maskiner",
"sidebarDomains": "Domener",
"sidebarGeneral": "Administrer",
@@ -1277,6 +1308,7 @@
"setupErrorCreateAdmin": "En feil oppstod under opprettelsen av serveradministratorkontoen.",
"certificateStatus": "Sertifikatstatus",
"loading": "Laster inn",
"loadingAnalytics": "Loading Analytics",
"restart": "Start på nytt",
"domains": "Domener",
"domainsDescription": "Opprett og behandle domener som er tilgjengelige i organisasjonen",
@@ -1304,6 +1336,7 @@
"refreshError": "Klarte ikke å oppdatere data",
"verified": "Verifisert",
"pending": "Venter",
"pendingApproval": "Pending Approval",
"sidebarBilling": "Fakturering",
"billing": "Fakturering",
"orgBillingDescription": "Administrer faktureringsinformasjon og abonnementer",
@@ -1420,7 +1453,7 @@
"securityKeyRemoveSuccess": "Sikkerhetsnøkkel fjernet",
"securityKeyRemoveError": "Klarte ikke å fjerne sikkerhetsnøkkel",
"securityKeyLoadError": "Klarte ikke å laste inn sikkerhetsnøkler",
"securityKeyLogin": "Fortsett med sikkerhetsnøkkel",
"securityKeyLogin": "Use Security Key",
"securityKeyAuthError": "Klarte ikke å autentisere med sikkerhetsnøkkel",
"securityKeyRecommendation": "Registrer en reservesikkerhetsnøkkel på en annen enhet for å sikre at du alltid har tilgang til kontoen din.",
"registering": "Registrerer...",
@@ -1547,6 +1580,8 @@
"IntervalSeconds": "Sunt intervall",
"timeoutSeconds": "Tidsavbrudd (sek)",
"timeIsInSeconds": "Tid er i sekunder",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need their devices approved by an admin before they can access resources",
"retryAttempts": "Forsøk på nytt",
"expectedResponseCodes": "Forventede svarkoder",
"expectedResponseCodesDescription": "HTTP-statuskode som indikerer sunn status. Hvis den blir stående tom, regnes 200-300 som sunn.",
@@ -1876,7 +1911,7 @@
"orgAuthChooseIdpDescription": "Velg din identitet leverandør for å fortsette",
"orgAuthNoIdpConfigured": "Denne organisasjonen har ikke noen identitetstjeneste konfigurert. Du kan i stedet logge inn med Pangolin identiteten din.",
"orgAuthSignInWithPangolin": "Logg inn med Pangolin",
"orgAuthSignInToOrg": "Logg inn på en organisasjon",
"orgAuthSignInToOrg": "Sign in to an organization",
"orgAuthSelectOrgTitle": "Organisasjonsinnlogging",
"orgAuthSelectOrgDescription": "Skriv inn organisasjons-ID-en din for å fortsette",
"orgAuthOrgIdPlaceholder": "din-organisasjon",
@@ -2232,6 +2267,8 @@
"deviceCodeInvalidFormat": "Kode må inneholde 9 tegn (f.eks A1AJ-N5JD)",
"deviceCodeInvalidOrExpired": "Ugyldig eller utløpt kode",
"deviceCodeVerifyFailed": "Klarte ikke å bekrefte enhetskoden",
"deviceCodeValidating": "Validating device code...",
"deviceCodeVerifying": "Verifying device authorization...",
"signedInAs": "Logget inn som",
"deviceCodeEnterPrompt": "Skriv inn koden som vises på enheten",
"continue": "Fortsett",
@@ -2244,7 +2281,7 @@
"deviceOrganizationsAccess": "Tilgang til alle organisasjoner din konto har tilgang til",
"deviceAuthorize": "Autoriser {applicationName}",
"deviceConnected": "Enhet tilkoblet!",
"deviceAuthorizedMessage": "Enhet er autorisert for tilgang til kontoen din.",
"deviceAuthorizedMessage": "Device is authorized to access your account. Please return to the client application.",
"pangolinCloud": "Pangolin Sky",
"viewDevices": "Vis enheter",
"viewDevicesDescription": "Administrer tilkoblede enheter",
@@ -2306,6 +2343,7 @@
"identifier": "Identifier",
"deviceLoginUseDifferentAccount": "Ikke du? Bruk en annen konto.",
"deviceLoginDeviceRequestingAccessToAccount": "En enhet ber om tilgang til denne kontoen.",
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
"noData": "Ingen data",
"machineClients": "Maskinklienter",
"install": "Installer",
@@ -2394,5 +2432,56 @@
"maintenanceScreenTitle": "Tjenesten er midlertidig utilgjengelig",
"maintenanceScreenMessage": "Vi opplever for øyeblikket tekniske problemer. Vennligst sjekk igjen snart.",
"maintenanceScreenEstimatedCompletion": "Estimert ferdigstillelse:",
"createInternalResourceDialogDestinationRequired": "Destinasjonen er nødvendig"
"createInternalResourceDialogDestinationRequired": "Destinasjonen er nødvendig",
"available": "Available",
"archived": "Archived",
"noArchivedDevices": "No archived devices found",
"deviceArchived": "Device archived",
"deviceArchivedDescription": "The device has been successfully archived.",
"errorArchivingDevice": "Error archiving device",
"failedToArchiveDevice": "Failed to archive device",
"deviceQuestionArchive": "Are you sure you want to archive this device?",
"deviceMessageArchive": "The device will be archived and removed from your active devices list.",
"deviceArchiveConfirm": "Archive Device",
"archiveDevice": "Archive Device",
"archive": "Archive",
"deviceUnarchived": "Device unarchived",
"deviceUnarchivedDescription": "The device has been successfully unarchived.",
"errorUnarchivingDevice": "Error unarchiving device",
"failedToUnarchiveDevice": "Failed to unarchive device",
"unarchive": "Unarchive",
"archiveClient": "Archive Client",
"archiveClientQuestion": "Are you sure you want to archive this client?",
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
"archiveClientConfirm": "Archive Client",
"blockClient": "Block Client",
"blockClientQuestion": "Are you sure you want to block this client?",
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
"blockClientConfirm": "Block Client",
"active": "Active",
"usernameOrEmail": "Username or Email",
"selectYourOrganization": "Select your organization",
"signInTo": "Log in in to",
"signInWithPassword": "Continue with Password",
"noAuthMethodsAvailable": "No authentication methods available for this organization.",
"enterPassword": "Enter your password",
"enterMfaCode": "Enter the code from your authenticator app",
"securityKeyRequired": "Please use your security key to sign in.",
"needToUseAnotherAccount": "Need to use a different account?",
"loginLegalDisclaimer": "By clicking the buttons below, you acknowledge you have read, understand, and agree to the <termsOfService>Terms of Service</termsOfService> and <privacyPolicy>Privacy Policy</privacyPolicy>.",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"userNotFoundWithUsername": "No user found with that username.",
"verify": "Verify",
"signIn": "Sign In",
"forgotPassword": "Forgot password?",
"orgSignInTip": "If you've logged in before, you can enter your username or email above to authenticate with your organization's identity provider instead. It's easier!",
"continueAnyway": "Continue anyway",
"dontShowAgain": "Don't show again",
"orgSignInNotice": "Did you know?",
"signupOrgNotice": "Trying to sign in?",
"signupOrgTip": "Are you trying to sign in through your organization's identity provider?",
"signupOrgLink": "Sign in or sign up with your organization instead",
"verifyEmailLogInWithDifferentAccount": "Use a Different Account",
"logIn": "Log In"
}

View File

@@ -257,6 +257,8 @@
"accessRolesSearch": "Rollen zoeken...",
"accessRolesAdd": "Rol toevoegen",
"accessRoleDelete": "Verwijder rol",
"accessApprovalsManage": "Manage Approvals",
"accessApprovalsDescription": "Manage approval requests in the organization",
"description": "Beschrijving",
"inviteTitle": "Open uitnodigingen",
"inviteDescription": "Beheer uitnodigingen voor andere gebruikers om deel te nemen aan de organisatie",
@@ -450,6 +452,18 @@
"selectDuration": "Selecteer duur",
"selectResource": "Selecteer Document",
"filterByResource": "Filter op pagina",
"selectApprovalState": "Select Approval State",
"filterByApprovalState": "Filter By Approval State",
"approvalListEmpty": "No approvals",
"approvalState": "Approval State",
"approve": "Approve",
"approved": "Approved",
"denied": "Denied",
"deniedApproval": "Denied Approval",
"all": "All",
"deny": "Deny",
"viewDetails": "View Details",
"requestingNewDeviceApproval": "requested a new device",
"resetFilters": "Filters resetten",
"totalBlocked": "Verzoeken geblokkeerd door Pangolin",
"totalRequests": "Totaal verzoeken",
@@ -729,16 +743,28 @@
"countries": "Landen",
"accessRoleCreate": "Rol aanmaken",
"accessRoleCreateDescription": "Maak een nieuwe rol aan om gebruikers te groeperen en hun rechten te beheren.",
"accessRoleEdit": "Edit Role",
"accessRoleEditDescription": "Edit role information.",
"accessRoleCreateSubmit": "Rol aanmaken",
"accessRoleCreated": "Rol aangemaakt",
"accessRoleCreatedDescription": "De rol is succesvol aangemaakt.",
"accessRoleErrorCreate": "Rol aanmaken mislukt",
"accessRoleErrorCreateDescription": "Fout opgetreden tijdens het aanmaken van de rol.",
"accessRoleUpdateSubmit": "Update Role",
"accessRoleUpdated": "Role updated",
"accessRoleUpdatedDescription": "The role has been successfully updated.",
"accessApprovalUpdated": "Approval processed",
"accessApprovalApprovedDescription": "Set Approval Request decision to approved.",
"accessApprovalDeniedDescription": "Set Approval Request decision to denied.",
"accessRoleErrorUpdate": "Failed to update role",
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
"accessApprovalErrorUpdate": "Failed to process approval",
"accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.",
"accessRoleErrorNewRequired": "Nieuwe rol is vereist",
"accessRoleErrorRemove": "Rol verwijderen mislukt",
"accessRoleErrorRemoveDescription": "Er is een fout opgetreden tijdens het verwijderen van de rol.",
"accessRoleName": "Rol naam",
"accessRoleQuestionRemove": "U staat op het punt de {name} rol te verwijderen. U kunt deze actie niet ongedaan maken.",
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
"accessRoleRemove": "Rol verwijderen",
"accessRoleRemoveDescription": "Verwijder een rol van de organisatie",
"accessRoleRemoveSubmit": "Rol verwijderen",
@@ -874,7 +900,7 @@
"inviteAlready": "Het lijkt erop dat je bent uitgenodigd!",
"inviteAlreadyDescription": "Om de uitnodiging te accepteren, moet je inloggen of een account aanmaken.",
"signupQuestion": "Heeft u al een account?",
"login": "Inloggen",
"login": "Log In",
"resourceNotFound": "Bron niet gevonden",
"resourceNotFoundDescription": "De bron die u probeert te benaderen bestaat niet.",
"pincodeRequirementsLength": "Pincode moet precies 6 cijfers zijn",
@@ -954,13 +980,13 @@
"passwordExpiryDescription": "Deze organisatie vereist dat u om de {maxDays} dagen uw wachtwoord wijzigt.",
"changePasswordNow": "Wijzig wachtwoord nu",
"pincodeAuth": "Authenticatiecode",
"pincodeSubmit2": "Code indienen",
"pincodeSubmit2": "Submit code",
"passwordResetSubmit": "Opnieuw instellen aanvragen",
"passwordResetAlreadyHaveCode": "Code invoeren",
"passwordResetSmtpRequired": "Neem contact op met uw beheerder",
"passwordResetSmtpRequiredDescription": "Er is een wachtwoord reset code nodig om uw wachtwoord opnieuw in te stellen. Neem contact op met uw beheerder voor hulp.",
"passwordBack": "Terug naar wachtwoord",
"loginBack": "Ga terug naar login",
"loginBack": "Go back to main login page",
"signup": "Registreer nu",
"loginStart": "Log in om te beginnen",
"idpOidcTokenValidating": "Valideer OIDC-token",
@@ -1118,6 +1144,10 @@
"actionUpdateIdpOrg": "IDP-org bijwerken",
"actionCreateClient": "Client aanmaken",
"actionDeleteClient": "Verwijder klant",
"actionArchiveClient": "Archive Client",
"actionUnarchiveClient": "Unarchive Client",
"actionBlockClient": "Block Client",
"actionUnblockClient": "Unblock Client",
"actionUpdateClient": "Klant bijwerken",
"actionListClients": "Lijst klanten",
"actionGetClient": "Client ophalen",
@@ -1134,14 +1164,14 @@
"searchProgress": "Zoeken...",
"create": "Aanmaken",
"orgs": "Organisaties",
"loginError": "Er is een fout opgetreden tijdens het inloggen",
"loginRequiredForDevice": "Inloggen is vereist om je apparaat te verifiëren.",
"loginError": "An unexpected error occurred. Please try again.",
"loginRequiredForDevice": "Login is required for your device.",
"passwordForgot": "Wachtwoord vergeten?",
"otpAuth": "Tweestapsverificatie verificatie",
"otpAuthDescription": "Voer de code van je authenticator-app of een van je reservekopiecodes voor het eenmalig gebruik in.",
"otpAuthSubmit": "Code indienen",
"idpContinue": "Of ga verder met",
"otpAuthBack": "Terug naar inloggen",
"otpAuthBack": "Back to Password",
"navbar": "Navigatiemenu",
"navbarDescription": "Hoofd navigatie menu voor de applicatie",
"navbarDocsLink": "Documentatie",
@@ -1189,6 +1219,7 @@
"sidebarOverview": "Overzicht.",
"sidebarHome": "Startpagina",
"sidebarSites": "Werkruimtes",
"sidebarApprovals": "Approval Requests",
"sidebarResources": "Bronnen",
"sidebarProxyResources": "Openbaar",
"sidebarClientResources": "Privé",
@@ -1205,7 +1236,7 @@
"sidebarIdentityProviders": "Identiteit aanbieders",
"sidebarLicense": "Licentie",
"sidebarClients": "Clienten",
"sidebarUserDevices": "Gebruikers",
"sidebarUserDevices": "User Devices",
"sidebarMachineClients": "Machines",
"sidebarDomains": "Domeinen",
"sidebarGeneral": "Beheren",
@@ -1277,6 +1308,7 @@
"setupErrorCreateAdmin": "Er is een fout opgetreden bij het maken van het serverbeheerdersaccount.",
"certificateStatus": "Certificaatstatus",
"loading": "Bezig met laden",
"loadingAnalytics": "Loading Analytics",
"restart": "Herstarten",
"domains": "Domeinen",
"domainsDescription": "Maak en beheer domeinen die beschikbaar zijn in de organisatie",
@@ -1304,6 +1336,7 @@
"refreshError": "Het vernieuwen van gegevens is mislukt",
"verified": "Gecontroleerd",
"pending": "In afwachting",
"pendingApproval": "Pending Approval",
"sidebarBilling": "Facturering",
"billing": "Facturering",
"orgBillingDescription": "Beheer factureringsinformatie en abonnementen",
@@ -1420,7 +1453,7 @@
"securityKeyRemoveSuccess": "Beveiligingssleutel succesvol verwijderd",
"securityKeyRemoveError": "Fout bij verwijderen van beveiligingssleutel",
"securityKeyLoadError": "Fout bij laden van beveiligingssleutels",
"securityKeyLogin": "Doorgaan met beveiligingssleutel",
"securityKeyLogin": "Use Security Key",
"securityKeyAuthError": "Fout bij authenticatie met beveiligingssleutel",
"securityKeyRecommendation": "Overweeg om een andere beveiligingssleutel te registreren op een ander apparaat om ervoor te zorgen dat u niet buitengesloten raakt van uw account.",
"registering": "Registreren...",
@@ -1547,6 +1580,8 @@
"IntervalSeconds": "Gezonde Interval",
"timeoutSeconds": "Timeout (sec)",
"timeIsInSeconds": "Tijd is in seconden",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need their devices approved by an admin before they can access resources",
"retryAttempts": "Herhaal Pogingen",
"expectedResponseCodes": "Verwachte Reactiecodes",
"expectedResponseCodesDescription": "HTTP-statuscode die gezonde status aangeeft. Indien leeg wordt 200-300 als gezond beschouwd.",
@@ -1876,7 +1911,7 @@
"orgAuthChooseIdpDescription": "Kies uw identiteitsprovider om door te gaan",
"orgAuthNoIdpConfigured": "Deze organisatie heeft geen identiteitsproviders geconfigureerd. Je kunt in plaats daarvan inloggen met je Pangolin-identiteit.",
"orgAuthSignInWithPangolin": "Log in met Pangolin",
"orgAuthSignInToOrg": "Meld u aan bij een organisatie",
"orgAuthSignInToOrg": "Sign in to an organization",
"orgAuthSelectOrgTitle": "Organisatie Inloggen",
"orgAuthSelectOrgDescription": "Voer je organisatie-ID in om verder te gaan",
"orgAuthOrgIdPlaceholder": "jouw-organisatie",
@@ -2232,6 +2267,8 @@
"deviceCodeInvalidFormat": "Code moet 9 tekens bevatten (bijv. A1AJ-N5JD)",
"deviceCodeInvalidOrExpired": "Ongeldige of verlopen code",
"deviceCodeVerifyFailed": "Apparaatcode verifiëren mislukt",
"deviceCodeValidating": "Validating device code...",
"deviceCodeVerifying": "Verifying device authorization...",
"signedInAs": "Ingelogd als",
"deviceCodeEnterPrompt": "Voer de op het apparaat weergegeven code in",
"continue": "Doorgaan",
@@ -2244,7 +2281,7 @@
"deviceOrganizationsAccess": "Toegang tot alle organisaties waar uw account toegang tot heeft",
"deviceAuthorize": "Autoriseer {applicationName}",
"deviceConnected": "Apparaat verbonden!",
"deviceAuthorizedMessage": "Apparaat is gemachtigd om toegang te krijgen tot je account.",
"deviceAuthorizedMessage": "Device is authorized to access your account. Please return to the client application.",
"pangolinCloud": "Pangoline Cloud",
"viewDevices": "Bekijk apparaten",
"viewDevicesDescription": "Beheer uw aangesloten apparaten",
@@ -2306,6 +2343,7 @@
"identifier": "Identifier",
"deviceLoginUseDifferentAccount": "Niet u? Gebruik een ander account.",
"deviceLoginDeviceRequestingAccessToAccount": "Een apparaat vraagt om toegang tot dit account.",
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
"noData": "Geen gegevens",
"machineClients": "Machine Clienten",
"install": "Installeren",
@@ -2394,5 +2432,56 @@
"maintenanceScreenTitle": "Dienst tijdelijk niet beschikbaar",
"maintenanceScreenMessage": "We hebben momenteel technische problemen. Probeer het later opnieuw.",
"maintenanceScreenEstimatedCompletion": "Geschatte voltooiing:",
"createInternalResourceDialogDestinationRequired": "Bestemming is vereist"
"createInternalResourceDialogDestinationRequired": "Bestemming is vereist",
"available": "Available",
"archived": "Archived",
"noArchivedDevices": "No archived devices found",
"deviceArchived": "Device archived",
"deviceArchivedDescription": "The device has been successfully archived.",
"errorArchivingDevice": "Error archiving device",
"failedToArchiveDevice": "Failed to archive device",
"deviceQuestionArchive": "Are you sure you want to archive this device?",
"deviceMessageArchive": "The device will be archived and removed from your active devices list.",
"deviceArchiveConfirm": "Archive Device",
"archiveDevice": "Archive Device",
"archive": "Archive",
"deviceUnarchived": "Device unarchived",
"deviceUnarchivedDescription": "The device has been successfully unarchived.",
"errorUnarchivingDevice": "Error unarchiving device",
"failedToUnarchiveDevice": "Failed to unarchive device",
"unarchive": "Unarchive",
"archiveClient": "Archive Client",
"archiveClientQuestion": "Are you sure you want to archive this client?",
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
"archiveClientConfirm": "Archive Client",
"blockClient": "Block Client",
"blockClientQuestion": "Are you sure you want to block this client?",
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
"blockClientConfirm": "Block Client",
"active": "Active",
"usernameOrEmail": "Username or Email",
"selectYourOrganization": "Select your organization",
"signInTo": "Log in in to",
"signInWithPassword": "Continue with Password",
"noAuthMethodsAvailable": "No authentication methods available for this organization.",
"enterPassword": "Enter your password",
"enterMfaCode": "Enter the code from your authenticator app",
"securityKeyRequired": "Please use your security key to sign in.",
"needToUseAnotherAccount": "Need to use a different account?",
"loginLegalDisclaimer": "By clicking the buttons below, you acknowledge you have read, understand, and agree to the <termsOfService>Terms of Service</termsOfService> and <privacyPolicy>Privacy Policy</privacyPolicy>.",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"userNotFoundWithUsername": "No user found with that username.",
"verify": "Verify",
"signIn": "Sign In",
"forgotPassword": "Forgot password?",
"orgSignInTip": "If you've logged in before, you can enter your username or email above to authenticate with your organization's identity provider instead. It's easier!",
"continueAnyway": "Continue anyway",
"dontShowAgain": "Don't show again",
"orgSignInNotice": "Did you know?",
"signupOrgNotice": "Trying to sign in?",
"signupOrgTip": "Are you trying to sign in through your organization's identity provider?",
"signupOrgLink": "Sign in or sign up with your organization instead",
"verifyEmailLogInWithDifferentAccount": "Use a Different Account",
"logIn": "Log In"
}

View File

@@ -257,6 +257,8 @@
"accessRolesSearch": "Szukaj ról...",
"accessRolesAdd": "Dodaj rolę",
"accessRoleDelete": "Usuń rolę",
"accessApprovalsManage": "Manage Approvals",
"accessApprovalsDescription": "Manage approval requests in the organization",
"description": "Opis",
"inviteTitle": "Otwórz zaproszenia",
"inviteDescription": "Zarządzaj zaproszeniami dla innych użytkowników do dołączenia do organizacji",
@@ -450,6 +452,18 @@
"selectDuration": "Wybierz okres",
"selectResource": "Wybierz zasób",
"filterByResource": "Filtruj według zasobów",
"selectApprovalState": "Select Approval State",
"filterByApprovalState": "Filter By Approval State",
"approvalListEmpty": "No approvals",
"approvalState": "Approval State",
"approve": "Approve",
"approved": "Approved",
"denied": "Denied",
"deniedApproval": "Denied Approval",
"all": "All",
"deny": "Deny",
"viewDetails": "View Details",
"requestingNewDeviceApproval": "requested a new device",
"resetFilters": "Resetuj filtry",
"totalBlocked": "Żądania zablokowane przez Pangolina",
"totalRequests": "Wszystkich Żądań",
@@ -729,16 +743,28 @@
"countries": "Kraje",
"accessRoleCreate": "Utwórz rolę",
"accessRoleCreateDescription": "Utwórz nową rolę aby zgrupować użytkowników i zarządzać ich uprawnieniami.",
"accessRoleEdit": "Edit Role",
"accessRoleEditDescription": "Edit role information.",
"accessRoleCreateSubmit": "Utwórz rolę",
"accessRoleCreated": "Rola utworzona",
"accessRoleCreatedDescription": "Rola została pomyślnie utworzona.",
"accessRoleErrorCreate": "Nie udało się utworzyć roli",
"accessRoleErrorCreateDescription": "Wystąpił błąd podczas tworzenia roli.",
"accessRoleUpdateSubmit": "Update Role",
"accessRoleUpdated": "Role updated",
"accessRoleUpdatedDescription": "The role has been successfully updated.",
"accessApprovalUpdated": "Approval processed",
"accessApprovalApprovedDescription": "Set Approval Request decision to approved.",
"accessApprovalDeniedDescription": "Set Approval Request decision to denied.",
"accessRoleErrorUpdate": "Failed to update role",
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
"accessApprovalErrorUpdate": "Failed to process approval",
"accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.",
"accessRoleErrorNewRequired": "Nowa rola jest wymagana",
"accessRoleErrorRemove": "Nie udało się usunąć roli",
"accessRoleErrorRemoveDescription": "Wystąpił błąd podczas usuwania roli.",
"accessRoleName": "Nazwa roli",
"accessRoleQuestionRemove": "Zamierzasz usunąć rolę {name}. Tej akcji nie można cofnąć.",
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
"accessRoleRemove": "Usuń rolę",
"accessRoleRemoveDescription": "Usuń rolę z organizacji",
"accessRoleRemoveSubmit": "Usuń rolę",
@@ -874,7 +900,7 @@
"inviteAlready": "Wygląda na to, że zostałeś już zaproszony!",
"inviteAlreadyDescription": "Aby zaakceptować zaproszenie, musisz się zalogować lub utworzyć konto.",
"signupQuestion": "Masz już konto?",
"login": "Zaloguj się",
"login": "Log In",
"resourceNotFound": "Nie znaleziono zasobu",
"resourceNotFoundDescription": "Zasób, do którego próbujesz uzyskać dostęp, nie istnieje.",
"pincodeRequirementsLength": "PIN musi składać się dokładnie z 6 cyfr",
@@ -954,13 +980,13 @@
"passwordExpiryDescription": "Organizacja wymaga zmiany hasła co {maxDays} dni.",
"changePasswordNow": "Zmień hasło teraz",
"pincodeAuth": "Kod uwierzytelniający",
"pincodeSubmit2": "Wyślij kod",
"pincodeSubmit2": "Submit code",
"passwordResetSubmit": "Zażądaj resetowania",
"passwordResetAlreadyHaveCode": "Wprowadź kod",
"passwordResetSmtpRequired": "Skontaktuj się z administratorem",
"passwordResetSmtpRequiredDescription": "Aby zresetować hasło, wymagany jest kod resetowania hasła. Skontaktuj się z administratorem.",
"passwordBack": "Powrót do hasła",
"loginBack": "Wróć do logowania",
"loginBack": "Go back to main login page",
"signup": "Zarejestruj się",
"loginStart": "Zaloguj się, aby rozpocząć",
"idpOidcTokenValidating": "Walidacja tokena OIDC",
@@ -1118,6 +1144,10 @@
"actionUpdateIdpOrg": "Aktualizuj organizację IDP",
"actionCreateClient": "Utwórz klienta",
"actionDeleteClient": "Usuń klienta",
"actionArchiveClient": "Archive Client",
"actionUnarchiveClient": "Unarchive Client",
"actionBlockClient": "Block Client",
"actionUnblockClient": "Unblock Client",
"actionUpdateClient": "Aktualizuj klienta",
"actionListClients": "Lista klientów",
"actionGetClient": "Pobierz klienta",
@@ -1134,14 +1164,14 @@
"searchProgress": "Szukaj...",
"create": "Utwórz",
"orgs": "Organizacje",
"loginError": "Wystąpił błąd podczas logowania",
"loginRequiredForDevice": "Logowanie jest wymagane do uwierzytelnienia urządzenia.",
"loginError": "An unexpected error occurred. Please try again.",
"loginRequiredForDevice": "Login is required for your device.",
"passwordForgot": "Zapomniałeś hasła?",
"otpAuth": "Uwierzytelnianie dwuskładnikowe",
"otpAuthDescription": "Wprowadź kod z aplikacji uwierzytelniającej lub jeden z jednorazowych kodów zapasowych.",
"otpAuthSubmit": "Wyślij kod",
"idpContinue": "Lub kontynuuj z",
"otpAuthBack": "Powrót do logowania",
"otpAuthBack": "Back to Password",
"navbar": "Menu nawigacyjne",
"navbarDescription": "Główne menu nawigacyjne aplikacji",
"navbarDocsLink": "Dokumentacja",
@@ -1189,6 +1219,7 @@
"sidebarOverview": "Przegląd",
"sidebarHome": "Strona główna",
"sidebarSites": "Witryny",
"sidebarApprovals": "Approval Requests",
"sidebarResources": "Zasoby",
"sidebarProxyResources": "Publiczne",
"sidebarClientResources": "Prywatny",
@@ -1205,7 +1236,7 @@
"sidebarIdentityProviders": "Dostawcy tożsamości",
"sidebarLicense": "Licencja",
"sidebarClients": "Klienty",
"sidebarUserDevices": "Użytkownicy",
"sidebarUserDevices": "User Devices",
"sidebarMachineClients": "Maszyny",
"sidebarDomains": "Domeny",
"sidebarGeneral": "Zarządzaj",
@@ -1277,6 +1308,7 @@
"setupErrorCreateAdmin": "Wystąpił błąd podczas tworzenia konta administratora serwera.",
"certificateStatus": "Status certyfikatu",
"loading": "Ładowanie",
"loadingAnalytics": "Loading Analytics",
"restart": "Uruchom ponownie",
"domains": "Domeny",
"domainsDescription": "Tworzenie domen dostępnych w organizacji i zarządzanie nimi",
@@ -1304,6 +1336,7 @@
"refreshError": "Nie udało się odświeżyć danych",
"verified": "Zatwierdzony",
"pending": "Oczekuje",
"pendingApproval": "Pending Approval",
"sidebarBilling": "Fakturowanie",
"billing": "Fakturowanie",
"orgBillingDescription": "Zarządzaj informacjami rozliczeniowymi i subskrypcjami",
@@ -1420,7 +1453,7 @@
"securityKeyRemoveSuccess": "Klucz bezpieczeństwa został pomyślnie usunięty",
"securityKeyRemoveError": "Błąd podczas usuwania klucza bezpieczeństwa",
"securityKeyLoadError": "Błąd podczas ładowania kluczy bezpieczeństwa",
"securityKeyLogin": "Zaloguj się kluczem bezpieczeństwa",
"securityKeyLogin": "Use Security Key",
"securityKeyAuthError": "Błąd podczas uwierzytelniania kluczem bezpieczeństwa",
"securityKeyRecommendation": "Rozważ zarejestrowanie innego klucza bezpieczeństwa na innym urządzeniu, aby upewnić się, że nie zostaniesz zablokowany z dostępu do swojego konta.",
"registering": "Rejestracja...",
@@ -1547,6 +1580,8 @@
"IntervalSeconds": "Interwał Zdrowy",
"timeoutSeconds": "Limit czasu (sek)",
"timeIsInSeconds": "Czas w sekundach",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need their devices approved by an admin before they can access resources",
"retryAttempts": "Próby Ponowienia",
"expectedResponseCodes": "Oczekiwane Kody Odpowiedzi",
"expectedResponseCodesDescription": "Kod statusu HTTP, który wskazuje zdrowy status. Jeśli pozostanie pusty, uznaje się 200-300 za zdrowy.",
@@ -1876,7 +1911,7 @@
"orgAuthChooseIdpDescription": "Wybierz swojego dostawcę tożsamości, aby kontynuować",
"orgAuthNoIdpConfigured": "Ta organizacja nie ma skonfigurowanych żadnych dostawców tożsamości. Zamiast tego możesz zalogować się za pomocą swojej tożsamości Pangolin.",
"orgAuthSignInWithPangolin": "Zaloguj się używając Pangolin",
"orgAuthSignInToOrg": "Zaloguj się do organizacji",
"orgAuthSignInToOrg": "Sign in to an organization",
"orgAuthSelectOrgTitle": "Logowanie do organizacji",
"orgAuthSelectOrgDescription": "Wprowadź identyfikator organizacji, aby kontynuować",
"orgAuthOrgIdPlaceholder": "twoja-organizacja",
@@ -2232,6 +2267,8 @@
"deviceCodeInvalidFormat": "Kod musi mieć 9 znaków (np. A1AJ-N5JD)",
"deviceCodeInvalidOrExpired": "Nieprawidłowy lub wygasły kod",
"deviceCodeVerifyFailed": "Nie udało się zweryfikować kodu urządzenia",
"deviceCodeValidating": "Validating device code...",
"deviceCodeVerifying": "Verifying device authorization...",
"signedInAs": "Zalogowany jako",
"deviceCodeEnterPrompt": "Wprowadź kod wyświetlany na urządzeniu",
"continue": "Kontynuuj",
@@ -2244,7 +2281,7 @@
"deviceOrganizationsAccess": "Dostęp do wszystkich organizacji, do których Twoje konto ma dostęp",
"deviceAuthorize": "Autoryzuj {applicationName}",
"deviceConnected": "Urządzenie podłączone!",
"deviceAuthorizedMessage": "Urządzenie jest upoważnione do dostępu do Twojego konta.",
"deviceAuthorizedMessage": "Device is authorized to access your account. Please return to the client application.",
"pangolinCloud": "Chmura Pangolin",
"viewDevices": "Zobacz urządzenia",
"viewDevicesDescription": "Zarządzaj podłączonymi urządzeniami",
@@ -2306,6 +2343,7 @@
"identifier": "Identifier",
"deviceLoginUseDifferentAccount": "Nie ty? Użyj innego konta.",
"deviceLoginDeviceRequestingAccessToAccount": "Urządzenie żąda dostępu do tego konta.",
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
"noData": "Brak danych",
"machineClients": "Klienci maszyn",
"install": "Zainstaluj",
@@ -2394,5 +2432,56 @@
"maintenanceScreenTitle": "Usługa chwilowo niedostępna",
"maintenanceScreenMessage": "Obecnie doświadczamy problemów technicznych. Proszę sprawdzić ponownie wkrótce.",
"maintenanceScreenEstimatedCompletion": "Szacowane zakończenie:",
"createInternalResourceDialogDestinationRequired": "Miejsce docelowe jest wymagane"
"createInternalResourceDialogDestinationRequired": "Miejsce docelowe jest wymagane",
"available": "Available",
"archived": "Archived",
"noArchivedDevices": "No archived devices found",
"deviceArchived": "Device archived",
"deviceArchivedDescription": "The device has been successfully archived.",
"errorArchivingDevice": "Error archiving device",
"failedToArchiveDevice": "Failed to archive device",
"deviceQuestionArchive": "Are you sure you want to archive this device?",
"deviceMessageArchive": "The device will be archived and removed from your active devices list.",
"deviceArchiveConfirm": "Archive Device",
"archiveDevice": "Archive Device",
"archive": "Archive",
"deviceUnarchived": "Device unarchived",
"deviceUnarchivedDescription": "The device has been successfully unarchived.",
"errorUnarchivingDevice": "Error unarchiving device",
"failedToUnarchiveDevice": "Failed to unarchive device",
"unarchive": "Unarchive",
"archiveClient": "Archive Client",
"archiveClientQuestion": "Are you sure you want to archive this client?",
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
"archiveClientConfirm": "Archive Client",
"blockClient": "Block Client",
"blockClientQuestion": "Are you sure you want to block this client?",
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
"blockClientConfirm": "Block Client",
"active": "Active",
"usernameOrEmail": "Username or Email",
"selectYourOrganization": "Select your organization",
"signInTo": "Log in in to",
"signInWithPassword": "Continue with Password",
"noAuthMethodsAvailable": "No authentication methods available for this organization.",
"enterPassword": "Enter your password",
"enterMfaCode": "Enter the code from your authenticator app",
"securityKeyRequired": "Please use your security key to sign in.",
"needToUseAnotherAccount": "Need to use a different account?",
"loginLegalDisclaimer": "By clicking the buttons below, you acknowledge you have read, understand, and agree to the <termsOfService>Terms of Service</termsOfService> and <privacyPolicy>Privacy Policy</privacyPolicy>.",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"userNotFoundWithUsername": "No user found with that username.",
"verify": "Verify",
"signIn": "Sign In",
"forgotPassword": "Forgot password?",
"orgSignInTip": "If you've logged in before, you can enter your username or email above to authenticate with your organization's identity provider instead. It's easier!",
"continueAnyway": "Continue anyway",
"dontShowAgain": "Don't show again",
"orgSignInNotice": "Did you know?",
"signupOrgNotice": "Trying to sign in?",
"signupOrgTip": "Are you trying to sign in through your organization's identity provider?",
"signupOrgLink": "Sign in or sign up with your organization instead",
"verifyEmailLogInWithDifferentAccount": "Use a Different Account",
"logIn": "Log In"
}

View File

@@ -257,6 +257,8 @@
"accessRolesSearch": "Pesquisar funções...",
"accessRolesAdd": "Adicionar função",
"accessRoleDelete": "Excluir Papel",
"accessApprovalsManage": "Manage Approvals",
"accessApprovalsDescription": "Manage approval requests in the organization",
"description": "Descrição:",
"inviteTitle": "Convites Abertos",
"inviteDescription": "Gerenciar convites para outros usuários participarem da organização",
@@ -450,6 +452,18 @@
"selectDuration": "Selecionar duração",
"selectResource": "Selecionar Recurso",
"filterByResource": "Filtrar por Recurso",
"selectApprovalState": "Select Approval State",
"filterByApprovalState": "Filter By Approval State",
"approvalListEmpty": "No approvals",
"approvalState": "Approval State",
"approve": "Approve",
"approved": "Approved",
"denied": "Denied",
"deniedApproval": "Denied Approval",
"all": "All",
"deny": "Deny",
"viewDetails": "View Details",
"requestingNewDeviceApproval": "requested a new device",
"resetFilters": "Redefinir filtros",
"totalBlocked": "Solicitações bloqueadas pelo Pangolin",
"totalRequests": "Total de pedidos",
@@ -729,16 +743,28 @@
"countries": "Países",
"accessRoleCreate": "Criar Função",
"accessRoleCreateDescription": "Crie uma nova função para agrupar utilizadores e gerir suas permissões.",
"accessRoleEdit": "Edit Role",
"accessRoleEditDescription": "Edit role information.",
"accessRoleCreateSubmit": "Criar Função",
"accessRoleCreated": "Função criada",
"accessRoleCreatedDescription": "A função foi criada com sucesso.",
"accessRoleErrorCreate": "Falha ao criar função",
"accessRoleErrorCreateDescription": "Ocorreu um erro ao criar a função.",
"accessRoleUpdateSubmit": "Update Role",
"accessRoleUpdated": "Role updated",
"accessRoleUpdatedDescription": "The role has been successfully updated.",
"accessApprovalUpdated": "Approval processed",
"accessApprovalApprovedDescription": "Set Approval Request decision to approved.",
"accessApprovalDeniedDescription": "Set Approval Request decision to denied.",
"accessRoleErrorUpdate": "Failed to update role",
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
"accessApprovalErrorUpdate": "Failed to process approval",
"accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.",
"accessRoleErrorNewRequired": "Nova função é necessária",
"accessRoleErrorRemove": "Falha ao remover função",
"accessRoleErrorRemoveDescription": "Ocorreu um erro ao remover a função.",
"accessRoleName": "Nome da Função",
"accessRoleQuestionRemove": "Você está prestes a apagar a função {name}. Você não pode desfazer esta ação.",
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
"accessRoleRemove": "Remover Função",
"accessRoleRemoveDescription": "Remover uma função da organização",
"accessRoleRemoveSubmit": "Remover Função",
@@ -874,7 +900,7 @@
"inviteAlready": "Parece que já foi convidado!",
"inviteAlreadyDescription": "Para aceitar o convite, deve iniciar sessão ou criar uma conta.",
"signupQuestion": "Já tem uma conta?",
"login": "Iniciar sessão",
"login": "Log In",
"resourceNotFound": "Recurso Não Encontrado",
"resourceNotFoundDescription": "O recurso que está a tentar aceder não existe.",
"pincodeRequirementsLength": "O PIN deve ter exatamente 6 dígitos",
@@ -954,13 +980,13 @@
"passwordExpiryDescription": "Esta organização exige que você altere sua senha a cada {maxDays} dias.",
"changePasswordNow": "Alterar a senha agora",
"pincodeAuth": "Código do Autenticador",
"pincodeSubmit2": "Submeter Código",
"pincodeSubmit2": "Submit code",
"passwordResetSubmit": "Solicitar Redefinição",
"passwordResetAlreadyHaveCode": "Inserir Código",
"passwordResetSmtpRequired": "Por favor, contate o administrador",
"passwordResetSmtpRequiredDescription": "É necessário um código de redefinição de senha para redefinir sua senha. Por favor, contate o administrador para assistência.",
"passwordBack": "Voltar à Palavra-passe",
"loginBack": "Voltar ao início de sessão",
"loginBack": "Go back to main login page",
"signup": "Registar",
"loginStart": "Inicie sessão para começar",
"idpOidcTokenValidating": "A validar token OIDC",
@@ -1118,6 +1144,10 @@
"actionUpdateIdpOrg": "Atualizar Organização IDP",
"actionCreateClient": "Criar Cliente",
"actionDeleteClient": "Excluir Cliente",
"actionArchiveClient": "Archive Client",
"actionUnarchiveClient": "Unarchive Client",
"actionBlockClient": "Block Client",
"actionUnblockClient": "Unblock Client",
"actionUpdateClient": "Atualizar Cliente",
"actionListClients": "Listar Clientes",
"actionGetClient": "Obter Cliente",
@@ -1134,14 +1164,14 @@
"searchProgress": "Pesquisar...",
"create": "Criar",
"orgs": "Organizações",
"loginError": "Ocorreu um erro ao iniciar sessão",
"loginRequiredForDevice": "É necessário entrar para autenticar seu dispositivo.",
"loginError": "An unexpected error occurred. Please try again.",
"loginRequiredForDevice": "Login is required for your device.",
"passwordForgot": "Esqueceu a sua palavra-passe?",
"otpAuth": "Autenticação de Dois Fatores",
"otpAuthDescription": "Insira o código da sua aplicação de autenticação ou um dos seus códigos de backup de uso único.",
"otpAuthSubmit": "Submeter Código",
"idpContinue": "Ou continuar com",
"otpAuthBack": "Voltar ao Início de Sessão",
"otpAuthBack": "Back to Password",
"navbar": "Menu de Navegação",
"navbarDescription": "Menu de navegação principal da aplicação",
"navbarDocsLink": "Documentação",
@@ -1189,6 +1219,7 @@
"sidebarOverview": "Geral",
"sidebarHome": "Residencial",
"sidebarSites": "sites",
"sidebarApprovals": "Approval Requests",
"sidebarResources": "Recursos",
"sidebarProxyResources": "Público",
"sidebarClientResources": "Privado",
@@ -1205,7 +1236,7 @@
"sidebarIdentityProviders": "Provedores de identidade",
"sidebarLicense": "Tipo:",
"sidebarClients": "Clientes",
"sidebarUserDevices": "Utilizadores",
"sidebarUserDevices": "User Devices",
"sidebarMachineClients": "Máquinas",
"sidebarDomains": "Domínios",
"sidebarGeneral": "Gerir",
@@ -1277,6 +1308,7 @@
"setupErrorCreateAdmin": "Ocorreu um erro ao criar a conta de administrador do servidor.",
"certificateStatus": "Status do Certificado",
"loading": "Carregando",
"loadingAnalytics": "Loading Analytics",
"restart": "Reiniciar",
"domains": "Domínios",
"domainsDescription": "Criar e gerenciar domínios disponíveis na organização",
@@ -1304,6 +1336,7 @@
"refreshError": "Falha ao atualizar dados",
"verified": "Verificado",
"pending": "Pendente",
"pendingApproval": "Pending Approval",
"sidebarBilling": "Faturamento",
"billing": "Faturamento",
"orgBillingDescription": "Gerenciar informações e assinaturas de cobrança",
@@ -1420,7 +1453,7 @@
"securityKeyRemoveSuccess": "Chave de segurança removida com sucesso",
"securityKeyRemoveError": "Erro ao remover chave de segurança",
"securityKeyLoadError": "Erro ao carregar chaves de segurança",
"securityKeyLogin": "Continuar com a chave de segurança",
"securityKeyLogin": "Use Security Key",
"securityKeyAuthError": "Erro ao autenticar com chave de segurança",
"securityKeyRecommendation": "Considere registrar outra chave de segurança em um dispositivo diferente para garantir que você não fique bloqueado da sua conta.",
"registering": "Registrando...",
@@ -1547,6 +1580,8 @@
"IntervalSeconds": "Intervalo Saudável",
"timeoutSeconds": "Tempo limite (seg)",
"timeIsInSeconds": "O tempo está em segundos",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need their devices approved by an admin before they can access resources",
"retryAttempts": "Tentativas de Repetição",
"expectedResponseCodes": "Códigos de Resposta Esperados",
"expectedResponseCodesDescription": "Código de status HTTP que indica estado saudável. Se deixado em branco, 200-300 é considerado saudável.",
@@ -1876,7 +1911,7 @@
"orgAuthChooseIdpDescription": "Escolha o seu provedor de identidade para continuar",
"orgAuthNoIdpConfigured": "Esta organização não tem nenhum provedor de identidade configurado. Você pode entrar com a identidade do seu Pangolin.",
"orgAuthSignInWithPangolin": "Entrar com o Pangolin",
"orgAuthSignInToOrg": "Entrar em uma organização",
"orgAuthSignInToOrg": "Sign in to an organization",
"orgAuthSelectOrgTitle": "Entrada da Organização",
"orgAuthSelectOrgDescription": "Digite seu ID da organização para continuar",
"orgAuthOrgIdPlaceholder": "sua-organização",
@@ -2232,6 +2267,8 @@
"deviceCodeInvalidFormat": "O código deve ter 9 caracteres (ex.: A1AJ-N5JD)",
"deviceCodeInvalidOrExpired": "Código inválido ou expirado",
"deviceCodeVerifyFailed": "Falha ao verificar o código do dispositivo",
"deviceCodeValidating": "Validating device code...",
"deviceCodeVerifying": "Verifying device authorization...",
"signedInAs": "Sessão iniciada como",
"deviceCodeEnterPrompt": "Digite o código exibido no dispositivo",
"continue": "Continuar",
@@ -2244,7 +2281,7 @@
"deviceOrganizationsAccess": "Acesso a todas as organizações que sua conta tem acesso a",
"deviceAuthorize": "Autorizar {applicationName}",
"deviceConnected": "Dispositivo Conectado!",
"deviceAuthorizedMessage": "O dispositivo está autorizado a acessar sua conta.",
"deviceAuthorizedMessage": "Device is authorized to access your account. Please return to the client application.",
"pangolinCloud": "Nuvem do Pangolin",
"viewDevices": "Ver Dispositivos",
"viewDevicesDescription": "Gerencie seus dispositivos conectados",
@@ -2306,6 +2343,7 @@
"identifier": "Identifier",
"deviceLoginUseDifferentAccount": "Não é você? Use uma conta diferente.",
"deviceLoginDeviceRequestingAccessToAccount": "Um dispositivo está solicitando acesso a essa conta.",
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
"noData": "Nenhum dado encontrado",
"machineClients": "Clientes de máquina",
"install": "Instale",
@@ -2394,5 +2432,56 @@
"maintenanceScreenTitle": "Serviço Temporariamente Indisponível",
"maintenanceScreenMessage": "Estamos enfrentando dificuldades técnicas no momento. Por favor, volte em breve.",
"maintenanceScreenEstimatedCompletion": "Conclusão Estimada:",
"createInternalResourceDialogDestinationRequired": "Destino é obrigatório"
"createInternalResourceDialogDestinationRequired": "Destino é obrigatório",
"available": "Available",
"archived": "Archived",
"noArchivedDevices": "No archived devices found",
"deviceArchived": "Device archived",
"deviceArchivedDescription": "The device has been successfully archived.",
"errorArchivingDevice": "Error archiving device",
"failedToArchiveDevice": "Failed to archive device",
"deviceQuestionArchive": "Are you sure you want to archive this device?",
"deviceMessageArchive": "The device will be archived and removed from your active devices list.",
"deviceArchiveConfirm": "Archive Device",
"archiveDevice": "Archive Device",
"archive": "Archive",
"deviceUnarchived": "Device unarchived",
"deviceUnarchivedDescription": "The device has been successfully unarchived.",
"errorUnarchivingDevice": "Error unarchiving device",
"failedToUnarchiveDevice": "Failed to unarchive device",
"unarchive": "Unarchive",
"archiveClient": "Archive Client",
"archiveClientQuestion": "Are you sure you want to archive this client?",
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
"archiveClientConfirm": "Archive Client",
"blockClient": "Block Client",
"blockClientQuestion": "Are you sure you want to block this client?",
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
"blockClientConfirm": "Block Client",
"active": "Active",
"usernameOrEmail": "Username or Email",
"selectYourOrganization": "Select your organization",
"signInTo": "Log in in to",
"signInWithPassword": "Continue with Password",
"noAuthMethodsAvailable": "No authentication methods available for this organization.",
"enterPassword": "Enter your password",
"enterMfaCode": "Enter the code from your authenticator app",
"securityKeyRequired": "Please use your security key to sign in.",
"needToUseAnotherAccount": "Need to use a different account?",
"loginLegalDisclaimer": "By clicking the buttons below, you acknowledge you have read, understand, and agree to the <termsOfService>Terms of Service</termsOfService> and <privacyPolicy>Privacy Policy</privacyPolicy>.",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"userNotFoundWithUsername": "No user found with that username.",
"verify": "Verify",
"signIn": "Sign In",
"forgotPassword": "Forgot password?",
"orgSignInTip": "If you've logged in before, you can enter your username or email above to authenticate with your organization's identity provider instead. It's easier!",
"continueAnyway": "Continue anyway",
"dontShowAgain": "Don't show again",
"orgSignInNotice": "Did you know?",
"signupOrgNotice": "Trying to sign in?",
"signupOrgTip": "Are you trying to sign in through your organization's identity provider?",
"signupOrgLink": "Sign in or sign up with your organization instead",
"verifyEmailLogInWithDifferentAccount": "Use a Different Account",
"logIn": "Log In"
}

View File

@@ -257,6 +257,8 @@
"accessRolesSearch": "Поиск ролей...",
"accessRolesAdd": "Добавить роль",
"accessRoleDelete": "Удалить роль",
"accessApprovalsManage": "Manage Approvals",
"accessApprovalsDescription": "Manage approval requests in the organization",
"description": "Описание",
"inviteTitle": "Открытые приглашения",
"inviteDescription": "Управление приглашениями для присоединения других пользователей к организации",
@@ -450,6 +452,18 @@
"selectDuration": "Укажите срок действия",
"selectResource": "Выберите ресурс",
"filterByResource": "Фильтровать по ресурсам",
"selectApprovalState": "Select Approval State",
"filterByApprovalState": "Filter By Approval State",
"approvalListEmpty": "No approvals",
"approvalState": "Approval State",
"approve": "Approve",
"approved": "Approved",
"denied": "Denied",
"deniedApproval": "Denied Approval",
"all": "All",
"deny": "Deny",
"viewDetails": "View Details",
"requestingNewDeviceApproval": "requested a new device",
"resetFilters": "Сбросить фильтры",
"totalBlocked": "Запросы заблокированы Панголином",
"totalRequests": "Всего запросов",
@@ -729,16 +743,28 @@
"countries": "Страны",
"accessRoleCreate": "Создание роли",
"accessRoleCreateDescription": "Создайте новую роль для группы пользователей и выдавайте им разрешения.",
"accessRoleEdit": "Edit Role",
"accessRoleEditDescription": "Edit role information.",
"accessRoleCreateSubmit": "Создать роль",
"accessRoleCreated": "Роль создана",
"accessRoleCreatedDescription": "Роль была успешно создана.",
"accessRoleErrorCreate": "Не удалось создать роль",
"accessRoleErrorCreateDescription": "Произошла ошибка при создании роли.",
"accessRoleUpdateSubmit": "Update Role",
"accessRoleUpdated": "Role updated",
"accessRoleUpdatedDescription": "The role has been successfully updated.",
"accessApprovalUpdated": "Approval processed",
"accessApprovalApprovedDescription": "Set Approval Request decision to approved.",
"accessApprovalDeniedDescription": "Set Approval Request decision to denied.",
"accessRoleErrorUpdate": "Failed to update role",
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
"accessApprovalErrorUpdate": "Failed to process approval",
"accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.",
"accessRoleErrorNewRequired": "Новая роль обязательна",
"accessRoleErrorRemove": "Не удалось удалить роль",
"accessRoleErrorRemoveDescription": "Произошла ошибка при удалении роли.",
"accessRoleName": "Название роли",
"accessRoleQuestionRemove": "Вы собираетесь удалить роль {name}. Это действие нельзя отменить.",
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
"accessRoleRemove": "Удалить роль",
"accessRoleRemoveDescription": "Удалить роль из организации",
"accessRoleRemoveSubmit": "Удалить роль",
@@ -874,7 +900,7 @@
"inviteAlready": "Похоже, вы были приглашены!",
"inviteAlreadyDescription": "Чтобы принять приглашение, вы должны войти или создать учётную запись.",
"signupQuestion": "Уже есть учётная запись?",
"login": "Войти",
"login": "Log In",
"resourceNotFound": "Ресурс не найден",
"resourceNotFoundDescription": "Ресурс, к которому вы пытаетесь получить доступ, не существует.",
"pincodeRequirementsLength": "PIN должен состоять ровно из 6 цифр",
@@ -954,13 +980,13 @@
"passwordExpiryDescription": "Эта организация требует смены пароля каждые {maxDays} дней.",
"changePasswordNow": "Изменить пароль сейчас",
"pincodeAuth": "Код аутентификатора",
"pincodeSubmit2": "Отправить код",
"pincodeSubmit2": "Submit code",
"passwordResetSubmit": "Запросить сброс",
"passwordResetAlreadyHaveCode": "Введите код",
"passwordResetSmtpRequired": "Пожалуйста, обратитесь к администратору",
"passwordResetSmtpRequiredDescription": "Для сброса пароля необходим код сброса пароля. Обратитесь к администратору за помощью.",
"passwordBack": "Назад к паролю",
"loginBack": "Вернуться к входу",
"loginBack": "Go back to main login page",
"signup": "Регистрация",
"loginStart": "Войдите для начала работы",
"idpOidcTokenValidating": "Проверка OIDC токена",
@@ -1118,6 +1144,10 @@
"actionUpdateIdpOrg": "Обновить организацию IDP",
"actionCreateClient": "Создать Клиента",
"actionDeleteClient": "Удалить Клиента",
"actionArchiveClient": "Archive Client",
"actionUnarchiveClient": "Unarchive Client",
"actionBlockClient": "Block Client",
"actionUnblockClient": "Unblock Client",
"actionUpdateClient": "Обновить Клиента",
"actionListClients": "Список Клиентов",
"actionGetClient": "Получить Клиента",
@@ -1134,14 +1164,14 @@
"searchProgress": "Поиск...",
"create": "Создать",
"orgs": "Организации",
"loginError": "Произошла ошибка при входе",
"loginRequiredForDevice": "Для аутентификации устройства необходимо войти в систему.",
"loginError": "An unexpected error occurred. Please try again.",
"loginRequiredForDevice": "Login is required for your device.",
"passwordForgot": "Забыли пароль?",
"otpAuth": "Двухфакторная аутентификация",
"otpAuthDescription": "Введите код из вашего приложения-аутентификатора или один из ваших одноразовых резервных кодов.",
"otpAuthSubmit": "Отправить код",
"idpContinue": "Или продолжить с",
"otpAuthBack": "Вернуться к входу",
"otpAuthBack": "Back to Password",
"navbar": "Навигационное меню",
"navbarDescription": "Главное навигационное меню приложения",
"navbarDocsLink": "Документация",
@@ -1189,6 +1219,7 @@
"sidebarOverview": "Обзор",
"sidebarHome": "Главная",
"sidebarSites": "Сайты",
"sidebarApprovals": "Approval Requests",
"sidebarResources": "Ресурсы",
"sidebarProxyResources": "Публичный",
"sidebarClientResources": "Приватный",
@@ -1205,7 +1236,7 @@
"sidebarIdentityProviders": "Поставщики удостоверений",
"sidebarLicense": "Лицензия",
"sidebarClients": "Клиенты",
"sidebarUserDevices": "Пользователи",
"sidebarUserDevices": "User Devices",
"sidebarMachineClients": "Машины",
"sidebarDomains": "Домены",
"sidebarGeneral": "Управление",
@@ -1277,6 +1308,7 @@
"setupErrorCreateAdmin": "Произошла ошибка при создании учётной записи администратора сервера.",
"certificateStatus": "Статус сертификата",
"loading": "Загрузка",
"loadingAnalytics": "Loading Analytics",
"restart": "Перезагрузка",
"domains": "Домены",
"domainsDescription": "Создание и управление доменами, доступными в организации",
@@ -1304,6 +1336,7 @@
"refreshError": "Не удалось обновить данные",
"verified": "Подтверждено",
"pending": "В ожидании",
"pendingApproval": "Pending Approval",
"sidebarBilling": "Выставление счетов",
"billing": "Выставление счетов",
"orgBillingDescription": "Управление платежной информацией и подписками",
@@ -1420,7 +1453,7 @@
"securityKeyRemoveSuccess": "Ключ безопасности успешно удален",
"securityKeyRemoveError": "Не удалось удалить ключ безопасности",
"securityKeyLoadError": "Не удалось загрузить ключи безопасности",
"securityKeyLogin": "Продолжить с ключом безопасности",
"securityKeyLogin": "Use Security Key",
"securityKeyAuthError": "Не удалось аутентифицироваться с ключом безопасности",
"securityKeyRecommendation": "Зарегистрируйте резервный ключ безопасности на другом устройстве, чтобы всегда иметь доступ к вашему аккаунту.",
"registering": "Регистрация...",
@@ -1547,6 +1580,8 @@
"IntervalSeconds": "Интервал здоровых состояний",
"timeoutSeconds": "Таймаут (сек)",
"timeIsInSeconds": "Время указано в секундах",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need their devices approved by an admin before they can access resources",
"retryAttempts": "Количество попыток повторного запроса",
"expectedResponseCodes": "Ожидаемые коды ответов",
"expectedResponseCodesDescription": "HTTP-код состояния, указывающий на здоровое состояние. Если оставить пустым, 200-300 считается здоровым.",
@@ -1876,7 +1911,7 @@
"orgAuthChooseIdpDescription": "Выберите своего поставщика удостоверений личности для продолжения",
"orgAuthNoIdpConfigured": "Эта организация не имеет настроенных поставщиков идентификационных данных. Вместо этого вы можете войти в свой Pangolin.",
"orgAuthSignInWithPangolin": "Войти через Pangolin",
"orgAuthSignInToOrg": "Войдите в организацию",
"orgAuthSignInToOrg": "Sign in to an organization",
"orgAuthSelectOrgTitle": "Вход в организацию",
"orgAuthSelectOrgDescription": "Введите ID вашей организации, чтобы продолжить",
"orgAuthOrgIdPlaceholder": "ваша-организация",
@@ -2232,6 +2267,8 @@
"deviceCodeInvalidFormat": "Код должен быть 9 символов (например, A1AJ-N5JD)",
"deviceCodeInvalidOrExpired": "Неверный или просроченный код",
"deviceCodeVerifyFailed": "Не удалось проверить код устройства",
"deviceCodeValidating": "Validating device code...",
"deviceCodeVerifying": "Verifying device authorization...",
"signedInAs": "Вы вошли как",
"deviceCodeEnterPrompt": "Введите код, отображаемый на устройстве",
"continue": "Продолжить",
@@ -2244,7 +2281,7 @@
"deviceOrganizationsAccess": "Доступ ко всем организациям, к которым ваш аккаунт имеет доступ",
"deviceAuthorize": "Авторизовать {applicationName}",
"deviceConnected": "Устройство подключено!",
"deviceAuthorizedMessage": "Устройство авторизовано для доступа к вашей учетной записи.",
"deviceAuthorizedMessage": "Device is authorized to access your account. Please return to the client application.",
"pangolinCloud": "Облако Панголина",
"viewDevices": "Просмотр устройств",
"viewDevicesDescription": "Управление подключенными устройствами",
@@ -2306,6 +2343,7 @@
"identifier": "Identifier",
"deviceLoginUseDifferentAccount": "Не вы? Используйте другую учетную запись.",
"deviceLoginDeviceRequestingAccessToAccount": "Устройство запрашивает доступ к этой учетной записи.",
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
"noData": "Нет данных",
"machineClients": "Машинные клиенты",
"install": "Установить",
@@ -2394,5 +2432,56 @@
"maintenanceScreenTitle": "Сервис временно недоступен",
"maintenanceScreenMessage": "В настоящее время мы испытываем технические трудности. Пожалуйста, зайдите позже.",
"maintenanceScreenEstimatedCompletion": "Предполагаемое завершение:",
"createInternalResourceDialogDestinationRequired": "Укажите адрес назначения. Это может быть имя хоста или IP-адрес."
"createInternalResourceDialogDestinationRequired": "Укажите адрес назначения. Это может быть имя хоста или IP-адрес.",
"available": "Available",
"archived": "Archived",
"noArchivedDevices": "No archived devices found",
"deviceArchived": "Device archived",
"deviceArchivedDescription": "The device has been successfully archived.",
"errorArchivingDevice": "Error archiving device",
"failedToArchiveDevice": "Failed to archive device",
"deviceQuestionArchive": "Are you sure you want to archive this device?",
"deviceMessageArchive": "The device will be archived and removed from your active devices list.",
"deviceArchiveConfirm": "Archive Device",
"archiveDevice": "Archive Device",
"archive": "Archive",
"deviceUnarchived": "Device unarchived",
"deviceUnarchivedDescription": "The device has been successfully unarchived.",
"errorUnarchivingDevice": "Error unarchiving device",
"failedToUnarchiveDevice": "Failed to unarchive device",
"unarchive": "Unarchive",
"archiveClient": "Archive Client",
"archiveClientQuestion": "Are you sure you want to archive this client?",
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
"archiveClientConfirm": "Archive Client",
"blockClient": "Block Client",
"blockClientQuestion": "Are you sure you want to block this client?",
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
"blockClientConfirm": "Block Client",
"active": "Active",
"usernameOrEmail": "Username or Email",
"selectYourOrganization": "Select your organization",
"signInTo": "Log in in to",
"signInWithPassword": "Continue with Password",
"noAuthMethodsAvailable": "No authentication methods available for this organization.",
"enterPassword": "Enter your password",
"enterMfaCode": "Enter the code from your authenticator app",
"securityKeyRequired": "Please use your security key to sign in.",
"needToUseAnotherAccount": "Need to use a different account?",
"loginLegalDisclaimer": "By clicking the buttons below, you acknowledge you have read, understand, and agree to the <termsOfService>Terms of Service</termsOfService> and <privacyPolicy>Privacy Policy</privacyPolicy>.",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"userNotFoundWithUsername": "No user found with that username.",
"verify": "Verify",
"signIn": "Sign In",
"forgotPassword": "Forgot password?",
"orgSignInTip": "If you've logged in before, you can enter your username or email above to authenticate with your organization's identity provider instead. It's easier!",
"continueAnyway": "Continue anyway",
"dontShowAgain": "Don't show again",
"orgSignInNotice": "Did you know?",
"signupOrgNotice": "Trying to sign in?",
"signupOrgTip": "Are you trying to sign in through your organization's identity provider?",
"signupOrgLink": "Sign in or sign up with your organization instead",
"verifyEmailLogInWithDifferentAccount": "Use a Different Account",
"logIn": "Log In"
}

View File

@@ -257,6 +257,8 @@
"accessRolesSearch": "Rolleri ara...",
"accessRolesAdd": "Rol Ekle",
"accessRoleDelete": "Rolü Sil",
"accessApprovalsManage": "Manage Approvals",
"accessApprovalsDescription": "Manage approval requests in the organization",
"description": "Açıklama",
"inviteTitle": "Açık Davetiyeler",
"inviteDescription": "Organizasyona katılmak için diğer kullanıcılar için davetleri yönetin",
@@ -450,6 +452,18 @@
"selectDuration": "Süreyi seçin",
"selectResource": "Kaynak Seçin",
"filterByResource": "Kaynağa Göre Filtrele",
"selectApprovalState": "Select Approval State",
"filterByApprovalState": "Filter By Approval State",
"approvalListEmpty": "No approvals",
"approvalState": "Approval State",
"approve": "Approve",
"approved": "Approved",
"denied": "Denied",
"deniedApproval": "Denied Approval",
"all": "All",
"deny": "Deny",
"viewDetails": "View Details",
"requestingNewDeviceApproval": "requested a new device",
"resetFilters": "Filtreleri Sıfırla",
"totalBlocked": "Pangolin Tarafından Engellenen İstekler",
"totalRequests": "Toplam İstekler",
@@ -729,16 +743,28 @@
"countries": "Ülkeler",
"accessRoleCreate": "Rol Oluştur",
"accessRoleCreateDescription": "Kullanıcıları gruplamak ve izinlerini yönetmek için yeni bir rol oluşturun.",
"accessRoleEdit": "Edit Role",
"accessRoleEditDescription": "Edit role information.",
"accessRoleCreateSubmit": "Rol Oluştur",
"accessRoleCreated": "Rol oluşturuldu",
"accessRoleCreatedDescription": "Rol başarıyla oluşturuldu.",
"accessRoleErrorCreate": "Rol oluşturulamadı",
"accessRoleErrorCreateDescription": "Rol oluşturulurken bir hata oluştu.",
"accessRoleUpdateSubmit": "Update Role",
"accessRoleUpdated": "Role updated",
"accessRoleUpdatedDescription": "The role has been successfully updated.",
"accessApprovalUpdated": "Approval processed",
"accessApprovalApprovedDescription": "Set Approval Request decision to approved.",
"accessApprovalDeniedDescription": "Set Approval Request decision to denied.",
"accessRoleErrorUpdate": "Failed to update role",
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
"accessApprovalErrorUpdate": "Failed to process approval",
"accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.",
"accessRoleErrorNewRequired": "Yeni rol gerekli",
"accessRoleErrorRemove": "Rol kaldırılamadı",
"accessRoleErrorRemoveDescription": "Rol kaldırılırken bir hata oluştu.",
"accessRoleName": "Rol Adı",
"accessRoleQuestionRemove": "{name} rolünü silmek üzeresiniz. Bu eylemi geri alamazsınız.",
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
"accessRoleRemove": "Rolü Kaldır",
"accessRoleRemoveDescription": "Kuruluştan bir rol kaldır",
"accessRoleRemoveSubmit": "Rolü Kaldır",
@@ -874,7 +900,7 @@
"inviteAlready": "Davetiye gönderilmiş gibi görünüyor!",
"inviteAlreadyDescription": "Daveti kabul etmek için giriş yapmalı veya bir hesap oluşturmalısınız.",
"signupQuestion": "Zaten bir hesabınız var mı?",
"login": "Giriş yap",
"login": "Log In",
"resourceNotFound": "No resources found",
"resourceNotFoundDescription": "Erişmeye çalıştığınız kaynak mevcut değil.",
"pincodeRequirementsLength": "PIN kesinlikle 6 haneli olmalıdır",
@@ -954,13 +980,13 @@
"passwordExpiryDescription": "Bu kuruluş, parolanızı {maxDays} günde bir değiştirmenizi gerektirir.",
"changePasswordNow": "Şifrenizi Şimdi Değiştirin",
"pincodeAuth": "Kimlik Doğrulama Kodu",
"pincodeSubmit2": "Kodu Gönder",
"pincodeSubmit2": "Submit code",
"passwordResetSubmit": "Sıfırlama İsteği",
"passwordResetAlreadyHaveCode": "Kodu Girin",
"passwordResetSmtpRequired": "Yönetici ile iletişime geçin",
"passwordResetSmtpRequiredDescription": "Parolanızı sıfırlamak için bir parola sıfırlama kodu gereklidir. Yardım için yönetici ile iletişime geçin.",
"passwordBack": "Şifreye Geri Dön",
"loginBack": "Girişe geri dön",
"loginBack": "Go back to main login page",
"signup": "Kaydol",
"loginStart": "Başlamak için giriş yapın",
"idpOidcTokenValidating": "OIDC token'ı doğrulanıyor",
@@ -1118,6 +1144,10 @@
"actionUpdateIdpOrg": "Kimlik Sağlayıcı Organizasyonu Güncelle",
"actionCreateClient": "Müşteri Oluştur",
"actionDeleteClient": "Müşteri Sil",
"actionArchiveClient": "Archive Client",
"actionUnarchiveClient": "Unarchive Client",
"actionBlockClient": "Block Client",
"actionUnblockClient": "Unblock Client",
"actionUpdateClient": "Müşteri Güncelle",
"actionListClients": "Müşterileri Listele",
"actionGetClient": "Müşteriyi Al",
@@ -1134,14 +1164,14 @@
"searchProgress": "Ara...",
"create": "Oluştur",
"orgs": "Organizasyonlar",
"loginError": "Giriş yaparken bir hata oluştu",
"loginRequiredForDevice": "Cihazınızı kimlik doğrulamak için giriş yapılması gereklidir.",
"loginError": "An unexpected error occurred. Please try again.",
"loginRequiredForDevice": "Login is required for your device.",
"passwordForgot": "Şifrenizi mi unuttunuz?",
"otpAuth": "İki Faktörlü Kimlik Doğrulama",
"otpAuthDescription": "Authenticator uygulamanızdan veya tek kullanımlık yedek kodlarınızdan birini girin.",
"otpAuthSubmit": "Kodu Gönder",
"idpContinue": "Veya devam et:",
"otpAuthBack": "Girişe Dön",
"otpAuthBack": "Back to Password",
"navbar": "Navigasyon Menüsü",
"navbarDescription": "Uygulamanın ana navigasyon menüsü",
"navbarDocsLink": "Dokümantasyon",
@@ -1189,6 +1219,7 @@
"sidebarOverview": "Genel Bakış",
"sidebarHome": "Ana Sayfa",
"sidebarSites": "Siteler",
"sidebarApprovals": "Approval Requests",
"sidebarResources": "Kaynaklar",
"sidebarProxyResources": "Herkese Açık",
"sidebarClientResources": "Özel",
@@ -1205,7 +1236,7 @@
"sidebarIdentityProviders": "Kimlik Sağlayıcılar",
"sidebarLicense": "Lisans",
"sidebarClients": "İstemciler",
"sidebarUserDevices": "Kullanıcılar",
"sidebarUserDevices": "User Devices",
"sidebarMachineClients": "Makineler",
"sidebarDomains": "Alan Adları",
"sidebarGeneral": "Yönet",
@@ -1277,6 +1308,7 @@
"setupErrorCreateAdmin": "Sunucu yönetici hesabı oluşturulurken bir hata oluştu.",
"certificateStatus": "Sertifika Durumu",
"loading": "Yükleniyor",
"loadingAnalytics": "Loading Analytics",
"restart": "Yeniden Başlat",
"domains": "Alan Adları",
"domainsDescription": "Organizasyonda kullanılabilir alan adlarını oluşturun ve yönetin",
@@ -1304,6 +1336,7 @@
"refreshError": "Veriler yenilenemedi",
"verified": "Doğrulandı",
"pending": "Beklemede",
"pendingApproval": "Pending Approval",
"sidebarBilling": "Faturalama",
"billing": "Faturalama",
"orgBillingDescription": "Fatura bilgilerinizi ve aboneliklerinizi yönetin",
@@ -1420,7 +1453,7 @@
"securityKeyRemoveSuccess": "Güvenlik anahtarı başarıyla kaldırıldı",
"securityKeyRemoveError": "Güvenlik anahtarı kaldırılırken hata oluştu",
"securityKeyLoadError": "Güvenlik anahtarları yüklenirken hata oluştu",
"securityKeyLogin": "Güvenlik anahtarı ile devam edin",
"securityKeyLogin": "Use Security Key",
"securityKeyAuthError": "Güvenlik anahtarı ile kimlik doğrulama başarısız oldu",
"securityKeyRecommendation": "Hesabınızdan kilitlenmediğinizden emin olmak için farklı bir cihazda başka bir güvenlik anahtarı kaydetmeyi düşünün.",
"registering": "Kaydediliyor...",
@@ -1547,6 +1580,8 @@
"IntervalSeconds": "Sağlıklı Aralık",
"timeoutSeconds": "Zaman Aşımı (saniye)",
"timeIsInSeconds": "Zaman saniye cinsindendir",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need their devices approved by an admin before they can access resources",
"retryAttempts": "Tekrar Deneme Girişimleri",
"expectedResponseCodes": "Beklenen Yanıt Kodları",
"expectedResponseCodesDescription": "Sağlıklı durumu gösteren HTTP durum kodu. Boş bırakılırsa, 200-300 arası sağlıklı kabul edilir.",
@@ -1876,7 +1911,7 @@
"orgAuthChooseIdpDescription": "Devam etmek için kimlik sağlayıcınızı seçin",
"orgAuthNoIdpConfigured": "Bu kuruluşta yapılandırılmış kimlik sağlayıcı yok. Bunun yerine Pangolin kimliğinizle giriş yapabilirsiniz.",
"orgAuthSignInWithPangolin": "Pangolin ile Giriş Yap",
"orgAuthSignInToOrg": "Bir kuruluşa giriş yapın",
"orgAuthSignInToOrg": "Sign in to an organization",
"orgAuthSelectOrgTitle": "Kuruluş Giriş",
"orgAuthSelectOrgDescription": "Devam etmek için kuruluş kimliğinizi girin",
"orgAuthOrgIdPlaceholder": "kuruluşunuz",
@@ -2232,6 +2267,8 @@
"deviceCodeInvalidFormat": "Kod 9 karakter olmalı (ör. A1AJ-N5JD)",
"deviceCodeInvalidOrExpired": "Geçersiz veya süresi dolmuş kod",
"deviceCodeVerifyFailed": "Cihaz kodu doğrulanamadı",
"deviceCodeValidating": "Validating device code...",
"deviceCodeVerifying": "Verifying device authorization...",
"signedInAs": "Olarak giriş yapıldı",
"deviceCodeEnterPrompt": "Cihazda gösterilen kodu girin",
"continue": "Devam Et",
@@ -2244,7 +2281,7 @@
"deviceOrganizationsAccess": "Hesabınızın erişim hakkına sahip olduğu tüm organizasyonlara erişim",
"deviceAuthorize": "{uygulamaAdi} yetkilendir",
"deviceConnected": "Cihaz Bağlandı!",
"deviceAuthorizedMessage": "Cihazınız, hesabınıza erişim izni almıştır.",
"deviceAuthorizedMessage": "Device is authorized to access your account. Please return to the client application.",
"pangolinCloud": "Pangolin Cloud",
"viewDevices": "Cihazları Görüntüle",
"viewDevicesDescription": "Bağlantılı cihazlarınızı yönetin",
@@ -2306,6 +2343,7 @@
"identifier": "Tanımlayıcı",
"deviceLoginUseDifferentAccount": "Siz değil misiniz? Farklı bir hesap kullanın.",
"deviceLoginDeviceRequestingAccessToAccount": "Bir cihaz bu hesaba erişim talep ediyor.",
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
"noData": "Veri Yok",
"machineClients": "Makine İstemcileri",
"install": "Yükle",
@@ -2394,5 +2432,56 @@
"maintenanceScreenTitle": "Servis Geçici Olarak Kullanılamıyor",
"maintenanceScreenMessage": "Şu anda teknik zorluklar yaşıyoruz. Lütfen yakında tekrar kontrol edin.",
"maintenanceScreenEstimatedCompletion": "Tahmini Tamamlama:",
"createInternalResourceDialogDestinationRequired": "Hedef gereklidir"
"createInternalResourceDialogDestinationRequired": "Hedef gereklidir",
"available": "Available",
"archived": "Archived",
"noArchivedDevices": "No archived devices found",
"deviceArchived": "Device archived",
"deviceArchivedDescription": "The device has been successfully archived.",
"errorArchivingDevice": "Error archiving device",
"failedToArchiveDevice": "Failed to archive device",
"deviceQuestionArchive": "Are you sure you want to archive this device?",
"deviceMessageArchive": "The device will be archived and removed from your active devices list.",
"deviceArchiveConfirm": "Archive Device",
"archiveDevice": "Archive Device",
"archive": "Archive",
"deviceUnarchived": "Device unarchived",
"deviceUnarchivedDescription": "The device has been successfully unarchived.",
"errorUnarchivingDevice": "Error unarchiving device",
"failedToUnarchiveDevice": "Failed to unarchive device",
"unarchive": "Unarchive",
"archiveClient": "Archive Client",
"archiveClientQuestion": "Are you sure you want to archive this client?",
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
"archiveClientConfirm": "Archive Client",
"blockClient": "Block Client",
"blockClientQuestion": "Are you sure you want to block this client?",
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
"blockClientConfirm": "Block Client",
"active": "Active",
"usernameOrEmail": "Username or Email",
"selectYourOrganization": "Select your organization",
"signInTo": "Log in in to",
"signInWithPassword": "Continue with Password",
"noAuthMethodsAvailable": "No authentication methods available for this organization.",
"enterPassword": "Enter your password",
"enterMfaCode": "Enter the code from your authenticator app",
"securityKeyRequired": "Please use your security key to sign in.",
"needToUseAnotherAccount": "Need to use a different account?",
"loginLegalDisclaimer": "By clicking the buttons below, you acknowledge you have read, understand, and agree to the <termsOfService>Terms of Service</termsOfService> and <privacyPolicy>Privacy Policy</privacyPolicy>.",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"userNotFoundWithUsername": "No user found with that username.",
"verify": "Verify",
"signIn": "Sign In",
"forgotPassword": "Forgot password?",
"orgSignInTip": "If you've logged in before, you can enter your username or email above to authenticate with your organization's identity provider instead. It's easier!",
"continueAnyway": "Continue anyway",
"dontShowAgain": "Don't show again",
"orgSignInNotice": "Did you know?",
"signupOrgNotice": "Trying to sign in?",
"signupOrgTip": "Are you trying to sign in through your organization's identity provider?",
"signupOrgLink": "Sign in or sign up with your organization instead",
"verifyEmailLogInWithDifferentAccount": "Use a Different Account",
"logIn": "Log In"
}

View File

@@ -257,6 +257,8 @@
"accessRolesSearch": "搜索角色...",
"accessRolesAdd": "添加角色",
"accessRoleDelete": "删除角色",
"accessApprovalsManage": "Manage Approvals",
"accessApprovalsDescription": "Manage approval requests in the organization",
"description": "描述",
"inviteTitle": "打开邀请",
"inviteDescription": "管理其他用户加入机构的邀请",
@@ -450,6 +452,18 @@
"selectDuration": "选择持续时间",
"selectResource": "选择资源",
"filterByResource": "按资源过滤",
"selectApprovalState": "Select Approval State",
"filterByApprovalState": "Filter By Approval State",
"approvalListEmpty": "No approvals",
"approvalState": "Approval State",
"approve": "Approve",
"approved": "Approved",
"denied": "Denied",
"deniedApproval": "Denied Approval",
"all": "All",
"deny": "Deny",
"viewDetails": "View Details",
"requestingNewDeviceApproval": "requested a new device",
"resetFilters": "重置过滤器",
"totalBlocked": "被Pangolin阻止的请求",
"totalRequests": "总请求",
@@ -729,16 +743,28 @@
"countries": "国家",
"accessRoleCreate": "创建角色",
"accessRoleCreateDescription": "创建一个新角色来分组用户并管理他们的权限。",
"accessRoleEdit": "Edit Role",
"accessRoleEditDescription": "Edit role information.",
"accessRoleCreateSubmit": "创建角色",
"accessRoleCreated": "角色已创建",
"accessRoleCreatedDescription": "角色已成功创建。",
"accessRoleErrorCreate": "创建角色失败",
"accessRoleErrorCreateDescription": "创建角色时出错。",
"accessRoleUpdateSubmit": "Update Role",
"accessRoleUpdated": "Role updated",
"accessRoleUpdatedDescription": "The role has been successfully updated.",
"accessApprovalUpdated": "Approval processed",
"accessApprovalApprovedDescription": "Set Approval Request decision to approved.",
"accessApprovalDeniedDescription": "Set Approval Request decision to denied.",
"accessRoleErrorUpdate": "Failed to update role",
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
"accessApprovalErrorUpdate": "Failed to process approval",
"accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.",
"accessRoleErrorNewRequired": "需要新角色",
"accessRoleErrorRemove": "删除角色失败",
"accessRoleErrorRemoveDescription": "删除角色时出错。",
"accessRoleName": "角色名称",
"accessRoleQuestionRemove": "您即将删除 {name} 角色。 此操作无法撤销。",
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
"accessRoleRemove": "删除角色",
"accessRoleRemoveDescription": "从组织中删除角色",
"accessRoleRemoveSubmit": "删除角色",
@@ -874,7 +900,7 @@
"inviteAlready": "看起来您已被邀请!",
"inviteAlreadyDescription": "要接受邀请,您必须登录或创建一个帐户。",
"signupQuestion": "已经有一个帐户?",
"login": "登录",
"login": "Log In",
"resourceNotFound": "找不到资源",
"resourceNotFoundDescription": "您要访问的资源不存在。",
"pincodeRequirementsLength": "PIN码必须是6位数字",
@@ -954,13 +980,13 @@
"passwordExpiryDescription": "该机构要求您每 {maxDays} 天更改一次密码。",
"changePasswordNow": "现在更改密码",
"pincodeAuth": "验证器代码",
"pincodeSubmit2": "提交代码",
"pincodeSubmit2": "Submit code",
"passwordResetSubmit": "请求重置",
"passwordResetAlreadyHaveCode": "输入代码",
"passwordResetSmtpRequired": "请联系您的管理员",
"passwordResetSmtpRequiredDescription": "需要密码重置密码。请联系您的管理员寻求帮助。",
"passwordBack": "回到密码",
"loginBack": "返回登录",
"loginBack": "Go back to main login page",
"signup": "注册",
"loginStart": "登录以开始",
"idpOidcTokenValidating": "正在验证 OIDC 令牌",
@@ -1118,6 +1144,10 @@
"actionUpdateIdpOrg": "更新 IDP组织",
"actionCreateClient": "创建客户端",
"actionDeleteClient": "删除客户端",
"actionArchiveClient": "Archive Client",
"actionUnarchiveClient": "Unarchive Client",
"actionBlockClient": "Block Client",
"actionUnblockClient": "Unblock Client",
"actionUpdateClient": "更新客户端",
"actionListClients": "列出客户端",
"actionGetClient": "获取客户端",
@@ -1134,14 +1164,14 @@
"searchProgress": "搜索中...",
"create": "创建",
"orgs": "组织",
"loginError": "登录时出错",
"loginRequiredForDevice": "需要登录才能验证您的设备。",
"loginError": "An unexpected error occurred. Please try again.",
"loginRequiredForDevice": "Login is required for your device.",
"passwordForgot": "忘记密码?",
"otpAuth": "两步验证",
"otpAuthDescription": "从您的身份验证程序中输入代码或您的单次备份代码。",
"otpAuthSubmit": "提交代码",
"idpContinue": "或者继续",
"otpAuthBack": "返回登录",
"otpAuthBack": "Back to Password",
"navbar": "导航菜单",
"navbarDescription": "应用程序的主导航菜单",
"navbarDocsLink": "文件",
@@ -1189,6 +1219,7 @@
"sidebarOverview": "概览",
"sidebarHome": "首页",
"sidebarSites": "站点",
"sidebarApprovals": "Approval Requests",
"sidebarResources": "资源",
"sidebarProxyResources": "公开的",
"sidebarClientResources": "非公开的",
@@ -1205,7 +1236,7 @@
"sidebarIdentityProviders": "身份提供商",
"sidebarLicense": "证书",
"sidebarClients": "客户端",
"sidebarUserDevices": "用户",
"sidebarUserDevices": "User Devices",
"sidebarMachineClients": "机",
"sidebarDomains": "域",
"sidebarGeneral": "管理",
@@ -1277,6 +1308,7 @@
"setupErrorCreateAdmin": "创建服务器管理员账户时发生错误。",
"certificateStatus": "证书状态",
"loading": "加载中",
"loadingAnalytics": "Loading Analytics",
"restart": "重启",
"domains": "域",
"domainsDescription": "创建和管理组织中可用的域",
@@ -1304,6 +1336,7 @@
"refreshError": "刷新数据失败",
"verified": "已验证",
"pending": "待定",
"pendingApproval": "Pending Approval",
"sidebarBilling": "计费",
"billing": "计费",
"orgBillingDescription": "管理账单信息和订阅",
@@ -1420,7 +1453,7 @@
"securityKeyRemoveSuccess": "安全密钥删除成功",
"securityKeyRemoveError": "删除安全密钥失败",
"securityKeyLoadError": "加载安全密钥失败",
"securityKeyLogin": "使用安全密钥继续",
"securityKeyLogin": "Use Security Key",
"securityKeyAuthError": "使用安全密钥认证失败",
"securityKeyRecommendation": "考虑在其他设备上注册另一个安全密钥,以确保不会被锁定在您的账户之外。",
"registering": "注册中...",
@@ -1547,6 +1580,8 @@
"IntervalSeconds": "正常间隔",
"timeoutSeconds": "超时(秒)",
"timeIsInSeconds": "时间以秒为单位",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need their devices approved by an admin before they can access resources",
"retryAttempts": "重试次数",
"expectedResponseCodes": "期望响应代码",
"expectedResponseCodesDescription": "HTTP 状态码表示健康状态。如留空200-300 被视为健康。",
@@ -1876,7 +1911,7 @@
"orgAuthChooseIdpDescription": "选择您的身份提供商以继续",
"orgAuthNoIdpConfigured": "此机构没有配置任何身份提供者。您可以使用您的 Pangolin 身份登录。",
"orgAuthSignInWithPangolin": "使用 Pangolin 登录",
"orgAuthSignInToOrg": "登录到一个组织",
"orgAuthSignInToOrg": "Sign in to an organization",
"orgAuthSelectOrgTitle": "组织登录",
"orgAuthSelectOrgDescription": "输入您的组织ID以继续",
"orgAuthOrgIdPlaceholder": "您的组织",
@@ -2232,6 +2267,8 @@
"deviceCodeInvalidFormat": "代码必须是9个字符(如A1AJ-N5JD)",
"deviceCodeInvalidOrExpired": "无效或过期的代码",
"deviceCodeVerifyFailed": "验证设备代码失败",
"deviceCodeValidating": "Validating device code...",
"deviceCodeVerifying": "Verifying device authorization...",
"signedInAs": "登录为",
"deviceCodeEnterPrompt": "输入设备上显示的代码",
"continue": "继续",
@@ -2244,7 +2281,7 @@
"deviceOrganizationsAccess": "访问您的帐户拥有访问权限的所有组织",
"deviceAuthorize": "授权{applicationName}",
"deviceConnected": "设备已连接!",
"deviceAuthorizedMessage": "设备被授权访问您的帐户。",
"deviceAuthorizedMessage": "Device is authorized to access your account. Please return to the client application.",
"pangolinCloud": "邦戈林云",
"viewDevices": "查看设备",
"viewDevicesDescription": "管理您已连接的设备",
@@ -2306,6 +2343,7 @@
"identifier": "Identifier",
"deviceLoginUseDifferentAccount": "不是你?使用一个不同的帐户。",
"deviceLoginDeviceRequestingAccessToAccount": "设备正在请求访问此帐户。",
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
"noData": "无数据",
"machineClients": "机器客户端",
"install": "安装",
@@ -2394,5 +2432,56 @@
"maintenanceScreenTitle": "服务暂时不可用",
"maintenanceScreenMessage": "我们目前遇到技术问题。 请稍后再回来查看。",
"maintenanceScreenEstimatedCompletion": "预计完成时间:",
"createInternalResourceDialogDestinationRequired": "需要目标地址"
"createInternalResourceDialogDestinationRequired": "需要目标地址",
"available": "Available",
"archived": "Archived",
"noArchivedDevices": "No archived devices found",
"deviceArchived": "Device archived",
"deviceArchivedDescription": "The device has been successfully archived.",
"errorArchivingDevice": "Error archiving device",
"failedToArchiveDevice": "Failed to archive device",
"deviceQuestionArchive": "Are you sure you want to archive this device?",
"deviceMessageArchive": "The device will be archived and removed from your active devices list.",
"deviceArchiveConfirm": "Archive Device",
"archiveDevice": "Archive Device",
"archive": "Archive",
"deviceUnarchived": "Device unarchived",
"deviceUnarchivedDescription": "The device has been successfully unarchived.",
"errorUnarchivingDevice": "Error unarchiving device",
"failedToUnarchiveDevice": "Failed to unarchive device",
"unarchive": "Unarchive",
"archiveClient": "Archive Client",
"archiveClientQuestion": "Are you sure you want to archive this client?",
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
"archiveClientConfirm": "Archive Client",
"blockClient": "Block Client",
"blockClientQuestion": "Are you sure you want to block this client?",
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
"blockClientConfirm": "Block Client",
"active": "Active",
"usernameOrEmail": "Username or Email",
"selectYourOrganization": "Select your organization",
"signInTo": "Log in in to",
"signInWithPassword": "Continue with Password",
"noAuthMethodsAvailable": "No authentication methods available for this organization.",
"enterPassword": "Enter your password",
"enterMfaCode": "Enter the code from your authenticator app",
"securityKeyRequired": "Please use your security key to sign in.",
"needToUseAnotherAccount": "Need to use a different account?",
"loginLegalDisclaimer": "By clicking the buttons below, you acknowledge you have read, understand, and agree to the <termsOfService>Terms of Service</termsOfService> and <privacyPolicy>Privacy Policy</privacyPolicy>.",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"userNotFoundWithUsername": "No user found with that username.",
"verify": "Verify",
"signIn": "Sign In",
"forgotPassword": "Forgot password?",
"orgSignInTip": "If you've logged in before, you can enter your username or email above to authenticate with your organization's identity provider instead. It's easier!",
"continueAnyway": "Continue anyway",
"dontShowAgain": "Don't show again",
"orgSignInNotice": "Did you know?",
"signupOrgNotice": "Trying to sign in?",
"signupOrgTip": "Are you trying to sign in through your organization's identity provider?",
"signupOrgLink": "Sign in or sign up with your organization instead",
"verifyEmailLogInWithDifferentAccount": "Use a Different Account",
"logIn": "Log In"
}

View File

@@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Identity-aware VPN and proxy for remote access to anything, anywhere and Dashboard UI",
"description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI",
"homepage": "https://github.com/fosrl/pangolin",
"repository": {
"type": "git",

View File

@@ -78,10 +78,6 @@ export enum ActionsEnum {
updateSiteResource = "updateSiteResource",
createClient = "createClient",
deleteClient = "deleteClient",
archiveClient = "archiveClient",
unarchiveClient = "unarchiveClient",
blockClient = "blockClient",
unblockClient = "unblockClient",
updateClient = "updateClient",
listClients = "listClients",
getClient = "getClient",
@@ -129,9 +125,7 @@ export enum ActionsEnum {
getBlueprint = "getBlueprint",
applyBlueprint = "applyBlueprint",
viewLogs = "viewLogs",
exportLogs = "exportLogs",
listApprovals = "listApprovals",
updateApprovals = "updateApprovals"
exportLogs = "exportLogs"
}
export async function checkUserActionPermission(

View File

@@ -1,150 +0,0 @@
{
"iPad1,1": "iPad",
"iPad2,1": "iPad 2",
"iPad2,2": "iPad 2",
"iPad2,3": "iPad 2",
"iPad2,4": "iPad 2",
"iPad3,1": "iPad 3rd Gen",
"iPad3,3": "iPad 3rd Gen",
"iPad3,2": "iPad 3rd Gen",
"iPad3,4": "iPad 4th Gen",
"iPad3,5": "iPad 4th Gen",
"iPad3,6": "iPad 4th Gen",
"iPad6,11": "iPad 9.7 5th Gen",
"iPad6,12": "iPad 9.7 5th Gen",
"iPad7,5": "iPad 9.7 6th Gen",
"iPad7,6": "iPad 9.7 6th Gen",
"iPad7,11": "iPad 10.2 7th Gen",
"iPad7,12": "iPad 10.2 7th Gen",
"iPad11,6": "iPad 10.2 8th Gen",
"iPad11,7": "iPad 10.2 8th Gen",
"iPad12,1": "iPad 10.2 9th Gen",
"iPad12,2": "iPad 10.2 9th Gen",
"iPad13,18": "iPad 10.9 10th Gen",
"iPad13,19": "iPad 10.9 10th Gen",
"iPad4,1": "iPad Air",
"iPad4,2": "iPad Air",
"iPad4,3": "iPad Air",
"iPad5,3": "iPad Air 2",
"iPad5,4": "iPad Air 2",
"iPad11,3": "iPad Air 3rd Gen",
"iPad11,4": "iPad Air 3rd Gen",
"iPad13,1": "iPad Air 4th Gen",
"iPad13,2": "iPad Air 4th Gen",
"iPad13,16": "iPad Air 5th Gen",
"iPad13,17": "iPad Air 5th Gen",
"iPad14,8": "iPad Air M2 11",
"iPad14,9": "iPad Air M2 11",
"iPad14,10": "iPad Air M2 13",
"iPad14,11": "iPad Air M2 13",
"iPad2,5": "iPad mini",
"iPad2,6": "iPad mini",
"iPad2,7": "iPad mini",
"iPad4,4": "iPad mini 2",
"iPad4,5": "iPad mini 2",
"iPad4,6": "iPad mini 2",
"iPad4,7": "iPad mini 3",
"iPad4,8": "iPad mini 3",
"iPad4,9": "iPad mini 3",
"iPad5,1": "iPad mini 4",
"iPad5,2": "iPad mini 4",
"iPad11,1": "iPad mini 5th Gen",
"iPad11,2": "iPad mini 5th Gen",
"iPad14,1": "iPad mini 6th Gen",
"iPad14,2": "iPad mini 6th Gen",
"iPad6,7": "iPad Pro 12.9",
"iPad6,8": "iPad Pro 12.9",
"iPad6,3": "iPad Pro 9.7",
"iPad6,4": "iPad Pro 9.7",
"iPad7,3": "iPad Pro 10.5",
"iPad7,4": "iPad Pro 10.5",
"iPad7,1": "iPad Pro 12.9",
"iPad7,2": "iPad Pro 12.9",
"iPad8,1": "iPad Pro 11",
"iPad8,2": "iPad Pro 11",
"iPad8,3": "iPad Pro 11",
"iPad8,4": "iPad Pro 11",
"iPad8,5": "iPad Pro 12.9",
"iPad8,6": "iPad Pro 12.9",
"iPad8,7": "iPad Pro 12.9",
"iPad8,8": "iPad Pro 12.9",
"iPad8,9": "iPad Pro 11",
"iPad8,10": "iPad Pro 11",
"iPad8,11": "iPad Pro 12.9",
"iPad8,12": "iPad Pro 12.9",
"iPad13,4": "iPad Pro 11",
"iPad13,5": "iPad Pro 11",
"iPad13,6": "iPad Pro 11",
"iPad13,7": "iPad Pro 11",
"iPad13,8": "iPad Pro 12.9",
"iPad13,9": "iPad Pro 12.9",
"iPad13,10": "iPad Pro 12.9",
"iPad13,11": "iPad Pro 12.9",
"iPad14,3": "iPad Pro 11",
"iPad14,4": "iPad Pro 11",
"iPad14,5": "iPad Pro 12.9",
"iPad14,6": "iPad Pro 12.9",
"iPad16,3": "iPad Pro M4 11",
"iPad16,4": "iPad Pro M4 11",
"iPad16,5": "iPad Pro M4 13",
"iPad16,6": "iPad Pro M4 13",
"iPhone1,1": "iPhone",
"iPhone1,2": "iPhone 3G",
"iPhone2,1": "iPhone 3GS",
"iPhone3,1": "iPhone 4",
"iPhone3,2": "iPhone 4",
"iPhone3,3": "iPhone 4",
"iPhone4,1": "iPhone 4S",
"iPhone5,1": "iPhone 5",
"iPhone5,2": "iPhone 5",
"iPhone5,3": "iPhone 5c",
"iPhone5,4": "iPhone 5c",
"iPhone6,1": "iPhone 5s",
"iPhone6,2": "iPhone 5s",
"iPhone7,2": "iPhone 6",
"iPhone7,1": "iPhone 6 Plus",
"iPhone8,1": "iPhone 6s",
"iPhone8,2": "iPhone 6s Plus",
"iPhone8,4": "iPhone SE",
"iPhone9,1": "iPhone 7",
"iPhone9,3": "iPhone 7",
"iPhone9,2": "iPhone 7 Plus",
"iPhone9,4": "iPhone 7 Plus",
"iPhone10,1": "iPhone 8",
"iPhone10,4": "iPhone 8",
"iPhone10,2": "iPhone 8 Plus",
"iPhone10,5": "iPhone 8 Plus",
"iPhone10,3": "iPhone X",
"iPhone10,6": "iPhone X",
"iPhone11,2": "iPhone Xs",
"iPhone11,6": "iPhone Xs Max",
"iPhone11,8": "iPhone XR",
"iPhone12,1": "iPhone 11",
"iPhone12,3": "iPhone 11 Pro",
"iPhone12,5": "iPhone 11 Pro Max",
"iPhone12,8": "iPhone SE",
"iPhone13,1": "iPhone 12 mini",
"iPhone13,2": "iPhone 12",
"iPhone13,3": "iPhone 12 Pro",
"iPhone13,4": "iPhone 12 Pro Max",
"iPhone14,4": "iPhone 13 mini",
"iPhone14,5": "iPhone 13",
"iPhone14,2": "iPhone 13 Pro",
"iPhone14,3": "iPhone 13 Pro Max",
"iPhone14,6": "iPhone SE",
"iPhone14,7": "iPhone 14",
"iPhone14,8": "iPhone 14 Plus",
"iPhone15,2": "iPhone 14 Pro",
"iPhone15,3": "iPhone 14 Pro Max",
"iPhone15,4": "iPhone 15",
"iPhone15,5": "iPhone 15 Plus",
"iPhone16,1": "iPhone 15 Pro",
"iPhone16,2": "iPhone 15 Pro Max",
"iPod1,1": "iPod touch Original",
"iPod2,1": "iPod touch 2nd",
"iPod3,1": "iPod touch 3rd Gen",
"iPod4,1": "iPod touch 4th",
"iPod5,1": "iPod touch 5th",
"iPod7,1": "iPod touch 6th Gen",
"iPod9,1": "iPod touch 7th Gen"
}

View File

@@ -1,201 +0,0 @@
{
"PowerMac4,4": "eMac",
"PowerMac6,4": "eMac",
"PowerBook2,1": "iBook",
"PowerBook2,2": "iBook",
"PowerBook4,1": "iBook",
"PowerBook4,2": "iBook",
"PowerBook4,3": "iBook",
"PowerBook6,3": "iBook",
"PowerBook6,5": "iBook",
"PowerBook6,7": "iBook",
"iMac,1": "iMac",
"PowerMac2,1": "iMac",
"PowerMac2,2": "iMac",
"PowerMac4,1": "iMac",
"PowerMac4,2": "iMac",
"PowerMac4,5": "iMac",
"PowerMac6,1": "iMac",
"PowerMac6,3*": "iMac",
"PowerMac6,3": "iMac",
"PowerMac8,1": "iMac",
"PowerMac8,2": "iMac",
"PowerMac12,1": "iMac",
"iMac4,1": "iMac",
"iMac4,2": "iMac",
"iMac5,2": "iMac",
"iMac5,1": "iMac",
"iMac6,1": "iMac",
"iMac7,1": "iMac",
"iMac8,1": "iMac",
"iMac9,1": "iMac",
"iMac10,1": "iMac",
"iMac11,1": "iMac",
"iMac11,2": "iMac",
"iMac11,3": "iMac",
"iMac12,1": "iMac",
"iMac12,2": "iMac",
"iMac13,1": "iMac",
"iMac13,2": "iMac",
"iMac14,1": "iMac",
"iMac14,3": "iMac",
"iMac14,2": "iMac",
"iMac14,4": "iMac",
"iMac15,1": "iMac",
"iMac16,1": "iMac",
"iMac16,2": "iMac",
"iMac17,1": "iMac",
"iMac18,1": "iMac",
"iMac18,2": "iMac",
"iMac18,3": "iMac",
"iMac19,2": "iMac",
"iMac19,1": "iMac",
"iMac20,1": "iMac",
"iMac20,2": "iMac",
"iMac21,2": "iMac",
"iMac21,1": "iMac",
"iMacPro1,1": "iMac Pro",
"PowerMac10,1": "Mac mini",
"PowerMac10,2": "Mac mini",
"Macmini1,1": "Mac mini",
"Macmini2,1": "Mac mini",
"Macmini3,1": "Mac mini",
"Macmini4,1": "Mac mini",
"Macmini5,1": "Mac mini",
"Macmini5,2": "Mac mini",
"Macmini5,3": "Mac mini",
"Macmini6,1": "Mac mini",
"Macmini6,2": "Mac mini",
"Macmini7,1": "Mac mini",
"Macmini8,1": "Mac mini",
"ADP3,2": "Mac mini",
"Macmini9,1": "Mac mini",
"Mac14,3": "Mac mini",
"Mac14,12": "Mac mini",
"MacPro1,1*": "Mac Pro",
"MacPro2,1": "Mac Pro",
"MacPro3,1": "Mac Pro",
"MacPro4,1": "Mac Pro",
"MacPro5,1": "Mac Pro",
"MacPro6,1": "Mac Pro",
"MacPro7,1": "Mac Pro",
"N/A*": "Power Macintosh",
"PowerMac1,1": "Power Macintosh",
"PowerMac3,1": "Power Macintosh",
"PowerMac3,3": "Power Macintosh",
"PowerMac3,4": "Power Macintosh",
"PowerMac3,5": "Power Macintosh",
"PowerMac3,6": "Power Macintosh",
"Mac13,1": "Mac Studio",
"Mac13,2": "Mac Studio",
"MacBook1,1": "MacBook",
"MacBook2,1": "MacBook",
"MacBook3,1": "MacBook",
"MacBook4,1": "MacBook",
"MacBook5,1": "MacBook",
"MacBook5,2": "MacBook",
"MacBook6,1": "MacBook",
"MacBook7,1": "MacBook",
"MacBook8,1": "MacBook",
"MacBook9,1": "MacBook",
"MacBook10,1": "MacBook",
"MacBookAir1,1": "MacBook Air",
"MacBookAir2,1": "MacBook Air",
"MacBookAir3,1": "MacBook Air",
"MacBookAir3,2": "MacBook Air",
"MacBookAir4,1": "MacBook Air",
"MacBookAir4,2": "MacBook Air",
"MacBookAir5,1": "MacBook Air",
"MacBookAir5,2": "MacBook Air",
"MacBookAir6,1": "MacBook Air",
"MacBookAir6,2": "MacBook Air",
"MacBookAir7,1": "MacBook Air",
"MacBookAir7,2": "MacBook Air",
"MacBookAir8,1": "MacBook Air",
"MacBookAir8,2": "MacBook Air",
"MacBookAir9,1": "MacBook Air",
"MacBookAir10,1": "MacBook Air",
"Mac14,2": "MacBook Air",
"MacBookPro1,1": "MacBook Pro",
"MacBookPro1,2": "MacBook Pro",
"MacBookPro2,2": "MacBook Pro",
"MacBookPro2,1": "MacBook Pro",
"MacBookPro3,1": "MacBook Pro",
"MacBookPro4,1": "MacBook Pro",
"MacBookPro5,1": "MacBook Pro",
"MacBookPro5,2": "MacBook Pro",
"MacBookPro5,5": "MacBook Pro",
"MacBookPro5,4": "MacBook Pro",
"MacBookPro5,3": "MacBook Pro",
"MacBookPro7,1": "MacBook Pro",
"MacBookPro6,2": "MacBook Pro",
"MacBookPro6,1": "MacBook Pro",
"MacBookPro8,1": "MacBook Pro",
"MacBookPro8,2": "MacBook Pro",
"MacBookPro8,3": "MacBook Pro",
"MacBookPro9,2": "MacBook Pro",
"MacBookPro9,1": "MacBook Pro",
"MacBookPro10,1": "MacBook Pro",
"MacBookPro10,2": "MacBook Pro",
"MacBookPro11,1": "MacBook Pro",
"MacBookPro11,2": "MacBook Pro",
"MacBookPro11,3": "MacBook Pro",
"MacBookPro12,1": "MacBook Pro",
"MacBookPro11,4": "MacBook Pro",
"MacBookPro11,5": "MacBook Pro",
"MacBookPro13,1": "MacBook Pro",
"MacBookPro13,2": "MacBook Pro",
"MacBookPro13,3": "MacBook Pro",
"MacBookPro14,1": "MacBook Pro",
"MacBookPro14,2": "MacBook Pro",
"MacBookPro14,3": "MacBook Pro",
"MacBookPro15,2": "MacBook Pro",
"MacBookPro15,1": "MacBook Pro",
"MacBookPro15,3": "MacBook Pro",
"MacBookPro15,4": "MacBook Pro",
"MacBookPro16,1": "MacBook Pro",
"MacBookPro16,3": "MacBook Pro",
"MacBookPro16,2": "MacBook Pro",
"MacBookPro16,4": "MacBook Pro",
"MacBookPro17,1": "MacBook Pro",
"MacBookPro18,3": "MacBook Pro",
"MacBookPro18,4": "MacBook Pro",
"MacBookPro18,1": "MacBook Pro",
"MacBookPro18,2": "MacBook Pro",
"Mac14,7": "MacBook Pro",
"Mac14,9": "MacBook Pro",
"Mac14,5": "MacBook Pro",
"Mac14,10": "MacBook Pro",
"Mac14,6": "MacBook Pro",
"PowerMac1,2": "Power Macintosh",
"PowerMac5,1": "Power Macintosh",
"PowerMac7,2": "Power Macintosh",
"PowerMac7,3": "Power Macintosh",
"PowerMac9,1": "Power Macintosh",
"PowerMac11,2": "Power Macintosh",
"PowerBook1,1": "PowerBook",
"PowerBook3,1": "PowerBook",
"PowerBook3,2": "PowerBook",
"PowerBook3,3": "PowerBook",
"PowerBook3,4": "PowerBook",
"PowerBook3,5": "PowerBook",
"PowerBook6,1": "PowerBook",
"PowerBook5,1": "PowerBook",
"PowerBook6,2": "PowerBook",
"PowerBook5,2": "PowerBook",
"PowerBook5,3": "PowerBook",
"PowerBook6,4": "PowerBook",
"PowerBook5,4": "PowerBook",
"PowerBook5,5": "PowerBook",
"PowerBook6,8": "PowerBook",
"PowerBook5,6": "PowerBook",
"PowerBook5,7": "PowerBook",
"PowerBook5,8": "PowerBook",
"PowerBook5,9": "PowerBook",
"RackMac1,1": "Xserve",
"RackMac1,2": "Xserve",
"RackMac3,1": "Xserve",
"Xserve1,1": "Xserve",
"Xserve2,1": "Xserve",
"Xserve3,1": "Xserve"
}

View File

@@ -16,24 +16,6 @@ if (!dev) {
}
export const names = JSON.parse(readFileSync(file, "utf-8"));
// Load iOS and Mac model mappings
let iosModelsFile: string;
let macModelsFile: string;
if (!dev) {
iosModelsFile = join(__DIRNAME, "ios_models.json");
macModelsFile = join(__DIRNAME, "mac_models.json");
} else {
iosModelsFile = join("server/db/ios_models.json");
macModelsFile = join("server/db/mac_models.json");
}
const iosModels: Record<string, string> = JSON.parse(
readFileSync(iosModelsFile, "utf-8")
);
const macModels: Record<string, string> = JSON.parse(
readFileSync(macModelsFile, "utf-8")
);
export async function getUniqueClientName(orgId: string): Promise<string> {
let loops = 0;
while (true) {
@@ -177,29 +159,3 @@ export function generateName(): string {
// clean out any non-alphanumeric characters except for dashes
return name.replace(/[^a-z0-9-]/g, "");
}
export function getMacDeviceName(macIdentifier?: string | null): string | null {
if (macIdentifier && macModels[macIdentifier]) {
return macModels[macIdentifier];
}
return null;
}
export function getIosDeviceName(iosIdentifier?: string | null): string | null {
if (iosIdentifier && iosModels[iosIdentifier]) {
return iosModels[iosIdentifier];
}
return null;
}
export function getUserDeviceName(
model: string | null,
fallBack: string | null
): string {
return (
getMacDeviceName(model) ||
getIosDeviceName(model) ||
fallBack ||
"Unknown Device"
);
}

View File

@@ -10,15 +10,7 @@ import {
index
} from "drizzle-orm/pg-core";
import { InferSelectModel } from "drizzle-orm";
import {
domains,
orgs,
targets,
users,
exitNodes,
sessions,
clients
} from "./schema";
import { domains, orgs, targets, users, exitNodes, sessions } from "./schema";
export const certificates = pgTable("certificates", {
certId: serial("certId").primaryKey(),
@@ -297,33 +289,6 @@ export const accessAuditLog = pgTable(
]
);
export const approvals = pgTable("approvals", {
approvalId: serial("approvalId").primaryKey(),
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull(),
clientId: integer("clientId").references(() => clients.clientId, {
onDelete: "cascade"
}), // clients reference user devices (in this case)
userId: varchar("userId")
.references(() => users.userId, {
// optionally tied to a user and in this case delete when the user deletes
onDelete: "cascade"
})
.notNull(),
decision: varchar("decision")
.$type<"approved" | "denied" | "pending">()
.default("pending")
.notNull(),
type: varchar("type")
.$type<"user_device" /*| 'proxy' // for later */>()
.notNull()
});
export type Approval = InferSelectModel<typeof approvals>;
export type Limit = InferSelectModel<typeof limits>;
export type Account = InferSelectModel<typeof account>;
export type Certificate = InferSelectModel<typeof certificates>;

View File

@@ -365,8 +365,7 @@ export const roles = pgTable("roles", {
.notNull(),
isAdmin: boolean("isAdmin"),
name: varchar("name").notNull(),
description: varchar("description"),
requireDeviceApproval: boolean("requireDeviceApproval").default(false)
description: varchar("description")
});
export const roleActions = pgTable("roleActions", {
@@ -592,8 +591,7 @@ export const idp = pgTable("idp", {
type: varchar("type").notNull(),
defaultRoleMapping: varchar("defaultRoleMapping"),
defaultOrgMapping: varchar("defaultOrgMapping"),
autoProvision: boolean("autoProvision").notNull().default(false),
tags: text("tags")
autoProvision: boolean("autoProvision").notNull().default(false)
});
export const idpOidcConfig = pgTable("idpOidcConfig", {
@@ -690,12 +688,7 @@ export const clients = pgTable("clients", {
online: boolean("online").notNull().default(false),
// endpoint: varchar("endpoint"),
lastHolePunch: integer("lastHolePunch"),
maxConnections: integer("maxConnections"),
archived: boolean("archived").notNull().default(false),
blocked: boolean("blocked").notNull().default(false),
approvalState: varchar("approvalState").$type<
"pending" | "approved" | "denied"
>()
maxConnections: integer("maxConnections")
});
export const clientSitesAssociationsCache = pgTable(
@@ -719,49 +712,6 @@ export const clientSiteResourcesAssociationsCache = pgTable(
}
);
export const clientPostureSnapshots = pgTable("clientPostureSnapshots", {
snapshotId: serial("snapshotId").primaryKey(),
clientId: integer("clientId").references(() => clients.clientId, {
onDelete: "cascade"
}),
// Platform-agnostic checks
biometricsEnabled: boolean("biometricsEnabled").notNull().default(false),
diskEncrypted: boolean("diskEncrypted").notNull().default(false),
firewallEnabled: boolean("firewallEnabled").notNull().default(false),
autoUpdatesEnabled: boolean("autoUpdatesEnabled").notNull().default(false),
tpmAvailable: boolean("tpmAvailable").notNull().default(false),
// Windows-specific posture check information
windowsDefenderEnabled: boolean("windowsDefenderEnabled")
.notNull()
.default(false),
// macOS-specific posture check information
macosSipEnabled: boolean("macosSipEnabled").notNull().default(false),
macosGatekeeperEnabled: boolean("macosGatekeeperEnabled")
.notNull()
.default(false),
macosFirewallStealthMode: boolean("macosFirewallStealthMode")
.notNull()
.default(false),
// Linux-specific posture check information
linuxAppArmorEnabled: boolean("linuxAppArmorEnabled")
.notNull()
.default(false),
linuxSELinuxEnabled: boolean("linuxSELinuxEnabled")
.notNull()
.default(false),
collectedAt: integer("collectedAt").notNull()
});
export const olms = pgTable("olms", {
olmId: varchar("id").primaryKey(),
secretHash: varchar("secretHash").notNull(),
@@ -776,29 +726,7 @@ export const olms = pgTable("olms", {
userId: text("userId").references(() => users.userId, {
// optionally tied to a user and in this case delete when the user deletes
onDelete: "cascade"
}),
archived: boolean("archived").notNull().default(false)
});
export const fingerprints = pgTable("fingerprints", {
fingerprintId: serial("id").primaryKey(),
olmId: text("olmId")
.references(() => olms.olmId, { onDelete: "cascade" })
.notNull(),
firstSeen: integer("firstSeen").notNull(),
lastSeen: integer("lastSeen").notNull(),
username: text("username"),
hostname: text("hostname"),
platform: text("platform"), // macos | windows | linux | ios | android | unknown
osVersion: text("osVersion"),
kernelVersion: text("kernelVersion"),
arch: text("arch"),
deviceModel: text("deviceModel"),
serialNumber: text("serialNumber"),
platformFingerprint: varchar("platformFingerprint")
})
});
export const olmSessions = pgTable("clientSession", {

View File

@@ -1,4 +1,4 @@
import { db, loginPage, LoginPage, loginPageOrg, Org, orgs, roles } from "@server/db";
import { db, loginPage, LoginPage, loginPageOrg, Org, orgs } from "@server/db";
import {
Resource,
ResourcePassword,
@@ -108,17 +108,9 @@ export async function getUserSessionWithUser(
*/
export async function getUserOrgRole(userId: string, orgId: string) {
const userOrgRole = await db
.select({
userId: userOrgs.userId,
orgId: userOrgs.orgId,
roleId: userOrgs.roleId,
isOwner: userOrgs.isOwner,
autoProvisioned: userOrgs.autoProvisioned,
roleName: roles.name
})
.select()
.from(userOrgs)
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
.limit(1);
return userOrgRole.length > 0 ? userOrgRole[0] : null;

View File

@@ -6,7 +6,7 @@ import {
sqliteTable,
text
} from "drizzle-orm/sqlite-core";
import { clients, domains, exitNodes, orgs, sessions, users } from "./schema";
import { domains, exitNodes, orgs, sessions, users } from "./schema";
export const certificates = sqliteTable("certificates", {
certId: integer("certId").primaryKey({ autoIncrement: true }),
@@ -289,31 +289,6 @@ export const accessAuditLog = sqliteTable(
]
);
export const approvals = sqliteTable("approvals", {
approvalId: integer("approvalId").primaryKey({ autoIncrement: true }),
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
orgId: text("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull(),
clientId: integer("clientId").references(() => clients.clientId, {
onDelete: "cascade"
}), // olms reference user devices clients
userId: text("userId").references(() => users.userId, {
// optionally tied to a user and in this case delete when the user deletes
onDelete: "cascade"
}),
decision: text("decision")
.$type<"approved" | "denied" | "pending">()
.default("pending")
.notNull(),
type: text("type")
.$type<"user_device" /*| 'proxy' // for later */>()
.notNull()
});
export type Approval = InferSelectModel<typeof approvals>;
export type Limit = InferSelectModel<typeof limits>;
export type Account = InferSelectModel<typeof account>;
export type Certificate = InferSelectModel<typeof certificates>;

View File

@@ -255,9 +255,7 @@ export const siteResources = sqliteTable("siteResources", {
aliasAddress: text("aliasAddress"),
tcpPortRangeString: text("tcpPortRangeString").notNull().default("*"),
udpPortRangeString: text("udpPortRangeString").notNull().default("*"),
disableIcmp: integer("disableIcmp", { mode: "boolean" })
.notNull()
.default(false)
disableIcmp: integer("disableIcmp", { mode: "boolean" }).notNull().default(false)
});
export const clientSiteResources = sqliteTable("clientSiteResources", {
@@ -385,12 +383,7 @@ export const clients = sqliteTable("clients", {
type: text("type").notNull(), // "olm"
online: integer("online", { mode: "boolean" }).notNull().default(false),
// endpoint: text("endpoint"),
lastHolePunch: integer("lastHolePunch"),
archived: integer("archived", { mode: "boolean" }).notNull().default(false),
blocked: integer("blocked", { mode: "boolean" }).notNull().default(false),
approvalState: text("approvalState").$type<
"pending" | "approved" | "denied"
>()
lastHolePunch: integer("lastHolePunch")
});
export const clientSitesAssociationsCache = sqliteTable(
@@ -416,69 +409,6 @@ export const clientSiteResourcesAssociationsCache = sqliteTable(
}
);
export const clientPostureSnapshots = sqliteTable("clientPostureSnapshots", {
snapshotId: integer("snapshotId").primaryKey({ autoIncrement: true }),
clientId: integer("clientId").references(() => clients.clientId, {
onDelete: "cascade"
}),
// Platform-agnostic checks
biometricsEnabled: integer("biometricsEnabled", { mode: "boolean" })
.notNull()
.default(false),
diskEncrypted: integer("diskEncrypted", { mode: "boolean" })
.notNull()
.default(false),
firewallEnabled: integer("firewallEnabled", { mode: "boolean" })
.notNull()
.default(false),
autoUpdatesEnabled: integer("autoUpdatesEnabled", { mode: "boolean" })
.notNull()
.default(false),
tpmAvailable: integer("tpmAvailable", { mode: "boolean" })
.notNull()
.default(false),
// Windows-specific posture check information
windowsDefenderEnabled: integer("windowsDefenderEnabled", {
mode: "boolean"
})
.notNull()
.default(false),
// macOS-specific posture check information
macosSipEnabled: integer("macosSipEnabled", { mode: "boolean" })
.notNull()
.default(false),
macosGatekeeperEnabled: integer("macosGatekeeperEnabled", {
mode: "boolean"
})
.notNull()
.default(false),
macosFirewallStealthMode: integer("macosFirewallStealthMode", {
mode: "boolean"
})
.notNull()
.default(false),
// Linux-specific posture check information
linuxAppArmorEnabled: integer("linuxAppArmorEnabled", { mode: "boolean" })
.notNull()
.default(false),
linuxSELinuxEnabled: integer("linuxSELinuxEnabled", {
mode: "boolean"
})
.notNull()
.default(false),
collectedAt: integer("collectedAt").notNull()
});
export const olms = sqliteTable("olms", {
olmId: text("id").primaryKey(),
secretHash: text("secretHash").notNull(),
@@ -493,29 +423,7 @@ export const olms = sqliteTable("olms", {
userId: text("userId").references(() => users.userId, {
// optionally tied to a user and in this case delete when the user deletes
onDelete: "cascade"
}),
archived: integer("archived", { mode: "boolean" }).notNull().default(false)
});
export const fingerprints = sqliteTable("fingerprints", {
fingerprintId: integer("id").primaryKey({ autoIncrement: true }),
olmId: text("olmId")
.references(() => olms.olmId, { onDelete: "cascade" })
.notNull(),
firstSeen: integer("firstSeen").notNull(),
lastSeen: integer("lastSeen").notNull(),
username: text("username"),
hostname: text("hostname"),
platform: text("platform"), // macos | windows | linux | ios | android | unknown
osVersion: text("osVersion"),
kernelVersion: text("kernelVersion"),
arch: text("arch"),
deviceModel: text("deviceModel"),
serialNumber: text("serialNumber"),
platformFingerprint: text("platformFingerprint")
})
});
export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", {
@@ -607,10 +515,7 @@ export const roles = sqliteTable("roles", {
.notNull(),
isAdmin: integer("isAdmin", { mode: "boolean" }),
name: text("name").notNull(),
description: text("description"),
requireDeviceApproval: integer("requireDeviceApproval", {
mode: "boolean"
}).default(false)
description: text("description")
});
export const roleActions = sqliteTable("roleActions", {
@@ -869,8 +774,7 @@ export const idp = sqliteTable("idp", {
mode: "boolean"
})
.notNull()
.default(false),
tags: text("tags")
.default(false)
});
// Identity Provider OAuth Configuration

View File

@@ -290,8 +290,8 @@ export const ClientResourceSchema = z
alias: z
.string()
.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])?$/,
"Alias must be a fully qualified domain name with optional wildcards (e.g., example.com, *.example.com, host-0?.example.internal)"
/^(?:[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])?$/,
"Alias must be a fully qualified domain name (e.g., example.com)"
)
.optional(),
roles: z

View File

@@ -1,24 +1,21 @@
import { listExitNodes } from "#dynamic/lib/exitNodes";
import { build } from "@server/build";
import {
approvals,
clients,
db,
olms,
orgs,
roleClients,
roles,
Transaction,
userClients,
userOrgs
userOrgs,
Transaction
} from "@server/db";
import { getUniqueClientName } from "@server/db/names";
import { eq, and, notInArray } from "drizzle-orm";
import { listExitNodes } from "#dynamic/lib/exitNodes";
import { getNextAvailableClientSubnet } from "@server/lib/ip";
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
import logger from "@server/logger";
import { sendTerminateClient } from "@server/routers/client/terminate";
import { and, eq, notInArray, type InferInsertModel } from "drizzle-orm";
import { rebuildClientAssociationsFromClient } from "./rebuildClientAssociations";
import { sendTerminateClient } from "@server/routers/client/terminate";
import { getUniqueClientName } from "@server/db/names";
export async function calculateUserClientsForOrgs(
userId: string,
@@ -41,15 +38,13 @@ export async function calculateUserClientsForOrgs(
const allUserOrgs = await transaction
.select()
.from(userOrgs)
.innerJoin(roles, eq(roles.roleId, userOrgs.roleId))
.where(eq(userOrgs.userId, userId));
const userOrgIds = allUserOrgs.map(({ userOrgs: uo }) => uo.orgId);
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 userRoleOrg of allUserOrgs) {
const { userOrgs: userOrg, roles: role } = userRoleOrg;
for (const userOrg of allUserOrgs) {
const orgId = userOrg.orgId;
const [org] = await transaction
@@ -187,46 +182,21 @@ export async function calculateUserClientsForOrgs(
const niceId = await getUniqueClientName(orgId);
const isOrgLicensed = await isLicensedOrSubscribed(
userOrg.orgId
);
const requireApproval =
build !== "oss" &&
isOrgLicensed &&
role.requireDeviceApproval;
const newClientData: InferInsertModel<typeof clients> = {
userId,
orgId: userOrg.orgId,
exitNodeId: randomExitNode.exitNodeId,
name: olm.name || "User Client",
subnet: updatedSubnet,
olmId: olm.olmId,
type: "olm",
niceId,
approvalState: requireApproval ? "pending" : null
};
// Create the client
const [newClient] = await transaction
.insert(clients)
.values(newClientData)
.values({
userId,
orgId: userOrg.orgId,
exitNodeId: randomExitNode.exitNodeId,
name: olm.name || "User Client",
subnet: updatedSubnet,
olmId: olm.olmId,
type: "olm",
niceId
})
.returning();
// create approval request
if (requireApproval) {
await transaction
.insert(approvals)
.values({
timestamp: Math.floor(new Date().getTime() / 1000),
orgId: userOrg.orgId,
clientId: newClient.clientId,
userId,
type: "user_device"
})
.returning();
}
await rebuildClientAssociationsFromClient(
newClient,
transaction

View File

@@ -26,7 +26,6 @@ export function initLogCleanupInterval() {
)
);
// TODO: handle when there are multiple nodes doing this clearing using redis
for (const org of orgsToClean) {
const {
orgId,

View File

@@ -13,4 +13,3 @@ export * from "./verifyApiKeyIsRoot";
export * from "./verifyApiKeyApiKeyAccess";
export * from "./verifyApiKeyClientAccess";
export * from "./verifyApiKeySiteResourceAccess";
export * from "./verifyApiKeyIdpAccess";

View File

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

View File

@@ -139,10 +139,6 @@ export class PrivateConfig {
process.env.USE_PANGOLIN_DNS =
this.rawPrivateConfig.flags.use_pangolin_dns.toString();
}
if (this.rawPrivateConfig.flags.use_org_only_idp) {
process.env.USE_ORG_ONLY_IDP =
this.rawPrivateConfig.flags.use_org_only_idp.toString();
}
}
public getRawPrivateConfig() {

View File

@@ -50,14 +50,10 @@ export async function sendToExitNode(
);
}
return sendToClient(
remoteExitNode.remoteExitNodeId,
{
type: request.remoteType,
data: request.data
},
{ incrementConfigVersion: true }
);
return sendToClient(remoteExitNode.remoteExitNodeId, {
type: request.remoteType,
data: request.data
});
} else {
let hostname = exitNode.reachableAt;

View File

@@ -288,7 +288,7 @@ export function selectBestExitNode(
const validNodes = pingResults.filter((n) => !n.error && n.weight > 0);
if (validNodes.length === 0) {
logger.debug("No valid exit nodes available");
logger.error("No valid exit nodes available");
return null;
}

View File

@@ -24,9 +24,7 @@ export class LockManager {
*/
async acquireLock(
lockKey: string,
ttlMs: number = 30000,
maxRetries: number = 3,
retryDelayMs: number = 100
ttlMs: number = 30000
): Promise<boolean> {
if (!redis || !redis.status || redis.status !== "ready") {
return true;
@@ -37,67 +35,49 @@ export class LockManager {
}:${Date.now()}`;
const redisKey = `lock:${lockKey}`;
for (let attempt = 0; attempt < maxRetries; attempt++) {
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"
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
}`
);
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;
}
// If this isn't our last attempt, wait before retrying with exponential backoff
if (attempt < maxRetries - 1) {
const delay = retryDelayMs * Math.pow(2, attempt);
logger.debug(
`Lock ${lockKey} not available, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`
);
await new Promise((resolve) => setTimeout(resolve, delay));
}
} catch (error) {
logger.error(`Failed to acquire lock ${lockKey} (attempt ${attempt + 1}/${maxRetries}):`, error);
// On error, still retry if we have attempts left
if (attempt < maxRetries - 1) {
const delay = retryDelayMs * Math.pow(2, attempt);
await new Promise((resolve) => setTimeout(resolve, delay));
}
return true;
}
}
logger.debug(
`Failed to acquire lock ${lockKey} after ${maxRetries} attempts`
);
return false;
// 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;
}
}
/**

View File

@@ -83,8 +83,7 @@ export const privateConfigSchema = z.object({
flags: z
.object({
enable_redis: z.boolean().optional().default(false),
use_pangolin_dns: z.boolean().optional().default(false),
use_org_only_idp: z.boolean().optional().default(false)
use_pangolin_dns: z.boolean().optional().default(false)
})
.optional()
.prefault({}),

View File

@@ -573,20 +573,6 @@ class RedisManager {
}
}
public async incr(key: string): Promise<number> {
if (!this.isRedisEnabled() || !this.writeClient) return 0;
try {
return await this.executeWithRetry(
() => this.writeClient!.incr(key),
"Redis INCR"
);
} catch (error) {
logger.error("Redis INCR error:", error);
return 0;
}
}
public async sadd(key: string, member: string): Promise<boolean> {
if (!this.isRedisEnabled() || !this.writeClient) return false;

View File

@@ -456,11 +456,11 @@ export async function getTraefikConfig(
// );
} else if (resource.maintenanceModeType === "automatic") {
showMaintenancePage = !hasHealthyServers;
// if (showMaintenancePage) {
// logger.warn(
// `Resource ${resource.name} (${fullDomain}) has no healthy servers - showing maintenance page (AUTOMATIC mode)`
// );
// }
if (showMaintenancePage) {
logger.warn(
`Resource ${resource.name} (${fullDomain}) has no healthy servers - showing maintenance page (AUTOMATIC mode)`
);
}
}
}

View File

@@ -27,18 +27,7 @@ export async function verifyValidSubscription(
return next();
}
const orgId = req.params.orgId || req.body.orgId || req.query.orgId || req.userOrgId;
if (!orgId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Organization ID is required to verify subscription"
)
);
}
const tier = await getOrgTierData(orgId);
const tier = await getOrgTierData(req.params.orgId);
if (!tier.active) {
return next(

View File

@@ -1,15 +0,0 @@
/*
* 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.
*/
export * from "./listApprovals";
export * from "./processPendingApproval";

View File

@@ -1,188 +0,0 @@
/*
* 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 logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import type { Request, Response, NextFunction } from "express";
import { build } from "@server/build";
import { getOrgTierData } from "@server/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { approvals, clients, db, users, type Approval } from "@server/db";
import { eq, isNull, sql, not, and, desc } from "drizzle-orm";
import response from "@server/lib/response";
const paramsSchema = z.strictObject({
orgId: z.string()
});
const querySchema = z.strictObject({
limit: z
.string()
.optional()
.default("1000")
.transform(Number)
.pipe(z.int().nonnegative()),
offset: z
.string()
.optional()
.default("0")
.transform(Number)
.pipe(z.int().nonnegative()),
approvalState: z
.enum(["pending", "approved", "denied", "all"])
.optional()
.default("all")
.catch("all")
});
async function queryApprovals(
orgId: string,
limit: number,
offset: number,
approvalState: z.infer<typeof querySchema>["approvalState"]
) {
let state: Array<Approval["decision"]> = [];
switch (approvalState) {
case "pending":
state = ["pending"];
break;
case "approved":
state = ["approved"];
break;
case "denied":
state = ["denied"];
break;
default:
state = ["approved", "denied", "pending"];
}
const res = await db
.select({
approvalId: approvals.approvalId,
orgId: approvals.orgId,
clientId: approvals.clientId,
decision: approvals.decision,
type: approvals.type,
user: {
name: users.name,
userId: users.userId,
username: users.username
}
})
.from(approvals)
.innerJoin(users, and(eq(approvals.userId, users.userId)))
.leftJoin(
clients,
and(
eq(approvals.clientId, clients.clientId),
not(isNull(clients.userId)) // only user devices
)
)
.where(
and(
eq(approvals.orgId, orgId),
sql`${approvals.decision} in ${state}`
)
)
.orderBy(
sql`CASE ${approvals.decision} WHEN 'pending' THEN 0 ELSE 1 END`,
desc(approvals.timestamp)
)
.limit(limit)
.offset(offset);
return res;
}
export type ListApprovalsResponse = {
approvals: NonNullable<Awaited<ReturnType<typeof queryApprovals>>>;
pagination: { total: number; limit: number; offset: number };
};
export async function listApprovals(
req: Request,
res: Response,
next: NextFunction
) {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const parsedQuery = querySchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error).toString()
)
);
}
const { limit, offset, approvalState } = parsedQuery.data;
const { orgId } = parsedParams.data;
if (build === "saas") {
const { tier } = await getOrgTierData(orgId);
const subscribed = tier === TierId.STANDARD;
if (!subscribed) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"This organization's current plan does not support this feature."
)
);
}
}
const approvalsList = await queryApprovals(
orgId.toString(),
limit,
offset,
approvalState
);
const [{ count }] = await db
.select({ count: sql<number>`count(*)` })
.from(approvals);
return response<ListApprovalsResponse>(res, {
data: {
approvals: approvalsList,
pagination: {
total: count,
limit,
offset
}
},
success: true,
error: false,
message: "Approvals retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -1,142 +0,0 @@
/*
* 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 logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import { build } from "@server/build";
import { approvals, clients, db, orgs, type Approval } from "@server/db";
import { getOrgTierData } from "@server/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import response from "@server/lib/response";
import { and, eq, type InferInsertModel } from "drizzle-orm";
import type { NextFunction, Request, Response } from "express";
const paramsSchema = z.strictObject({
orgId: z.string(),
approvalId: z.string().transform(Number).pipe(z.int().positive())
});
const bodySchema = z.strictObject({
decision: z.enum(["approved", "denied"])
});
export type ProcessApprovalResponse = Approval;
export async function processPendingApproval(
req: Request,
res: Response,
next: NextFunction
) {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { orgId, approvalId } = parsedParams.data;
if (build === "saas") {
const { tier } = await getOrgTierData(orgId);
const subscribed = tier === TierId.STANDARD;
if (!subscribed) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"This organization's current plan does not support this feature."
)
);
}
}
const updateData = parsedBody.data;
const approval = await db
.select()
.from(approvals)
.where(
and(
eq(approvals.approvalId, approvalId),
eq(approvals.decision, "pending")
)
)
.innerJoin(orgs, eq(approvals.orgId, approvals.orgId))
.limit(1);
if (approval.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Pending Approval with ID ${approvalId} not found`
)
);
}
const [updatedApproval] = await db
.update(approvals)
.set(updateData)
.where(eq(approvals.approvalId, approvalId))
.returning();
// Update user device approval state too
if (
updatedApproval.type === "user_device" &&
updatedApproval.clientId
) {
const updateDataBody: Partial<InferInsertModel<typeof clients>> = {
approvalState: updateData.decision
};
if (updateData.decision === "denied") {
updateDataBody.blocked = true;
}
await db
.update(clients)
.set(updateDataBody)
.where(eq(clients.clientId, updatedApproval.clientId));
}
return response(res, {
data: updatedApproval,
success: true,
error: false,
message: "Approval updated successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -24,7 +24,6 @@ import * as generateLicense from "./generatedLicense";
import * as logs from "#private/routers/auditLogs";
import * as misc from "#private/routers/misc";
import * as reKey from "#private/routers/re-key";
import * as approval from "#private/routers/approvals";
import {
verifyOrgAccess,
@@ -312,24 +311,6 @@ authenticated.get(
loginPage.getLoginPage
);
authenticated.get(
"/org/:orgId/approvals",
verifyValidLicense,
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.listApprovals),
logActionAudit(ActionsEnum.listApprovals),
approval.listApprovals
);
authenticated.put(
"/org/:orgId/approvals/:approvalId",
verifyValidLicense,
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.updateApprovals),
logActionAudit(ActionsEnum.updateApprovals),
approval.processPendingApproval
);
authenticated.get(
"/org/:orgId/login-page-branding",
verifyValidLicense,
@@ -455,18 +436,18 @@ authenticated.get(
authenticated.post(
"/re-key/:clientId/regenerate-client-secret",
verifyClientAccess, // this is first to set the org id
verifyValidLicense,
verifyValidSubscription,
verifyClientAccess,
verifyUserHasAction(ActionsEnum.reGenerateSecret),
reKey.reGenerateClientSecret
);
authenticated.post(
"/re-key/:siteId/regenerate-site-secret",
verifySiteAccess, // this is first to set the org id
verifyValidLicense,
verifyValidSubscription,
verifySiteAccess,
verifyUserHasAction(ActionsEnum.reGenerateSecret),
reKey.reGenerateSiteSecret
);

View File

@@ -18,8 +18,7 @@ import * as logs from "#private/routers/auditLogs";
import {
verifyApiKeyHasAction,
verifyApiKeyIsRoot,
verifyApiKeyOrgAccess,
verifyApiKeyIdpAccess
verifyApiKeyOrgAccess
} from "@server/middlewares";
import {
verifyValidSubscription,
@@ -32,8 +31,6 @@ import {
authenticated as a
} from "@server/routers/integration";
import { logActionAudit } from "#private/middlewares";
import config from "#private/lib/config";
import { build } from "@server/build";
export const unauthenticated = ua;
export const authenticated = a;
@@ -91,49 +88,3 @@ authenticated.get(
logActionAudit(ActionsEnum.exportLogs),
logs.exportAccessAuditLogs
);
authenticated.put(
"/org/:orgId/idp/oidc",
verifyValidLicense,
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.createIdp),
logActionAudit(ActionsEnum.createIdp),
orgIdp.createOrgOidcIdp
);
authenticated.post(
"/org/:orgId/idp/:idpId/oidc",
verifyValidLicense,
verifyApiKeyOrgAccess,
verifyApiKeyIdpAccess,
verifyApiKeyHasAction(ActionsEnum.updateIdp),
logActionAudit(ActionsEnum.updateIdp),
orgIdp.updateOrgOidcIdp
);
authenticated.delete(
"/org/:orgId/idp/:idpId",
verifyValidLicense,
verifyApiKeyOrgAccess,
verifyApiKeyIdpAccess,
verifyApiKeyHasAction(ActionsEnum.deleteIdp),
logActionAudit(ActionsEnum.deleteIdp),
orgIdp.deleteOrgIdp
);
authenticated.get(
"/org/:orgId/idp/:idpId",
verifyValidLicense,
verifyApiKeyOrgAccess,
verifyApiKeyIdpAccess,
verifyApiKeyHasAction(ActionsEnum.getIdp),
orgIdp.getOrgIdp
);
authenticated.get(
"/org/:orgId/idp",
verifyValidLicense,
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.listIdps),
orgIdp.listOrgIdps
);

View File

@@ -29,9 +29,11 @@ import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build";
const paramsSchema = z.strictObject({
orgId: z.string()
});
const paramsSchema = z
.object({
orgId: z.string()
})
.strict();
export async function getLoginPageBranding(
req: Request,

View File

@@ -28,7 +28,6 @@ import { eq, InferInsertModel } from "drizzle-orm";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build";
import config from "@server/private/lib/config";
const paramsSchema = z.strictObject({
orgId: z.string()
@@ -95,10 +94,8 @@ export async function upsertLoginPageBranding(
typeof loginPageBranding
>;
if (
build !== "saas" &&
!config.getRawPrivateConfig().flags.use_org_only_idp
) {
if (build !== "saas") {
// org branding settings are only considered in the saas build
const { orgTitle, orgSubtitle, ...rest } = updateData;
updateData = rest;
}

View File

@@ -43,27 +43,25 @@ const bodySchema = z.strictObject({
scopes: z.string().nonempty(),
autoProvision: z.boolean().optional(),
variant: z.enum(["oidc", "google", "azure"]).optional().default("oidc"),
roleMapping: z.string().optional(),
tags: z.string().optional()
roleMapping: z.string().optional()
});
registry.registerPath({
method: "put",
path: "/org/{orgId}/idp/oidc",
description: "Create an OIDC IdP for a specific organization.",
tags: [OpenAPITags.Idp, OpenAPITags.Org],
request: {
params: paramsSchema,
body: {
content: {
"application/json": {
schema: bodySchema
}
}
}
},
responses: {}
});
// registry.registerPath({
// method: "put",
// path: "/idp/oidc",
// description: "Create an OIDC IdP.",
// tags: [OpenAPITags.Idp],
// request: {
// body: {
// content: {
// "application/json": {
// schema: bodySchema
// }
// }
// }
// },
// responses: {}
// });
export async function createOrgOidcIdp(
req: Request,
@@ -105,8 +103,7 @@ export async function createOrgOidcIdp(
name,
autoProvision,
variant,
roleMapping,
tags
roleMapping
} = parsedBody.data;
if (build === "saas") {
@@ -134,8 +131,7 @@ export async function createOrgOidcIdp(
.values({
name,
autoProvision,
type: "oidc",
tags
type: "oidc"
})
.returning();

View File

@@ -32,9 +32,9 @@ const paramsSchema = z
registry.registerPath({
method: "delete",
path: "/org/{orgId}/idp/{idpId}",
description: "Delete IDP for a specific organization.",
tags: [OpenAPITags.Idp, OpenAPITags.Org],
path: "/idp/{idpId}",
description: "Delete IDP.",
tags: [OpenAPITags.Idp],
request: {
params: paramsSchema
},

View File

@@ -48,16 +48,16 @@ async function query(idpId: number, orgId: string) {
return res;
}
registry.registerPath({
method: "get",
path: "/org/:orgId/idp/:idpId",
description: "Get an IDP by its IDP ID for a specific organization.",
tags: [OpenAPITags.Idp, OpenAPITags.Org],
request: {
params: paramsSchema
},
responses: {}
});
// registry.registerPath({
// method: "get",
// path: "/idp/{idpId}",
// description: "Get an IDP by its IDP ID.",
// tags: [OpenAPITags.Idp],
// request: {
// params: paramsSchema
// },
// responses: {}
// });
export async function getOrgIdp(
req: Request,

View File

@@ -50,8 +50,7 @@ async function query(orgId: string, limit: number, offset: number) {
orgId: idpOrg.orgId,
name: idp.name,
type: idp.type,
variant: idpOidcConfig.variant,
tags: idp.tags
variant: idpOidcConfig.variant
})
.from(idpOrg)
.where(eq(idpOrg.orgId, orgId))
@@ -63,17 +62,16 @@ async function query(orgId: string, limit: number, offset: number) {
return res;
}
registry.registerPath({
method: "get",
path: "/org/{orgId}/idp",
description: "List all IDP for a specific organization.",
tags: [OpenAPITags.Idp, OpenAPITags.Org],
request: {
query: querySchema,
params: paramsSchema
},
responses: {}
});
// registry.registerPath({
// method: "get",
// path: "/idp",
// description: "List all IDP in the system.",
// tags: [OpenAPITags.Idp],
// request: {
// query: querySchema
// },
// responses: {}
// });
export async function listOrgIdps(
req: Request,

View File

@@ -46,31 +46,30 @@ const bodySchema = z.strictObject({
namePath: z.string().optional(),
scopes: z.string().optional(),
autoProvision: z.boolean().optional(),
roleMapping: z.string().optional(),
tags: z.string().optional()
roleMapping: z.string().optional()
});
export type UpdateOrgIdpResponse = {
idpId: number;
};
registry.registerPath({
method: "post",
path: "/org/{orgId}/idp/{idpId}/oidc",
description: "Update an OIDC IdP for a specific organization.",
tags: [OpenAPITags.Idp, OpenAPITags.Org],
request: {
params: paramsSchema,
body: {
content: {
"application/json": {
schema: bodySchema
}
}
}
},
responses: {}
});
// registry.registerPath({
// method: "post",
// path: "/idp/{idpId}/oidc",
// description: "Update an OIDC IdP.",
// tags: [OpenAPITags.Idp],
// request: {
// params: paramsSchema,
// body: {
// content: {
// "application/json": {
// schema: bodySchema
// }
// }
// }
// },
// responses: {}
// });
export async function updateOrgOidcIdp(
req: Request,
@@ -110,8 +109,7 @@ export async function updateOrgOidcIdp(
namePath,
name,
autoProvision,
roleMapping,
tags
roleMapping
} = parsedBody.data;
if (build === "saas") {
@@ -169,8 +167,7 @@ export async function updateOrgOidcIdp(
await db.transaction(async (trx) => {
const idpData = {
name,
autoProvision,
tags
autoProvision
};
// only update if at least one key is not undefined

View File

@@ -43,8 +43,7 @@ import {
WSMessage,
TokenPayload,
WebSocketRequest,
RedisMessage,
SendMessageOptions
RedisMessage
} from "@server/routers/ws";
import { validateSessionToken } from "@server/auth/sessions/app";
@@ -119,21 +118,12 @@ const processMessage = async (
if (response.broadcast) {
await broadcastToAllExcept(
response.message,
response.excludeSender ? clientId : undefined,
response.options
response.excludeSender ? clientId : undefined
);
} else if (response.targetClientId) {
await sendToClient(
response.targetClientId,
response.message,
response.options
);
await sendToClient(response.targetClientId, response.message);
} else {
await sendToClient(
clientId,
response.message,
response.options
);
ws.send(JSON.stringify(response.message));
}
}
} catch (error) {
@@ -182,9 +172,6 @@ const REDIS_CHANNEL = "websocket_messages";
// Client tracking map (local to this node)
const connectedClients: Map<string, AuthenticatedWebSocket[]> = new Map();
// Config version tracking map (local to this node, resets on server restart)
const clientConfigVersions: Map<string, number> = new Map();
// Recovery tracking
let isRedisRecoveryInProgress = false;
@@ -195,8 +182,6 @@ const getClientMapKey = (clientId: string) => clientId;
const getConnectionsKey = (clientId: string) => `ws:connections:${clientId}`;
const getNodeConnectionsKey = (nodeId: string, clientId: string) =>
`ws:node:${nodeId}:${clientId}`;
const getConfigVersionKey = (clientId: string) =>
`ws:configVersion:${clientId}`;
// Initialize Redis subscription for cross-node messaging
const initializeRedisSubscription = async (): Promise<void> => {
@@ -319,45 +304,6 @@ const addClient = async (
existingClients.push(ws);
connectedClients.set(mapKey, existingClients);
// Get or initialize config version
let configVersion = 0;
// Check Redis first if enabled
if (redisManager.isRedisEnabled()) {
try {
const redisVersion = await redisManager.get(getConfigVersionKey(clientId));
if (redisVersion !== null) {
configVersion = parseInt(redisVersion, 10);
// Sync to local cache
clientConfigVersions.set(clientId, configVersion);
} else if (!clientConfigVersions.has(clientId)) {
// No version in Redis or local cache, initialize to 0
await redisManager.set(getConfigVersionKey(clientId), "0");
clientConfigVersions.set(clientId, 0);
} else {
// Use local cache version and sync to Redis
configVersion = clientConfigVersions.get(clientId) || 0;
await redisManager.set(getConfigVersionKey(clientId), configVersion.toString());
}
} catch (error) {
logger.error("Failed to get/set config version in Redis:", error);
// Fall back to local cache
if (!clientConfigVersions.has(clientId)) {
clientConfigVersions.set(clientId, 0);
}
configVersion = clientConfigVersions.get(clientId) || 0;
}
} else {
// Redis not enabled, use local cache only
if (!clientConfigVersions.has(clientId)) {
clientConfigVersions.set(clientId, 0);
}
configVersion = clientConfigVersions.get(clientId) || 0;
}
// Set config version on websocket
ws.configVersion = configVersion;
// Add to Redis tracking if enabled
if (redisManager.isRedisEnabled()) {
try {
@@ -376,7 +322,7 @@ const addClient = async (
}
logger.info(
`Client added to tracking - ${clientType.toUpperCase()} ID: ${clientId}, Connection ID: ${connectionId}, Total connections: ${existingClients.length}, Config version: ${configVersion}`
`Client added to tracking - ${clientType.toUpperCase()} ID: ${clientId}, Connection ID: ${connectionId}, Total connections: ${existingClients.length}`
);
};
@@ -431,133 +377,53 @@ const removeClient = async (
}
};
// Helper to get the current config version for a client
const getClientConfigVersion = async (clientId: string): Promise<number | undefined> => {
// Try Redis first if available
if (redisManager.isRedisEnabled()) {
try {
const redisVersion = await redisManager.get(
getConfigVersionKey(clientId)
);
if (redisVersion !== null) {
const version = parseInt(redisVersion, 10);
// Sync local cache with Redis
clientConfigVersions.set(clientId, version);
return version;
}
} catch (error) {
logger.error("Failed to get config version from Redis:", error);
}
}
// Fall back to local cache
return clientConfigVersions.get(clientId);
};
// Helper to increment and get the new config version for a client
const incrementClientConfigVersion = async (
clientId: string
): Promise<number> => {
let newVersion: number;
if (redisManager.isRedisEnabled()) {
try {
// Use Redis INCR for atomic increment across nodes
newVersion = await redisManager.incr(getConfigVersionKey(clientId));
// Sync local cache
clientConfigVersions.set(clientId, newVersion);
return newVersion;
} catch (error) {
logger.error("Failed to increment config version in Redis:", error);
// Fall through to local increment
}
}
// Local increment
const currentVersion = clientConfigVersions.get(clientId) || 0;
newVersion = currentVersion + 1;
clientConfigVersions.set(clientId, newVersion);
return newVersion;
};
// Local message sending (within this node)
const sendToClientLocal = async (
clientId: string,
message: WSMessage,
options: SendMessageOptions = {}
message: WSMessage
): Promise<boolean> => {
const mapKey = getClientMapKey(clientId);
const clients = connectedClients.get(mapKey);
if (!clients || clients.length === 0) {
return false;
}
// Handle config version
let configVersion = await getClientConfigVersion(clientId);
// Add config version to message
const messageWithVersion = {
...message,
configVersion
};
const messageString = JSON.stringify(messageWithVersion);
const messageString = JSON.stringify(message);
clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(messageString);
}
});
logger.debug(
`sendToClient: Message type ${message.type} sent to clientId ${clientId}`
);
return true;
};
const broadcastToAllExceptLocal = async (
message: WSMessage,
excludeClientId?: string,
options: SendMessageOptions = {}
excludeClientId?: string
): Promise<void> => {
for (const [mapKey, clients] of connectedClients.entries()) {
connectedClients.forEach((clients, mapKey) => {
const [type, id] = mapKey.split(":");
const clientId = mapKey; // mapKey is the clientId
if (!(excludeClientId && clientId === excludeClientId)) {
// Handle config version per client
let configVersion = await getClientConfigVersion(clientId);
if (options.incrementConfigVersion) {
configVersion = await incrementClientConfigVersion(clientId);
}
// Add config version to message
const messageWithVersion = {
...message,
configVersion
};
if (!(excludeClientId && id === excludeClientId)) {
clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(messageWithVersion));
client.send(JSON.stringify(message));
}
});
}
}
});
};
// Cross-node message sending (via Redis)
const sendToClient = async (
clientId: string,
message: WSMessage,
options: SendMessageOptions = {}
message: WSMessage
): Promise<boolean> => {
let configVersion = await getClientConfigVersion(clientId);
if (options.incrementConfigVersion) {
configVersion = await incrementClientConfigVersion(clientId);
}
logger.debug(
`sendToClient: Message type ${message.type} sent to clientId ${clientId} (new configVersion: ${configVersion})`
);
// Try to send locally first
const localSent = await sendToClientLocal(clientId, message, options);
const localSent = await sendToClientLocal(clientId, message);
// Only send via Redis if the client is not connected locally and Redis is enabled
if (!localSent && redisManager.isRedisEnabled()) {
@@ -565,10 +431,7 @@ const sendToClient = async (
const redisMessage: RedisMessage = {
type: "direct",
targetClientId: clientId,
message: {
...message,
configVersion
},
message,
fromNodeId: NODE_ID
};
@@ -595,22 +458,19 @@ const sendToClient = async (
const broadcastToAllExcept = async (
message: WSMessage,
excludeClientId?: string,
options: SendMessageOptions = {}
excludeClientId?: string
): Promise<void> => {
// Broadcast locally
await broadcastToAllExceptLocal(message, excludeClientId, options);
await broadcastToAllExceptLocal(message, excludeClientId);
// If Redis is enabled, also broadcast via Redis pub/sub to other nodes
// Note: For broadcasts, we include the options so remote nodes can handle versioning
if (redisManager.isRedisEnabled()) {
try {
const redisMessage: RedisMessage = {
type: "broadcast",
excludeClientId,
message,
fromNodeId: NODE_ID,
options
fromNodeId: NODE_ID
};
await redisManager.publish(
@@ -1076,6 +936,5 @@ export {
getActiveNodes,
disconnectClient,
NODE_ID,
cleanup,
getClientConfigVersion
cleanup
};

View File

@@ -17,4 +17,3 @@ export * from "./securityKey";
export * from "./startDeviceWebAuth";
export * from "./verifyDeviceWebAuth";
export * from "./pollDeviceWebAuth";
export * from "./lookupUser";

View File

@@ -1,224 +0,0 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import {
users,
userOrgs,
orgs,
idpOrg,
idp,
idpOidcConfig
} from "@server/db";
import { eq, or, sql, and, isNotNull, inArray } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { UserType } from "@server/types/UserTypes";
const lookupBodySchema = z.strictObject({
identifier: z.string().min(1).toLowerCase()
});
export type LookupUserResponse = {
found: boolean;
identifier: string;
accounts: Array<{
userId: string;
email: string | null;
username: string;
hasInternalAuth: boolean;
orgs: Array<{
orgId: string;
orgName: string;
idps: Array<{
idpId: number;
name: string;
variant: string | null;
}>;
hasInternalAuth: boolean;
}>;
}>;
};
// registry.registerPath({
// method: "post",
// path: "/auth/lookup-user",
// description: "Lookup user accounts by username or email and return available authentication methods.",
// tags: [OpenAPITags.Auth],
// request: {
// body: lookupBodySchema
// },
// responses: {}
// });
export async function lookupUser(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = lookupBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { identifier } = parsedBody.data;
// Query users matching identifier (case-insensitive)
// Match by username OR email
const matchingUsers = await db
.select({
userId: users.userId,
email: users.email,
username: users.username,
type: users.type,
passwordHash: users.passwordHash,
idpId: users.idpId
})
.from(users)
.where(
or(
sql`LOWER(${users.username}) = ${identifier}`,
sql`LOWER(${users.email}) = ${identifier}`
)
);
if (!matchingUsers || matchingUsers.length === 0) {
return response<LookupUserResponse>(res, {
data: {
found: false,
identifier,
accounts: []
},
success: true,
error: false,
message: "No accounts found",
status: HttpCode.OK
});
}
// Get unique user IDs
const userIds = [...new Set(matchingUsers.map((u) => u.userId))];
// Get all org memberships for these users
const orgMemberships = await db
.select({
userId: userOrgs.userId,
orgId: userOrgs.orgId,
orgName: orgs.name
})
.from(userOrgs)
.innerJoin(orgs, eq(orgs.orgId, userOrgs.orgId))
.where(inArray(userOrgs.userId, userIds));
// Get unique org IDs
const orgIds = [...new Set(orgMemberships.map((m) => m.orgId))];
// Get all IdPs for these orgs
const orgIdps =
orgIds.length > 0
? await db
.select({
orgId: idpOrg.orgId,
idpId: idp.idpId,
idpName: idp.name,
variant: idpOidcConfig.variant
})
.from(idpOrg)
.innerJoin(idp, eq(idp.idpId, idpOrg.idpId))
.innerJoin(
idpOidcConfig,
eq(idpOidcConfig.idpId, idp.idpId)
)
.where(inArray(idpOrg.orgId, orgIds))
: [];
// Build response structure
const accounts: LookupUserResponse["accounts"] = [];
for (const user of matchingUsers) {
const hasInternalAuth =
user.type === UserType.Internal && user.passwordHash !== null;
// Get orgs for this user
const userOrgMemberships = orgMemberships.filter(
(m) => m.userId === user.userId
);
// Deduplicate orgs (user might have multiple memberships in same org)
const uniqueOrgs = new Map<string, typeof userOrgMemberships[0]>();
for (const membership of userOrgMemberships) {
if (!uniqueOrgs.has(membership.orgId)) {
uniqueOrgs.set(membership.orgId, membership);
}
}
const orgsData = Array.from(uniqueOrgs.values()).map((membership) => {
// Get IdPs for this org where the user (with the exact identifier) is authenticated via that IdP
// Only show IdPs where the user's idpId matches
// Internal users don't have an idpId, so they won't see any IdPs
const orgIdpsList = orgIdps
.filter((idp) => {
if (idp.orgId !== membership.orgId) {
return false;
}
// Only show IdPs where the user (with exact identifier) is authenticated via that IdP
// This means user.idpId must match idp.idpId
if (user.idpId !== null && user.idpId === idp.idpId) {
return true;
}
return false;
})
.map((idp) => ({
idpId: idp.idpId,
name: idp.idpName,
variant: idp.variant
}));
// Check if user has internal auth for this org
// User has internal auth if they have an internal account type
const orgHasInternalAuth = hasInternalAuth;
return {
orgId: membership.orgId,
orgName: membership.orgName,
idps: orgIdpsList,
hasInternalAuth: orgHasInternalAuth
};
});
accounts.push({
userId: user.userId,
email: user.email,
username: user.username,
hasInternalAuth,
orgs: orgsData
});
}
return response<LookupUserResponse>(res, {
data: {
found: true,
identifier,
accounts
},
success: true,
error: false,
message: "User lookup completed",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -10,7 +10,6 @@ import { eq, and, gt } from "drizzle-orm";
import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
import { unauthorized } from "@server/auth/unauthorizedResponse";
import { getIosDeviceName, getMacDeviceName } from "@server/db/names";
const bodySchema = z
.object({
@@ -121,11 +120,6 @@ export async function verifyDeviceWebAuth(
);
}
const deviceName =
getMacDeviceName(deviceCode.deviceName) ||
getIosDeviceName(deviceCode.deviceName) ||
deviceCode.deviceName;
// If verify is false, just return metadata without verifying
if (!verify) {
return response<VerifyDeviceWebAuthResponse>(res, {
@@ -135,7 +129,7 @@ export async function verifyDeviceWebAuth(
metadata: {
ip: deviceCode.ip,
city: deviceCode.city,
deviceName: deviceName,
deviceName: deviceCode.deviceName,
applicationName: deviceCode.applicationName,
createdAt: deviceCode.createdAt
}

View File

@@ -49,43 +49,27 @@ const auditLogBuffer: Array<{
const BATCH_SIZE = 100; // Write to DB every 100 logs
const BATCH_INTERVAL_MS = 5000; // Or every 5 seconds, whichever comes first
const MAX_BUFFER_SIZE = 10000; // Prevent unbounded memory growth
let flushTimer: NodeJS.Timeout | null = null;
let isFlushInProgress = false;
/**
* Flush buffered logs to database
*/
async function flushAuditLogs() {
if (auditLogBuffer.length === 0 || isFlushInProgress) {
if (auditLogBuffer.length === 0) {
return;
}
isFlushInProgress = true;
// Take all current logs and clear buffer
const logsToWrite = auditLogBuffer.splice(0, auditLogBuffer.length);
try {
// Batch insert logs in groups of 25 to avoid overwhelming the database
const BATCH_DB_SIZE = 25;
for (let i = 0; i < logsToWrite.length; i += BATCH_DB_SIZE) {
const batch = logsToWrite.slice(i, i + BATCH_DB_SIZE);
await db.insert(requestAuditLog).values(batch);
}
// Batch insert all logs at once
await db.insert(requestAuditLog).values(logsToWrite);
logger.debug(`Flushed ${logsToWrite.length} audit logs to database`);
} catch (error) {
logger.error("Error flushing audit logs:", error);
// On error, we lose these logs - consider a fallback strategy if needed
// (e.g., write to file, or put back in buffer with retry limit)
} finally {
isFlushInProgress = false;
// If buffer filled up while we were flushing, flush again
if (auditLogBuffer.length >= BATCH_SIZE) {
flushAuditLogs().catch((err) =>
logger.error("Error in follow-up flush:", err)
);
}
}
}
@@ -111,10 +95,6 @@ export async function shutdownAuditLogger() {
clearTimeout(flushTimer);
flushTimer = null;
}
// Force flush even if one is in progress by waiting and retrying
while (isFlushInProgress) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
await flushAuditLogs();
}
@@ -232,14 +212,6 @@ export async function logRequestAudit(
? stripPortFromHost(body.requestIp)
: undefined;
// Prevent unbounded buffer growth - drop oldest entries if buffer is too large
if (auditLogBuffer.length >= MAX_BUFFER_SIZE) {
const dropped = auditLogBuffer.splice(0, BATCH_SIZE);
logger.warn(
`Audit log buffer exceeded max size (${MAX_BUFFER_SIZE}), dropped ${dropped.length} oldest entries`
);
}
// Add to buffer instead of writing directly to DB
auditLogBuffer.push({
timestamp,

View File

@@ -942,7 +942,7 @@ async function isUserAllowedToAccessResource(
username: user.username,
email: user.email,
name: user.name,
role: userOrgRole.roleName
role: user.role
};
}
@@ -956,7 +956,7 @@ async function isUserAllowedToAccessResource(
username: user.username,
email: user.email,
name: user.name,
role: userOrgRole.roleName
role: user.role
};
}
@@ -1035,25 +1035,14 @@ export function isPathAllowed(pattern: string, path: string): boolean {
logger.debug(`Normalized pattern parts: [${patternParts.join(", ")}]`);
logger.debug(`Normalized path parts: [${pathParts.join(", ")}]`);
// Maximum recursion depth to prevent stack overflow and memory issues
const MAX_RECURSION_DEPTH = 100;
// Recursive function to try different wildcard matches
function matchSegments(patternIndex: number, pathIndex: number, depth: number = 0): boolean {
// Check recursion depth limit
if (depth > MAX_RECURSION_DEPTH) {
logger.warn(
`Path matching exceeded maximum recursion depth (${MAX_RECURSION_DEPTH}) for pattern "${pattern}" and path "${path}"`
);
return false;
}
const indent = " ".repeat(depth); // Indent based on recursion depth
function matchSegments(patternIndex: number, pathIndex: number): boolean {
const indent = " ".repeat(pathIndex); // Indent based on recursion depth
const currentPatternPart = patternParts[patternIndex];
const currentPathPart = pathParts[pathIndex];
logger.debug(
`${indent}Checking patternIndex=${patternIndex} (${currentPatternPart || "END"}) vs pathIndex=${pathIndex} (${currentPathPart || "END"}) [depth=${depth}]`
`${indent}Checking patternIndex=${patternIndex} (${currentPatternPart || "END"}) vs pathIndex=${pathIndex} (${currentPathPart || "END"})`
);
// If we've consumed all pattern parts, we should have consumed all path parts
@@ -1086,7 +1075,7 @@ export function isPathAllowed(pattern: string, path: string): boolean {
logger.debug(
`${indent}Trying to skip wildcard (consume 0 segments)`
);
if (matchSegments(patternIndex + 1, pathIndex, depth + 1)) {
if (matchSegments(patternIndex + 1, pathIndex)) {
logger.debug(
`${indent}Successfully matched by skipping wildcard`
);
@@ -1097,7 +1086,7 @@ export function isPathAllowed(pattern: string, path: string): boolean {
logger.debug(
`${indent}Trying to consume segment "${currentPathPart}" for wildcard`
);
if (matchSegments(patternIndex, pathIndex + 1, depth + 1)) {
if (matchSegments(patternIndex, pathIndex + 1)) {
logger.debug(
`${indent}Successfully matched by consuming segment for wildcard`
);
@@ -1125,7 +1114,7 @@ export function isPathAllowed(pattern: string, path: string): boolean {
logger.debug(
`${indent}Segment with wildcard matches: "${currentPatternPart}" matches "${currentPathPart}"`
);
return matchSegments(patternIndex + 1, pathIndex + 1, depth + 1);
return matchSegments(patternIndex + 1, pathIndex + 1);
}
logger.debug(
@@ -1146,10 +1135,10 @@ export function isPathAllowed(pattern: string, path: string): boolean {
`${indent}Segments match: "${currentPatternPart}" = "${currentPathPart}"`
);
// Move to next segments in both pattern and path
return matchSegments(patternIndex + 1, pathIndex + 1, depth + 1);
return matchSegments(patternIndex + 1, pathIndex + 1);
}
const result = matchSegments(0, 0, 0);
const result = matchSegments(0, 0);
logger.debug(`Final result: ${result}`);
return result;
}

View File

@@ -1,105 +0,0 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { clients } from "@server/db";
import { eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
import { sendTerminateClient } from "./terminate";
const archiveClientSchema = z.strictObject({
clientId: z.string().transform(Number).pipe(z.int().positive())
});
registry.registerPath({
method: "post",
path: "/client/{clientId}/archive",
description: "Archive a client by its client ID.",
tags: [OpenAPITags.Client],
request: {
params: archiveClientSchema
},
responses: {}
});
export async function archiveClient(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = archiveClientSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { clientId } = parsedParams.data;
// Check if client exists
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, clientId))
.limit(1);
if (!client) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Client with ID ${clientId} not found`
)
);
}
if (client.archived) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Client with ID ${clientId} is already archived`
)
);
}
await db.transaction(async (trx) => {
// Archive the client
await trx
.update(clients)
.set({ archived: true })
.where(eq(clients.clientId, clientId));
// Rebuild associations to clean up related data
await rebuildClientAssociationsFromClient(client, trx);
// Send terminate signal if there's an associated OLM
if (client.olmId) {
await sendTerminateClient(client.clientId, client.olmId);
}
});
return response(res, {
data: null,
success: true,
error: false,
message: "Client archived successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to archive client"
)
);
}
}

View File

@@ -1,101 +0,0 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { clients } from "@server/db";
import { eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { sendTerminateClient } from "./terminate";
const blockClientSchema = z.strictObject({
clientId: z.string().transform(Number).pipe(z.int().positive())
});
registry.registerPath({
method: "post",
path: "/client/{clientId}/block",
description: "Block a client by its client ID.",
tags: [OpenAPITags.Client],
request: {
params: blockClientSchema
},
responses: {}
});
export async function blockClient(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = blockClientSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { clientId } = parsedParams.data;
// Check if client exists
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, clientId))
.limit(1);
if (!client) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Client with ID ${clientId} not found`
)
);
}
if (client.blocked) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Client with ID ${clientId} is already blocked`
)
);
}
await db.transaction(async (trx) => {
// Block the client
await trx
.update(clients)
.set({ blocked: true, approvalState: "denied" })
.where(eq(clients.clientId, clientId));
// Send terminate signal if there's an associated OLM and it's connected
if (client.olmId && client.online) {
await sendTerminateClient(client.clientId, client.olmId);
}
});
return response(res, {
data: null,
success: true,
error: false,
message: "Client blocked successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to block client"
)
);
}
}

View File

@@ -60,12 +60,11 @@ export async function deleteClient(
);
}
// Only allow deletion of machine clients (clients without userId)
if (client.userId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Cannot delete a user client. User clients must be archived instead.`
`Cannot delete a user client with this endpoint`
)
);
}

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, olms } from "@server/db";
import { clients, fingerprints } from "@server/db";
import { clients } from "@server/db";
import { eq, and } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -10,7 +10,6 @@ import logger from "@server/logger";
import stoi from "@server/lib/stoi";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { getUserDeviceName } from "@server/db/names";
const getClientSchema = z.strictObject({
clientId: z
@@ -30,7 +29,6 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
.from(clients)
.where(eq(clients.clientId, clientId))
.leftJoin(olms, eq(clients.clientId, olms.clientId))
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
.limit(1);
return res;
} else if (niceId && orgId) {
@@ -39,7 +37,6 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
.from(clients)
.where(and(eq(clients.niceId, niceId), eq(clients.orgId, orgId)))
.leftJoin(olms, eq(clients.clientId, olms.clientId))
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
.limit(1);
return res;
}
@@ -108,16 +105,8 @@ export async function getClient(
);
}
// Replace name with device name if OLM exists
let clientName = client.clients.name;
if (client.olms) {
const model = client.fingerprints?.deviceModel || null;
clientName = getUserDeviceName(model, client.clients.name);
}
const data: GetClientResponse = {
...client.clients,
name: clientName,
olmId: client.olms ? client.olms.olmId : null
};

View File

@@ -1,10 +1,6 @@
export * from "./pickClientDefaults";
export * from "./createClient";
export * from "./deleteClient";
export * from "./archiveClient";
export * from "./unarchiveClient";
export * from "./blockClient";
export * from "./unblockClient";
export * from "./listClients";
export * from "./updateClient";
export * from "./getClient";

View File

@@ -5,8 +5,7 @@ import {
roleClients,
sites,
userClients,
clientSitesAssociationsCache,
fingerprints
clientSitesAssociationsCache
} from "@server/db";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
@@ -28,7 +27,6 @@ import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import NodeCache from "node-cache";
import semver from "semver";
import { getUserDeviceName } from "@server/db/names";
const olmVersionCache = new NodeCache({ stdTTL: 3600 });
@@ -138,18 +136,12 @@ function queryClients(
username: users.username,
userEmail: users.email,
niceId: clients.niceId,
agent: olms.agent,
approvalState: clients.approvalState,
olmArchived: olms.archived,
archived: clients.archived,
blocked: clients.blocked,
deviceModel: fingerprints.deviceModel
agent: olms.agent
})
.from(clients)
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
.leftJoin(olms, eq(clients.clientId, olms.clientId))
.leftJoin(users, eq(clients.userId, users.userId))
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
.where(and(...conditions));
}
@@ -168,22 +160,21 @@ async function getSiteAssociations(clientIds: number[]) {
.where(inArray(clientSitesAssociationsCache.clientId, clientIds));
}
type ClientWithSites = Omit<
Awaited<ReturnType<typeof queryClients>>[0],
"deviceModel"
> & {
sites: Array<{
siteId: number;
siteName: string | null;
siteNiceId: string | null;
}>;
type OlmWithUpdateAvailable = Awaited<ReturnType<typeof queryClients>>[0] & {
olmUpdateAvailable?: boolean;
};
type OlmWithUpdateAvailable = ClientWithSites;
export type ListClientsResponse = {
clients: Array<ClientWithSites>;
clients: Array<
Awaited<ReturnType<typeof queryClients>>[0] & {
sites: Array<{
siteId: number;
siteName: string | null;
siteNiceId: string | null;
}>;
olmUpdateAvailable?: boolean;
}
>;
pagination: { total: number; limit: number; offset: number };
};
@@ -313,17 +304,11 @@ export async function listClients(
>
);
// Merge clients with their site associations and replace name with device name
const clientsWithSites = clientsList.map((client) => {
const model = client.deviceModel || null;
const newName = getUserDeviceName(model, client.name);
const { deviceModel, ...clientWithoutDeviceModel } = client;
return {
...clientWithoutDeviceModel,
name: newName,
sites: sitesByClient[client.clientId] || []
};
});
// Merge clients with their site associations
const clientsWithSites = clientsList.map((client) => ({
...client,
sites: sitesByClient[client.clientId] || []
}));
const latestOlVersionPromise = getLatestOlmVersion();
@@ -362,7 +347,7 @@ export async function listClients(
return response<ListClientsResponse>(res, {
data: {
clients: olmsWithUpdates,
clients: clientsWithSites,
pagination: {
total: totalCount,
limit,

View File

@@ -28,7 +28,7 @@ export async function addTargets(newtId: string, targets: SubnetProxyTarget[]) {
await sendToClient(newtId, {
type: `newt/wg/targets/add`,
data: batches[i]
}, { incrementConfigVersion: true });
});
}
}
@@ -44,7 +44,7 @@ export async function removeTargets(
await sendToClient(newtId, {
type: `newt/wg/targets/remove`,
data: batches[i]
},{ incrementConfigVersion: true });
});
}
}
@@ -69,7 +69,7 @@ export async function updateTargets(
oldTargets: oldBatches[i] || [],
newTargets: newBatches[i] || []
}
}, { incrementConfigVersion: true }).catch((error) => {
}).catch((error) => {
logger.warn(`Error sending message:`, error);
});
}
@@ -101,7 +101,7 @@ export async function addPeerData(
remoteSubnets: remoteSubnets,
aliases: aliases
}
}, { incrementConfigVersion: true }).catch((error) => {
}).catch((error) => {
logger.warn(`Error sending message:`, error);
});
}
@@ -132,7 +132,7 @@ export async function removePeerData(
remoteSubnets: remoteSubnets,
aliases: aliases
}
}, { incrementConfigVersion: true }).catch((error) => {
}).catch((error) => {
logger.warn(`Error sending message:`, error);
});
}
@@ -173,7 +173,7 @@ export async function updatePeerData(
...remoteSubnets,
...aliases
}
}, { incrementConfigVersion: true }).catch((error) => {
}).catch((error) => {
logger.warn(`Error sending message:`, error);
});
}

View File

@@ -1,93 +0,0 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { clients } from "@server/db";
import { eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
const unarchiveClientSchema = z.strictObject({
clientId: z.string().transform(Number).pipe(z.int().positive())
});
registry.registerPath({
method: "post",
path: "/client/{clientId}/unarchive",
description: "Unarchive a client by its client ID.",
tags: [OpenAPITags.Client],
request: {
params: unarchiveClientSchema
},
responses: {}
});
export async function unarchiveClient(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = unarchiveClientSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { clientId } = parsedParams.data;
// Check if client exists
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, clientId))
.limit(1);
if (!client) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Client with ID ${clientId} not found`
)
);
}
if (!client.archived) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Client with ID ${clientId} is not archived`
)
);
}
// Unarchive the client
await db
.update(clients)
.set({ archived: false })
.where(eq(clients.clientId, clientId));
return response(res, {
data: null,
success: true,
error: false,
message: "Client unarchived successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to unarchive client"
)
);
}
}

View File

@@ -1,93 +0,0 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { clients } from "@server/db";
import { eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
const unblockClientSchema = z.strictObject({
clientId: z.string().transform(Number).pipe(z.int().positive())
});
registry.registerPath({
method: "post",
path: "/client/{clientId}/unblock",
description: "Unblock a client by its client ID.",
tags: [OpenAPITags.Client],
request: {
params: unblockClientSchema
},
responses: {}
});
export async function unblockClient(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = unblockClientSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { clientId } = parsedParams.data;
// Check if client exists
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, clientId))
.limit(1);
if (!client) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Client with ID ${clientId} not found`
)
);
}
if (!client.blocked) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Client with ID ${clientId} is not blocked`
)
);
}
// Unblock the client
await db
.update(clients)
.set({ blocked: false, approvalState: null })
.where(eq(clients.clientId, clientId));
return response(res, {
data: null,
success: true,
error: false,
message: "Client unblocked successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to unblock client"
)
);
}
}

View File

@@ -174,38 +174,6 @@ authenticated.delete(
client.deleteClient
);
authenticated.post(
"/client/:clientId/archive",
verifyClientAccess,
verifyUserHasAction(ActionsEnum.archiveClient),
logActionAudit(ActionsEnum.archiveClient),
client.archiveClient
);
authenticated.post(
"/client/:clientId/unarchive",
verifyClientAccess,
verifyUserHasAction(ActionsEnum.unarchiveClient),
logActionAudit(ActionsEnum.unarchiveClient),
client.unarchiveClient
);
authenticated.post(
"/client/:clientId/block",
verifyClientAccess,
verifyUserHasAction(ActionsEnum.blockClient),
logActionAudit(ActionsEnum.blockClient),
client.blockClient
);
authenticated.post(
"/client/:clientId/unblock",
verifyClientAccess,
verifyUserHasAction(ActionsEnum.unblockClient),
logActionAudit(ActionsEnum.unblockClient),
client.unblockClient
);
authenticated.post(
"/client/:clientId",
verifyClientAccess, // this will check if the user has access to the client
@@ -586,14 +554,6 @@ authenticated.get(
verifyUserHasAction(ActionsEnum.listRoles),
role.listRoles
);
authenticated.post(
"/org/:orgId/role/:roleId",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.updateRole),
logActionAudit(ActionsEnum.updateRole),
role.updateRole
);
// authenticated.get(
// "/role/:roleId",
// verifyRoleAccess,
@@ -848,18 +808,11 @@ authenticated.put("/user/:userId/olm", verifyIsLoggedInUser, olm.createUserOlm);
authenticated.get("/user/:userId/olms", verifyIsLoggedInUser, olm.listUserOlms);
authenticated.post(
"/user/:userId/olm/:olmId/archive",
authenticated.delete(
"/user/:userId/olm/:olmId",
verifyIsLoggedInUser,
verifyOlmAccess,
olm.archiveUserOlm
);
authenticated.post(
"/user/:userId/olm/:olmId/unarchive",
verifyIsLoggedInUser,
verifyOlmAccess,
olm.unarchiveUserOlm
olm.deleteUserOlm
);
authenticated.get(
@@ -869,12 +822,6 @@ authenticated.get(
olm.getUserOlm
);
authenticated.post(
"/user/:userId/olm/recover",
verifyIsLoggedInUser,
olm.recoverOlmWithFingerprint
);
authenticated.put(
"/idp/oidc",
verifyUserIsServerAdmin,
@@ -1121,21 +1068,6 @@ authRouter.post(
auth.login
);
authRouter.post("/logout", auth.logout);
authRouter.post(
"/lookup-user",
rateLimit({
windowMs: 15 * 60 * 1000,
max: 15,
keyGenerator: (req) =>
`lookupUser:${req.body.identifier || ipKeyGenerator(req.ip || "")}`,
handler: (req, res, next) => {
const message = `You can only lookup users ${15} times every ${15} minutes. Please try again later.`;
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
},
store: createStore()
}),
auth.lookupUser
);
authRouter.post(
"/newt/get-token",
rateLimit({

View File

@@ -24,8 +24,7 @@ const bodySchema = z.strictObject({
emailPath: z.string().optional(),
namePath: z.string().optional(),
scopes: z.string().nonempty(),
autoProvision: z.boolean().optional(),
tags: z.string().optional()
autoProvision: z.boolean().optional()
});
export type CreateIdpResponse = {
@@ -76,8 +75,7 @@ export async function createOidcIdp(
emailPath,
namePath,
name,
autoProvision,
tags
autoProvision
} = parsedBody.data;
const key = config.getRawConfig().server.secret!;
@@ -92,8 +90,7 @@ export async function createOidcIdp(
.values({
name,
autoProvision,
type: "oidc",
tags
type: "oidc"
})
.returning();

View File

@@ -33,8 +33,7 @@ async function query(limit: number, offset: number) {
type: idp.type,
variant: idpOidcConfig.variant,
orgCount: sql<number>`count(${idpOrg.orgId})`,
autoProvision: idp.autoProvision,
tags: idp.tags
autoProvision: idp.autoProvision
})
.from(idp)
.leftJoin(idpOrg, sql`${idp.idpId} = ${idpOrg.idpId}`)

View File

@@ -30,8 +30,7 @@ const bodySchema = z.strictObject({
scopes: z.string().optional(),
autoProvision: z.boolean().optional(),
defaultRoleMapping: z.string().optional(),
defaultOrgMapping: z.string().optional(),
tags: z.string().optional()
defaultOrgMapping: z.string().optional()
});
export type UpdateIdpResponse = {
@@ -95,8 +94,7 @@ export async function updateOidcIdp(
name,
autoProvision,
defaultRoleMapping,
defaultOrgMapping,
tags
defaultOrgMapping
} = parsedBody.data;
// Check if IDP exists and is of type OIDC
@@ -129,8 +127,7 @@ export async function updateOidcIdp(
name,
autoProvision,
defaultRoleMapping,
defaultOrgMapping,
tags
defaultOrgMapping
};
// only update if at least one key is not undefined

View File

@@ -467,14 +467,6 @@ authenticated.put(
role.createRole
);
authenticated.post(
"/org/:orgId/role/:roleId",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.updateRole),
logActionAudit(ActionsEnum.updateRole),
role.updateRole
);
authenticated.get(
"/org/:orgId/roles",
verifyApiKeyOrgAccess,
@@ -759,10 +751,9 @@ authenticated.post(
);
authenticated.get(
"/idp", // no guards on this because anyone can list idps for login purposes
// we do the same for the external api
// verifyApiKeyIsRoot,
// verifyApiKeyHasAction(ActionsEnum.listIdps),
"/idp",
verifyApiKeyIsRoot,
verifyApiKeyHasAction(ActionsEnum.listIdps),
idp.listIdps
);
@@ -851,38 +842,6 @@ authenticated.delete(
client.deleteClient
);
authenticated.post(
"/client/:clientId/archive",
verifyApiKeyClientAccess,
verifyApiKeyHasAction(ActionsEnum.archiveClient),
logActionAudit(ActionsEnum.archiveClient),
client.archiveClient
);
authenticated.post(
"/client/:clientId/unarchive",
verifyApiKeyClientAccess,
verifyApiKeyHasAction(ActionsEnum.unarchiveClient),
logActionAudit(ActionsEnum.unarchiveClient),
client.unarchiveClient
);
authenticated.post(
"/client/:clientId/block",
verifyApiKeyClientAccess,
verifyApiKeyHasAction(ActionsEnum.blockClient),
logActionAudit(ActionsEnum.blockClient),
client.blockClient
);
authenticated.post(
"/client/:clientId/unblock",
verifyApiKeyClientAccess,
verifyApiKeyHasAction(ActionsEnum.unblockClient),
logActionAudit(ActionsEnum.unblockClient),
client.unblockClient
);
authenticated.post(
"/client/:clientId",
verifyApiKeyClientAccess,

View File

@@ -1,278 +0,0 @@
import { clients, clientSiteResourcesAssociationsCache, clientSitesAssociationsCache, db, ExitNode, resources, Site, siteResources, targetHealthCheck, targets } from "@server/db";
import logger from "@server/logger";
import { initPeerAddHandshake, updatePeer } from "../olm/peers";
import { eq, and } from "drizzle-orm";
import config from "@server/lib/config";
import { generateSubnetProxyTargets, SubnetProxyTarget } from "@server/lib/ip";
export async function buildClientConfigurationForNewtClient(
site: Site,
exitNode?: ExitNode
) {
const siteId = site.siteId;
// Get all clients connected to this site
const clientsRes = await db
.select()
.from(clients)
.innerJoin(
clientSitesAssociationsCache,
eq(clients.clientId, clientSitesAssociationsCache.clientId)
)
.where(eq(clientSitesAssociationsCache.siteId, siteId));
let peers: Array<{
publicKey: string;
allowedIps: string[];
endpoint?: string;
}> = [];
if (site.publicKey && site.endpoint && exitNode) {
// Prepare peers data for the response
peers = await Promise.all(
clientsRes
.filter((client) => {
if (!client.clients.pubKey) {
logger.warn(
`Client ${client.clients.clientId} has no public key, skipping`
);
return false;
}
if (!client.clients.subnet) {
logger.warn(
`Client ${client.clients.clientId} has no subnet, skipping`
);
return false;
}
return true;
})
.map(async (client) => {
// Add or update this peer on the olm if it is connected
// const allSiteResources = await db // only get the site resources that this client has access to
// .select()
// .from(siteResources)
// .innerJoin(
// clientSiteResourcesAssociationsCache,
// eq(
// siteResources.siteResourceId,
// clientSiteResourcesAssociationsCache.siteResourceId
// )
// )
// .where(
// and(
// eq(siteResources.siteId, site.siteId),
// eq(
// clientSiteResourcesAssociationsCache.clientId,
// client.clients.clientId
// )
// )
// );
// update the peer info on the olm
// if the peer has not been added yet this will be a no-op
await updatePeer(client.clients.clientId, {
siteId: site.siteId,
endpoint: site.endpoint!,
relayEndpoint: `${exitNode.endpoint}:${config.getRawConfig().gerbil.clients_start_port}`,
publicKey: site.publicKey!,
serverIP: site.address,
serverPort: site.listenPort
// remoteSubnets: generateRemoteSubnets(
// allSiteResources.map(
// ({ siteResources }) => siteResources
// )
// ),
// aliases: generateAliasConfig(
// allSiteResources.map(
// ({ siteResources }) => siteResources
// )
// )
});
// also trigger the peer add handshake in case the peer was not already added to the olm and we need to hole punch
// if it has already been added this will be a no-op
await initPeerAddHandshake(
// this will kick off the add peer process for the client
client.clients.clientId,
{
siteId,
exitNode: {
publicKey: exitNode.publicKey,
endpoint: exitNode.endpoint
}
}
);
return {
publicKey: client.clients.pubKey!,
allowedIps: [
`${client.clients.subnet.split("/")[0]}/32`
], // we want to only allow from that client
endpoint: client.clientSitesAssociationsCache.isRelayed
? ""
: client.clientSitesAssociationsCache.endpoint! // if its relayed it should be localhost
};
})
);
}
// Filter out any null values from peers that didn't have an olm
const validPeers = peers.filter((peer) => peer !== null);
// Get all enabled site resources for this site
const allSiteResources = await db
.select()
.from(siteResources)
.where(eq(siteResources.siteId, siteId));
const targetsToSend: SubnetProxyTarget[] = [];
for (const resource of allSiteResources) {
// Get clients associated with this specific resource
const resourceClients = await db
.select({
clientId: clients.clientId,
pubKey: clients.pubKey,
subnet: clients.subnet
})
.from(clients)
.innerJoin(
clientSiteResourcesAssociationsCache,
eq(
clients.clientId,
clientSiteResourcesAssociationsCache.clientId
)
)
.where(
eq(
clientSiteResourcesAssociationsCache.siteResourceId,
resource.siteResourceId
)
);
const resourceTargets = generateSubnetProxyTargets(
resource,
resourceClients
);
targetsToSend.push(...resourceTargets);
}
return {
peers: validPeers,
targets: targetsToSend
};
}
export async function buildTargetConfigurationForNewtClient(siteId: number) {
// Get all enabled targets with their resource protocol information
const allTargets = await db
.select({
resourceId: targets.resourceId,
targetId: targets.targetId,
ip: targets.ip,
method: targets.method,
port: targets.port,
internalPort: targets.internalPort,
enabled: targets.enabled,
protocol: resources.protocol,
hcEnabled: targetHealthCheck.hcEnabled,
hcPath: targetHealthCheck.hcPath,
hcScheme: targetHealthCheck.hcScheme,
hcMode: targetHealthCheck.hcMode,
hcHostname: targetHealthCheck.hcHostname,
hcPort: targetHealthCheck.hcPort,
hcInterval: targetHealthCheck.hcInterval,
hcUnhealthyInterval: targetHealthCheck.hcUnhealthyInterval,
hcTimeout: targetHealthCheck.hcTimeout,
hcHeaders: targetHealthCheck.hcHeaders,
hcMethod: targetHealthCheck.hcMethod,
hcTlsServerName: targetHealthCheck.hcTlsServerName
})
.from(targets)
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
.leftJoin(
targetHealthCheck,
eq(targets.targetId, targetHealthCheck.targetId)
)
.where(and(eq(targets.siteId, siteId), eq(targets.enabled, true)));
const { tcpTargets, udpTargets } = allTargets.reduce(
(acc, target) => {
// Filter out invalid targets
if (!target.internalPort || !target.ip || !target.port) {
return acc;
}
// Format target into string
const formattedTarget = `${target.internalPort}:${target.ip}:${target.port}`;
// Add to the appropriate protocol array
if (target.protocol === "tcp") {
acc.tcpTargets.push(formattedTarget);
} else {
acc.udpTargets.push(formattedTarget);
}
return acc;
},
{ tcpTargets: [] as string[], udpTargets: [] as string[] }
);
const healthCheckTargets = allTargets.map((target) => {
// make sure the stuff is defined
if (
!target.hcPath ||
!target.hcHostname ||
!target.hcPort ||
!target.hcInterval ||
!target.hcMethod
) {
logger.debug(
`Skipping target ${target.targetId} due to missing health check fields`
);
return null; // Skip targets with missing health check fields
}
// parse headers
const hcHeadersParse = target.hcHeaders
? JSON.parse(target.hcHeaders)
: null;
const hcHeadersSend: { [key: string]: string } = {};
if (hcHeadersParse) {
hcHeadersParse.forEach(
(header: { name: string; value: string }) => {
hcHeadersSend[header.name] = header.value;
}
);
}
return {
id: target.targetId,
hcEnabled: target.hcEnabled,
hcPath: target.hcPath,
hcScheme: target.hcScheme,
hcMode: target.hcMode,
hcHostname: target.hcHostname,
hcPort: target.hcPort,
hcInterval: target.hcInterval, // in seconds
hcUnhealthyInterval: target.hcUnhealthyInterval, // in seconds
hcTimeout: target.hcTimeout, // in seconds
hcHeaders: hcHeadersSend,
hcMethod: target.hcMethod,
hcTlsServerName: target.hcTlsServerName
};
});
// Filter out any null values from health check targets
const validHealthCheckTargets = healthCheckTargets.filter(
(target) => target !== null
);
return {
validHealthCheckTargets,
tcpTargets,
udpTargets
};
}

View File

@@ -2,10 +2,19 @@ import { z } from "zod";
import { MessageHandler } from "@server/routers/ws";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { db, ExitNode, exitNodes, Newt, sites } from "@server/db";
import {
db,
ExitNode,
exitNodes,
siteResources,
clientSiteResourcesAssociationsCache
} from "@server/db";
import { clients, clientSitesAssociationsCache, Newt, sites } from "@server/db";
import { eq } from "drizzle-orm";
import { initPeerAddHandshake, updatePeer } from "../olm/peers";
import { sendToExitNode } from "#dynamic/lib/exitNodes";
import { buildClientConfigurationForNewtClient } from "./buildConfiguration";
import { generateSubnetProxyTargets, SubnetProxyTarget } from "@server/lib/ip";
import config from "@server/lib/config";
const inputSchema = z.object({
publicKey: z.string(),
@@ -121,18 +130,167 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
}
}
const { peers, targets } = await buildClientConfigurationForNewtClient(
site,
exitNode
);
// Get all clients connected to this site
const clientsRes = await db
.select()
.from(clients)
.innerJoin(
clientSitesAssociationsCache,
eq(clients.clientId, clientSitesAssociationsCache.clientId)
)
.where(eq(clientSitesAssociationsCache.siteId, siteId));
let peers: Array<{
publicKey: string;
allowedIps: string[];
endpoint?: string;
}> = [];
if (site.publicKey && site.endpoint && exitNode) {
// Prepare peers data for the response
peers = await Promise.all(
clientsRes
.filter((client) => {
if (!client.clients.pubKey) {
logger.warn(
`Client ${client.clients.clientId} has no public key, skipping`
);
return false;
}
if (!client.clients.subnet) {
logger.warn(
`Client ${client.clients.clientId} has no subnet, skipping`
);
return false;
}
return true;
})
.map(async (client) => {
// Add or update this peer on the olm if it is connected
// const allSiteResources = await db // only get the site resources that this client has access to
// .select()
// .from(siteResources)
// .innerJoin(
// clientSiteResourcesAssociationsCache,
// eq(
// siteResources.siteResourceId,
// clientSiteResourcesAssociationsCache.siteResourceId
// )
// )
// .where(
// and(
// eq(siteResources.siteId, site.siteId),
// eq(
// clientSiteResourcesAssociationsCache.clientId,
// client.clients.clientId
// )
// )
// );
// update the peer info on the olm
// if the peer has not been added yet this will be a no-op
await updatePeer(client.clients.clientId, {
siteId: site.siteId,
endpoint: site.endpoint!,
relayEndpoint: `${exitNode.endpoint}:${config.getRawConfig().gerbil.clients_start_port}`,
publicKey: site.publicKey!,
serverIP: site.address,
serverPort: site.listenPort
// remoteSubnets: generateRemoteSubnets(
// allSiteResources.map(
// ({ siteResources }) => siteResources
// )
// ),
// aliases: generateAliasConfig(
// allSiteResources.map(
// ({ siteResources }) => siteResources
// )
// )
});
// also trigger the peer add handshake in case the peer was not already added to the olm and we need to hole punch
// if it has already been added this will be a no-op
await initPeerAddHandshake(
// this will kick off the add peer process for the client
client.clients.clientId,
{
siteId,
exitNode: {
publicKey: exitNode.publicKey,
endpoint: exitNode.endpoint
}
}
);
return {
publicKey: client.clients.pubKey!,
allowedIps: [
`${client.clients.subnet.split("/")[0]}/32`
], // we want to only allow from that client
endpoint: client.clientSitesAssociationsCache.isRelayed
? ""
: client.clientSitesAssociationsCache.endpoint! // if its relayed it should be localhost
};
})
);
}
// Filter out any null values from peers that didn't have an olm
const validPeers = peers.filter((peer) => peer !== null);
// Get all enabled site resources for this site
const allSiteResources = await db
.select()
.from(siteResources)
.where(eq(siteResources.siteId, siteId));
const targetsToSend: SubnetProxyTarget[] = [];
for (const resource of allSiteResources) {
// Get clients associated with this specific resource
const resourceClients = await db
.select({
clientId: clients.clientId,
pubKey: clients.pubKey,
subnet: clients.subnet
})
.from(clients)
.innerJoin(
clientSiteResourcesAssociationsCache,
eq(
clients.clientId,
clientSiteResourcesAssociationsCache.clientId
)
)
.where(
eq(
clientSiteResourcesAssociationsCache.siteResourceId,
resource.siteResourceId
)
);
const resourceTargets = generateSubnetProxyTargets(
resource,
resourceClients
);
targetsToSend.push(...resourceTargets);
}
// Build the configuration response
const configResponse = {
ipAddress: site.address,
peers: validPeers,
targets: targetsToSend
};
logger.debug("Sending config: ", configResponse);
return {
message: {
type: "newt/wg/receive-config",
data: {
ipAddress: site.address,
peers,
targets
...configResponse
}
},
broadcast: false,

View File

@@ -1,163 +0,0 @@
import { db, sites } from "@server/db";
import { disconnectClient, getClientConfigVersion } from "#dynamic/routers/ws";
import { MessageHandler } from "@server/routers/ws";
import { clients, Newt } from "@server/db";
import { eq, lt, isNull, and, or } from "drizzle-orm";
import logger from "@server/logger";
import { validateSessionToken } from "@server/auth/sessions/app";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { sendTerminateClient } from "../client/terminate";
import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
import { sendNewtSyncMessage } from "./sync";
// Track if the offline checker interval is running
// let offlineCheckerInterval: NodeJS.Timeout | null = null;
// const OFFLINE_CHECK_INTERVAL = 30 * 1000; // Check every 30 seconds
// const OFFLINE_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes
/**
* Starts the background interval that checks for clients that haven't pinged recently
* and marks them as offline
*/
// export const startNewtOfflineChecker = (): void => {
// if (offlineCheckerInterval) {
// return; // Already running
// }
// offlineCheckerInterval = setInterval(async () => {
// try {
// const twoMinutesAgo = Math.floor(
// (Date.now() - OFFLINE_THRESHOLD_MS) / 1000
// );
// // TODO: WE NEED TO MAKE SURE THIS WORKS WITH DISTRIBUTED NODES ALL DOING THE SAME THING
// // Find clients that haven't pinged in the last 2 minutes and mark them as offline
// const offlineClients = await db
// .update(clients)
// .set({ online: false })
// .where(
// and(
// eq(clients.online, true),
// or(
// lt(clients.lastPing, twoMinutesAgo),
// isNull(clients.lastPing)
// )
// )
// )
// .returning();
// for (const offlineClient of offlineClients) {
// logger.info(
// `Kicking offline newt client ${offlineClient.clientId} due to inactivity`
// );
// if (!offlineClient.newtId) {
// logger.warn(
// `Offline client ${offlineClient.clientId} has no newtId, cannot disconnect`
// );
// continue;
// }
// // Send a disconnect message to the client if connected
// try {
// await sendTerminateClient(
// offlineClient.clientId,
// offlineClient.newtId
// ); // terminate first
// // wait a moment to ensure the message is sent
// await new Promise((resolve) => setTimeout(resolve, 1000));
// await disconnectClient(offlineClient.newtId);
// } catch (error) {
// logger.error(
// `Error sending disconnect to offline newt ${offlineClient.clientId}`,
// { error }
// );
// }
// }
// } catch (error) {
// logger.error("Error in offline checker interval", { error });
// }
// }, OFFLINE_CHECK_INTERVAL);
// logger.debug("Started offline checker interval");
// };
/**
* Stops the background interval that checks for offline clients
*/
// export const stopNewtOfflineChecker = (): void => {
// if (offlineCheckerInterval) {
// clearInterval(offlineCheckerInterval);
// offlineCheckerInterval = null;
// logger.info("Stopped offline checker interval");
// }
// };
/**
* Handles ping messages from clients and responds with pong
*/
export const handleNewtPingMessage: MessageHandler = async (context) => {
const { message, client: c, sendToClient } = context;
const newt = c as Newt;
if (!newt) {
logger.warn("Newt ping message: Newt not found");
return;
}
if (!newt.siteId) {
logger.warn("Newt ping message: has no site ID");
return;
}
// get the version
const configVersion = await getClientConfigVersion(newt.newtId);
if (message.configVersion && configVersion != null && configVersion != message.configVersion) {
logger.warn(
`Newt ping with outdated config version: ${message.configVersion} (current: ${configVersion})`
);
// get the site
const [site] = await db
.select()
.from(sites)
.where(eq(sites.siteId, newt.siteId))
.limit(1);
if (!site) {
logger.warn(
`Newt ping message: site with ID ${newt.siteId} not found`
);
return;
}
await sendNewtSyncMessage(newt, site);
}
// try {
// // Update the client's last ping timestamp
// await db
// .update(clients)
// .set({
// lastPing: Math.floor(Date.now() / 1000),
// online: true
// })
// .where(eq(clients.clientId, newt.clientId));
// } catch (error) {
// logger.error("Error handling ping message", { error });
// }
return {
message: {
type: "pong",
data: {
timestamp: new Date().toISOString()
}
},
broadcast: false,
excludeSender: false
};
};

View File

@@ -18,7 +18,6 @@ import {
} from "#dynamic/lib/exitNodes";
import { fetchContainers } from "./dockerSocket";
import { lockManager } from "#dynamic/lib/lock";
import { buildTargetConfigurationForNewtClient } from "./buildConfiguration";
export type ExitNodePingResult = {
exitNodeId: number;
@@ -234,8 +233,109 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
.where(eq(newts.newtId, newt.newtId));
}
const { tcpTargets, udpTargets, validHealthCheckTargets } =
await buildTargetConfigurationForNewtClient(siteId);
// Get all enabled targets with their resource protocol information
const allTargets = await db
.select({
resourceId: targets.resourceId,
targetId: targets.targetId,
ip: targets.ip,
method: targets.method,
port: targets.port,
internalPort: targets.internalPort,
enabled: targets.enabled,
protocol: resources.protocol,
hcEnabled: targetHealthCheck.hcEnabled,
hcPath: targetHealthCheck.hcPath,
hcScheme: targetHealthCheck.hcScheme,
hcMode: targetHealthCheck.hcMode,
hcHostname: targetHealthCheck.hcHostname,
hcPort: targetHealthCheck.hcPort,
hcInterval: targetHealthCheck.hcInterval,
hcUnhealthyInterval: targetHealthCheck.hcUnhealthyInterval,
hcTimeout: targetHealthCheck.hcTimeout,
hcHeaders: targetHealthCheck.hcHeaders,
hcMethod: targetHealthCheck.hcMethod,
hcTlsServerName: targetHealthCheck.hcTlsServerName
})
.from(targets)
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
.leftJoin(
targetHealthCheck,
eq(targets.targetId, targetHealthCheck.targetId)
)
.where(and(eq(targets.siteId, siteId), eq(targets.enabled, true)));
const { tcpTargets, udpTargets } = allTargets.reduce(
(acc, target) => {
// Filter out invalid targets
if (!target.internalPort || !target.ip || !target.port) {
return acc;
}
// Format target into string
const formattedTarget = `${target.internalPort}:${target.ip}:${target.port}`;
// Add to the appropriate protocol array
if (target.protocol === "tcp") {
acc.tcpTargets.push(formattedTarget);
} else {
acc.udpTargets.push(formattedTarget);
}
return acc;
},
{ tcpTargets: [] as string[], udpTargets: [] as string[] }
);
const healthCheckTargets = allTargets.map((target) => {
// make sure the stuff is defined
if (
!target.hcPath ||
!target.hcHostname ||
!target.hcPort ||
!target.hcInterval ||
!target.hcMethod
) {
logger.debug(
`Skipping target ${target.targetId} due to missing health check fields`
);
return null; // Skip targets with missing health check fields
}
// parse headers
const hcHeadersParse = target.hcHeaders
? JSON.parse(target.hcHeaders)
: null;
const hcHeadersSend: { [key: string]: string } = {};
if (hcHeadersParse) {
hcHeadersParse.forEach(
(header: { name: string; value: string }) => {
hcHeadersSend[header.name] = header.value;
}
);
}
return {
id: target.targetId,
hcEnabled: target.hcEnabled,
hcPath: target.hcPath,
hcScheme: target.hcScheme,
hcMode: target.hcMode,
hcHostname: target.hcHostname,
hcPort: target.hcPort,
hcInterval: target.hcInterval, // in seconds
hcUnhealthyInterval: target.hcUnhealthyInterval, // in seconds
hcTimeout: target.hcTimeout, // in seconds
hcHeaders: hcHeadersSend,
hcMethod: target.hcMethod,
hcTlsServerName: target.hcTlsServerName
};
});
// Filter out any null values from health check targets
const validHealthCheckTargets = healthCheckTargets.filter(
(target) => target !== null
);
logger.debug(
`Sending health check targets to newt ${newt.newtId}: ${JSON.stringify(validHealthCheckTargets)}`

View File

@@ -6,4 +6,3 @@ export * from "./handleGetConfigMessage";
export * from "./handleSocketMessages";
export * from "./handleNewtPingRequestMessage";
export * from "./handleApplyBlueprintMessage";
export * from "./handleNewtPingMessage";

View File

@@ -39,7 +39,7 @@ export async function addPeer(
await sendToClient(newtId, {
type: "newt/wg/peer/add",
data: peer
}, { incrementConfigVersion: true }).catch((error) => {
}).catch((error) => {
logger.warn(`Error sending message:`, error);
});
@@ -81,7 +81,7 @@ export async function deletePeer(
data: {
publicKey
}
}, { incrementConfigVersion: true }).catch((error) => {
}).catch((error) => {
logger.warn(`Error sending message:`, error);
});
@@ -128,7 +128,7 @@ export async function updatePeer(
publicKey,
...peer
}
}, { incrementConfigVersion: true }).catch((error) => {
}).catch((error) => {
logger.warn(`Error sending message:`, error);
});

View File

@@ -1,41 +0,0 @@
import { ExitNode, exitNodes, Newt, Site, db } from "@server/db";
import { eq } from "drizzle-orm";
import { sendToClient } from "#dynamic/routers/ws";
import logger from "@server/logger";
import {
buildClientConfigurationForNewtClient,
buildTargetConfigurationForNewtClient
} from "./buildConfiguration";
export async function sendNewtSyncMessage(newt: Newt, site: Site) {
const { tcpTargets, udpTargets, validHealthCheckTargets } =
await buildTargetConfigurationForNewtClient(site.siteId);
let exitNode: ExitNode | undefined;
if (site.exitNodeId) {
[exitNode] = await db
.select()
.from(exitNodes)
.where(eq(exitNodes.exitNodeId, site.exitNodeId))
.limit(1);
}
const { peers, targets } = await buildClientConfigurationForNewtClient(
site,
exitNode
);
await sendToClient(newt.newtId, {
type: "newt/sync",
data: {
proxyTargets: {
udp: udpTargets,
tcp: tcpTargets
},
healthCheckTargets: validHealthCheckTargets,
peers: peers,
clientTargets: targets
}
}).catch((error) => {
logger.warn(`Error sending newt sync message:`, error);
});
}

View File

@@ -22,7 +22,7 @@ export async function addTargets(
data: {
targets: payloadTargets
}
}, { incrementConfigVersion: true });
});
// Create a map for quick lookup
const healthCheckMap = new Map<number, TargetHealthCheck>();
@@ -103,7 +103,7 @@ export async function addTargets(
data: {
targets: validHealthCheckTargets
}
}, { incrementConfigVersion: true });
});
}
export async function removeTargets(
@@ -124,7 +124,7 @@ export async function removeTargets(
data: {
targets: payloadTargets
}
}, { incrementConfigVersion: true });
});
const healthCheckTargets = targets.map((target) => {
return target.targetId;
@@ -135,5 +135,5 @@ export async function removeTargets(
data: {
ids: healthCheckTargets
}
}, { incrementConfigVersion: true });
});
}

View File

@@ -1,81 +0,0 @@
import { NextFunction, Request, Response } from "express";
import { db } from "@server/db";
import { olms, clients } from "@server/db";
import { eq } from "drizzle-orm";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import response from "@server/lib/response";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
import { sendTerminateClient } from "../client/terminate";
const paramsSchema = z
.object({
userId: z.string(),
olmId: z.string()
})
.strict();
export async function archiveUserOlm(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { olmId } = parsedParams.data;
// Archive the OLM and disconnect associated clients in a transaction
await db.transaction(async (trx) => {
// Find all clients associated with this OLM
const associatedClients = await trx
.select()
.from(clients)
.where(eq(clients.olmId, olmId));
// Disconnect clients from the OLM (set olmId to null)
for (const client of associatedClients) {
await trx
.update(clients)
.set({ olmId: null })
.where(eq(clients.clientId, client.clientId));
await rebuildClientAssociationsFromClient(client, trx);
await sendTerminateClient(client.clientId, olmId);
}
// Archive the OLM (set archived to true)
await trx
.update(olms)
.set({ archived: true })
.where(eq(olms.olmId, olmId));
});
return response(res, {
data: null,
success: true,
error: false,
message: "Device archived successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to archive device"
)
);
}
}

View File

@@ -1,145 +0,0 @@
import { Client, clientSiteResourcesAssociationsCache, clientSitesAssociationsCache, db, exitNodes, siteResources, sites } from "@server/db";
import { generateAliasConfig, generateRemoteSubnets } from "@server/lib/ip";
import logger from "@server/logger";
import { and, eq } from "drizzle-orm";
import { addPeer, deletePeer } from "../newt/peers";
import config from "@server/lib/config";
export async function buildSiteConfigurationForOlmClient(
client: Client,
publicKey: string | null,
relay: boolean
) {
const siteConfigurations = [];
// Get all sites data
const sitesData = await db
.select()
.from(sites)
.innerJoin(
clientSitesAssociationsCache,
eq(sites.siteId, clientSitesAssociationsCache.siteId)
)
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
// Process each site
for (const {
sites: site,
clientSitesAssociationsCache: association
} of sitesData) {
if (!site.exitNodeId) {
logger.warn(
`Site ${site.siteId} does not have exit node, skipping`
);
continue;
}
// Validate endpoint and hole punch status
if (!site.endpoint) {
logger.warn(
`In olm register: site ${site.siteId} has no endpoint, skipping`
);
continue;
}
// if (site.lastHolePunch && now - site.lastHolePunch > 6 && relay) {
// logger.warn(
// `Site ${site.siteId} last hole punch is too old, skipping`
// );
// continue;
// }
// If public key changed, delete old peer from this site
if (client.pubKey && client.pubKey != publicKey) {
logger.info(
`Public key mismatch. Deleting old peer from site ${site.siteId}...`
);
await deletePeer(site.siteId, client.pubKey!);
}
if (!site.subnet) {
logger.warn(`Site ${site.siteId} has no subnet, skipping`);
continue;
}
const [clientSite] = await db
.select()
.from(clientSitesAssociationsCache)
.where(
and(
eq(clientSitesAssociationsCache.clientId, client.clientId),
eq(clientSitesAssociationsCache.siteId, site.siteId)
)
)
.limit(1);
// Add the peer to the exit node for this site
if (clientSite.endpoint && publicKey) {
logger.info(
`Adding peer ${publicKey} to site ${site.siteId} with endpoint ${clientSite.endpoint}`
);
await addPeer(site.siteId, {
publicKey: publicKey,
allowedIps: [`${client.subnet.split("/")[0]}/32`], // we want to only allow from that client
endpoint: relay ? "" : clientSite.endpoint
});
} else {
logger.warn(
`Client ${client.clientId} has no endpoint, skipping peer addition`
);
}
let relayEndpoint: string | undefined = undefined;
if (relay) {
const [exitNode] = await db
.select()
.from(exitNodes)
.where(eq(exitNodes.exitNodeId, site.exitNodeId))
.limit(1);
if (!exitNode) {
logger.warn(`Exit node not found for site ${site.siteId}`);
continue;
}
relayEndpoint = `${exitNode.endpoint}:${config.getRawConfig().gerbil.clients_start_port}`;
}
const allSiteResources = await db // only get the site resources that this client has access to
.select()
.from(siteResources)
.innerJoin(
clientSiteResourcesAssociationsCache,
eq(
siteResources.siteResourceId,
clientSiteResourcesAssociationsCache.siteResourceId
)
)
.where(
and(
eq(siteResources.siteId, site.siteId),
eq(
clientSiteResourcesAssociationsCache.clientId,
client.clientId
)
)
);
// Add site configuration to the array
siteConfigurations.push({
siteId: site.siteId,
name: site.name,
// relayEndpoint: relayEndpoint, // this can be undefined now if not relayed // lets not do this for now because it would conflict with the hole punch testing
endpoint: site.endpoint,
publicKey: site.publicKey,
serverIP: site.address,
serverPort: site.listenPort,
remoteSubnets: generateRemoteSubnets(
allSiteResources.map(({ siteResources }) => siteResources)
),
aliases: generateAliasConfig(
allSiteResources.map(({ siteResources }) => siteResources)
)
});
}
return siteConfigurations;
}

View File

@@ -1,6 +1,6 @@
import { NextFunction, Request, Response } from "express";
import { db } from "@server/db";
import { olms, clients, fingerprints } from "@server/db";
import { olms } from "@server/db";
import { eq, and } from "drizzle-orm";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
@@ -9,7 +9,6 @@ import { z } from "zod";
import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi";
import { getUserDeviceName } from "@server/db/names";
const paramsSchema = z
.object({
@@ -18,10 +17,6 @@ const paramsSchema = z
})
.strict();
const querySchema = z.object({
orgId: z.string().optional()
});
// registry.registerPath({
// method: "get",
// path: "/user/{userId}/olm/{olmId}",
@@ -49,64 +44,15 @@ export async function getUserOlm(
);
}
const parsedQuery = querySchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error).toString()
)
);
}
const { olmId, userId } = parsedParams.data;
const { orgId } = parsedQuery.data;
const [result] = await db
const [olm] = await db
.select()
.from(olms)
.where(and(eq(olms.userId, userId), eq(olms.olmId, olmId)))
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
.limit(1);
if (!result || !result.olms) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Olm not found"
)
);
}
const olm = result.olms;
// If orgId is provided and olm has a clientId, fetch the client to check blocked status
let blocked: boolean | undefined;
if (orgId && olm.clientId) {
const [client] = await db
.select({ blocked: clients.blocked })
.from(clients)
.where(
and(
eq(clients.clientId, olm.clientId),
eq(clients.orgId, orgId)
)
)
.limit(1);
blocked = client?.blocked ?? false;
}
// Replace name with device name
const model = result.fingerprints?.deviceModel || null;
const newName = getUserDeviceName(model, olm.name);
const responseData = blocked !== undefined
? { ...olm, name: newName, blocked }
: { ...olm, name: newName };
.where(and(eq(olms.userId, userId), eq(olms.olmId, olmId)));
return response(res, {
data: responseData,
data: olm,
success: true,
error: false,
message: "Successfully retrieved olm",

View File

@@ -1,7 +1,7 @@
import { disconnectClient, getClientConfigVersion } from "#dynamic/routers/ws";
import { clientPostureSnapshots, db, fingerprints } from "@server/db";
import { db } from "@server/db";
import { disconnectClient } from "#dynamic/routers/ws";
import { MessageHandler } from "@server/routers/ws";
import { clients, olms, Olm } from "@server/db";
import { clients, Olm } from "@server/db";
import { eq, lt, isNull, and, or } from "drizzle-orm";
import logger from "@server/logger";
import { validateSessionToken } from "@server/auth/sessions/app";
@@ -9,7 +9,6 @@ import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { sendTerminateClient } from "../client/terminate";
import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
import { sendOlmSyncMessage } from "./sync";
// Track if the offline checker interval is running
let offlineCheckerInterval: NodeJS.Timeout | null = null;
@@ -102,178 +101,79 @@ export const handleOlmPingMessage: MessageHandler = async (context) => {
const { message, client: c, sendToClient } = context;
const olm = c as Olm;
const { userToken, fingerprint, postures } = message.data;
const { userToken } = message.data;
if (!olm) {
logger.warn("Olm not found");
return;
}
if (olm.userId) {
// we need to check a user token to make sure its still valid
const { session: userSession, user } =
await validateSessionToken(userToken);
if (!userSession || !user) {
logger.warn("Invalid user session for olm ping");
return; // by returning here we just ignore the ping and the setInterval will force it to disconnect
}
if (user.userId !== olm.userId) {
logger.warn("User ID mismatch for olm ping");
return;
}
// get the client
const [client] = await db
.select()
.from(clients)
.where(
and(
eq(clients.olmId, olm.olmId),
eq(clients.userId, olm.userId)
)
)
.limit(1);
if (!client) {
logger.warn("Client not found for olm ping");
return;
}
const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(userToken))
);
const policyCheck = await checkOrgAccessPolicy({
orgId: client.orgId,
userId: olm.userId,
sessionId // this is the user token passed in the message
});
if (!policyCheck.allowed) {
logger.warn(
`Olm user ${olm.userId} does not pass access policies for org ${client.orgId}: ${policyCheck.error}`
);
return;
}
}
if (!olm.clientId) {
logger.warn("Olm has no client ID!");
return;
}
try {
// get the client
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, olm.clientId))
.limit(1);
if (!client) {
logger.warn("Client not found for olm ping");
return;
}
if (client.blocked) {
// NOTE: by returning we dont update the lastPing, so the offline checker will eventually disconnect them
logger.debug(
`Blocked client ${client.clientId} attempted olm ping`
);
return;
}
if (olm.userId) {
// we need to check a user token to make sure its still valid
const { session: userSession, user } =
await validateSessionToken(userToken);
if (!userSession || !user) {
logger.warn("Invalid user session for olm ping");
return; // by returning here we just ignore the ping and the setInterval will force it to disconnect
}
if (user.userId !== olm.userId) {
logger.warn("User ID mismatch for olm ping");
return;
}
if (user.userId !== client.userId) {
logger.warn("Client user ID mismatch for olm ping");
return;
}
const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(userToken))
);
const policyCheck = await checkOrgAccessPolicy({
orgId: client.orgId,
userId: olm.userId,
sessionId // this is the user token passed in the message
});
if (!policyCheck.allowed) {
logger.warn(
`Olm user ${olm.userId} does not pass access policies for org ${client.orgId}: ${policyCheck.error}`
);
return;
}
}
// get the version
logger.debug(`handleOlmPingMessage: About to get config version for olmId: ${olm.olmId}`);
const configVersion = await getClientConfigVersion(olm.olmId);
logger.debug(`handleOlmPingMessage: Got config version: ${configVersion} (type: ${typeof configVersion})`);
if (configVersion == null || configVersion === undefined) {
logger.debug(`handleOlmPingMessage: could not get config version from server for olmId: ${olm.olmId}`)
}
if (message.configVersion != null && configVersion != null && configVersion != message.configVersion) {
logger.debug(
`handleOlmPingMessage: Olm ping with outdated config version: ${message.configVersion} (current: ${configVersion})`
);
await sendOlmSyncMessage(olm, client);
}
// Update the client's last ping timestamp
await db
.update(clients)
.set({
lastPing: Math.floor(Date.now() / 1000),
online: true,
archived: false
online: true
})
.where(eq(clients.clientId, olm.clientId));
if (olm.archived) {
await db
.update(olms)
.set({ archived: false })
.where(eq(olms.olmId, olm.olmId));
}
} catch (error) {
logger.error("Error handling ping message", { error });
}
const now = Math.floor(Date.now() / 1000);
if (fingerprint && olm.olmId) {
const [existingFingerprint] = await db
.select()
.from(fingerprints)
.where(eq(fingerprints.olmId, olm.olmId))
.limit(1);
if (!existingFingerprint) {
await db.insert(fingerprints).values({
olmId: olm.olmId,
firstSeen: now,
lastSeen: now,
username: fingerprint.username,
hostname: fingerprint.hostname,
platform: fingerprint.platform,
osVersion: fingerprint.osVersion,
kernelVersion: fingerprint.kernelVersion,
arch: fingerprint.arch,
deviceModel: fingerprint.deviceModel,
serialNumber: fingerprint.serialNumber,
platformFingerprint: fingerprint.platformFingerprint
});
} else {
await db
.update(fingerprints)
.set({
lastSeen: now,
username: fingerprint.username,
hostname: fingerprint.hostname,
platform: fingerprint.platform,
osVersion: fingerprint.osVersion,
kernelVersion: fingerprint.kernelVersion,
arch: fingerprint.arch,
deviceModel: fingerprint.deviceModel,
serialNumber: fingerprint.serialNumber,
platformFingerprint: fingerprint.platformFingerprint
})
.where(eq(fingerprints.olmId, olm.olmId));
}
}
if (postures && olm.clientId) {
await db.insert(clientPostureSnapshots).values({
clientId: olm.clientId,
biometricsEnabled: postures?.biometricsEnabled,
diskEncrypted: postures?.diskEncrypted,
firewallEnabled: postures?.firewallEnabled,
autoUpdatesEnabled: postures?.autoUpdatesEnabled,
tpmAvailable: postures?.tpmAvailable,
windowsDefenderEnabled: postures?.windowsDefenderEnabled,
macosSipEnabled: postures?.macosSipEnabled,
macosGatekeeperEnabled: postures?.macosGatekeeperEnabled,
macosFirewallStealthMode: postures?.macosFirewallStealthMode,
linuxAppArmorEnabled: postures?.linuxAppArmorEnabled,
linuxSELinuxEnabled: postures?.linuxSELinuxEnabled,
collectedAt: now
});
}
return {
message: {
type: "pong",

View File

@@ -1,9 +1,6 @@
import {
Client,
clientPostureSnapshots,
clientSiteResourcesAssociationsCache,
db,
fingerprints,
orgs,
siteResources
} from "@server/db";
@@ -16,7 +13,7 @@ import {
olms,
sites
} from "@server/db";
import { and, count, eq, inArray, isNull } from "drizzle-orm";
import { and, eq, inArray, isNull } from "drizzle-orm";
import { addPeer, deletePeer } from "../newt/peers";
import logger from "@server/logger";
import { generateAliasConfig } from "@server/lib/ip";
@@ -26,7 +23,6 @@ import { validateSessionToken } from "@server/auth/sessions/app";
import config from "@server/lib/config";
import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
import { buildSiteConfigurationForOlmClient } from "./buildConfiguration";
export const handleOlmRegisterMessage: MessageHandler = async (context) => {
logger.info("Handling register olm message!");
@@ -40,16 +36,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
return;
}
const {
publicKey,
relay,
olmVersion,
olmAgent,
orgId,
userToken,
fingerprint,
postures
} = message.data;
const { publicKey, relay, olmVersion, olmAgent, orgId, userToken } =
message.data;
if (!olm.clientId) {
logger.warn("Olm client ID not found");
@@ -67,11 +55,6 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
return;
}
if (client.blocked) {
logger.debug(`Client ${client.clientId} is blocked. Ignoring register.`);
return;
}
const [org] = await db
.select()
.from(orgs)
@@ -129,20 +112,18 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
if (
(olmVersion && olm.version !== olmVersion) ||
(olmAgent && olm.agent !== olmAgent) ||
olm.archived
(olmAgent && olm.agent !== olmAgent)
) {
await db
.update(olms)
.set({
version: olmVersion,
agent: olmAgent,
archived: false
agent: olmAgent
})
.where(eq(olms.olmId, olm.olmId));
}
if (client.pubKey !== publicKey || client.archived) {
if (client.pubKey !== publicKey) {
logger.info(
"Public key mismatch. Updating public key and clearing session info..."
);
@@ -150,8 +131,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
await db
.update(clients)
.set({
pubKey: publicKey,
archived: false,
pubKey: publicKey
})
.where(eq(clients.clientId, client.clientId));
@@ -165,8 +145,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
}
// Get all sites data
const sitesCountResult = await db
.select({ count: count() })
const sitesData = await db
.select()
.from(sites)
.innerJoin(
clientSitesAssociationsCache,
@@ -174,93 +154,138 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
)
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
// Extract the count value from the result array
const sitesCount =
sitesCountResult.length > 0 ? sitesCountResult[0].count : 0;
// Prepare an array to store site configurations
logger.debug(`Found ${sitesCount} sites for client ${client.clientId}`);
const siteConfigurations = [];
logger.debug(
`Found ${sitesData.length} sites for client ${client.clientId}`
);
// this prevents us from accepting a register from an olm that has not hole punched yet.
// the olm will pump the register so we can keep checking
// TODO: I still think there is a better way to do this rather than locking it out here but ???
if (now - (client.lastHolePunch || 0) > 5 && sitesCount > 0) {
if (now - (client.lastHolePunch || 0) > 5 && sitesData.length > 0) {
logger.warn(
"Client last hole punch is too old and we have sites to send; skipping this register"
);
return;
}
// NOTE: its important that the client here is the old client and the public key is the new key
const siteConfigurations = await buildSiteConfigurationForOlmClient(
client,
publicKey,
relay
);
// Process each site
for (const {
sites: site,
clientSitesAssociationsCache: association
} of sitesData) {
if (!site.exitNodeId) {
logger.warn(
`Site ${site.siteId} does not have exit node, skipping`
);
continue;
}
if (fingerprint) {
const [existingFingerprint] = await db
// Validate endpoint and hole punch status
if (!site.endpoint) {
logger.warn(
`In olm register: site ${site.siteId} has no endpoint, skipping`
);
continue;
}
// if (site.lastHolePunch && now - site.lastHolePunch > 6 && relay) {
// logger.warn(
// `Site ${site.siteId} last hole punch is too old, skipping`
// );
// continue;
// }
// If public key changed, delete old peer from this site
if (client.pubKey && client.pubKey != publicKey) {
logger.info(
`Public key mismatch. Deleting old peer from site ${site.siteId}...`
);
await deletePeer(site.siteId, client.pubKey!);
}
if (!site.subnet) {
logger.warn(`Site ${site.siteId} has no subnet, skipping`);
continue;
}
const [clientSite] = await db
.select()
.from(fingerprints)
.where(eq(fingerprints.olmId, olm.olmId))
.from(clientSitesAssociationsCache)
.where(
and(
eq(clientSitesAssociationsCache.clientId, client.clientId),
eq(clientSitesAssociationsCache.siteId, site.siteId)
)
)
.limit(1);
if (!existingFingerprint) {
await db.insert(fingerprints).values({
olmId: olm.olmId,
firstSeen: now,
lastSeen: now,
username: fingerprint.username,
hostname: fingerprint.hostname,
platform: fingerprint.platform,
osVersion: fingerprint.osVersion,
kernelVersion: fingerprint.kernelVersion,
arch: fingerprint.arch,
deviceModel: fingerprint.deviceModel,
serialNumber: fingerprint.serialNumber,
platformFingerprint: fingerprint.platformFingerprint
// Add the peer to the exit node for this site
if (clientSite.endpoint) {
logger.info(
`Adding peer ${publicKey} to site ${site.siteId} with endpoint ${clientSite.endpoint}`
);
await addPeer(site.siteId, {
publicKey: publicKey,
allowedIps: [`${client.subnet.split("/")[0]}/32`], // we want to only allow from that client
endpoint: relay ? "" : clientSite.endpoint
});
} else {
await db
.update(fingerprints)
.set({
lastSeen: now,
username: fingerprint.username,
hostname: fingerprint.hostname,
platform: fingerprint.platform,
osVersion: fingerprint.osVersion,
kernelVersion: fingerprint.kernelVersion,
arch: fingerprint.arch,
deviceModel: fingerprint.deviceModel,
serialNumber: fingerprint.serialNumber,
platformFingerprint: fingerprint.platformFingerprint
})
.where(eq(fingerprints.olmId, olm.olmId));
logger.warn(
`Client ${client.clientId} has no endpoint, skipping peer addition`
);
}
}
if (postures && olm.clientId) {
await db.insert(clientPostureSnapshots).values({
clientId: olm.clientId,
let relayEndpoint: string | undefined = undefined;
if (relay) {
const [exitNode] = await db
.select()
.from(exitNodes)
.where(eq(exitNodes.exitNodeId, site.exitNodeId))
.limit(1);
if (!exitNode) {
logger.warn(`Exit node not found for site ${site.siteId}`);
continue;
}
relayEndpoint = `${exitNode.endpoint}:${config.getRawConfig().gerbil.clients_start_port}`;
}
biometricsEnabled: postures?.biometricsEnabled,
diskEncrypted: postures?.diskEncrypted,
firewallEnabled: postures?.firewallEnabled,
autoUpdatesEnabled: postures?.autoUpdatesEnabled,
tpmAvailable: postures?.tpmAvailable,
const allSiteResources = await db // only get the site resources that this client has access to
.select()
.from(siteResources)
.innerJoin(
clientSiteResourcesAssociationsCache,
eq(
siteResources.siteResourceId,
clientSiteResourcesAssociationsCache.siteResourceId
)
)
.where(
and(
eq(siteResources.siteId, site.siteId),
eq(
clientSiteResourcesAssociationsCache.clientId,
client.clientId
)
)
);
windowsDefenderEnabled: postures?.windowsDefenderEnabled,
macosSipEnabled: postures?.macosSipEnabled,
macosGatekeeperEnabled: postures?.macosGatekeeperEnabled,
macosFirewallStealthMode: postures?.macosFirewallStealthMode,
linuxAppArmorEnabled: postures?.linuxAppArmorEnabled,
linuxSELinuxEnabled: postures?.linuxSELinuxEnabled,
collectedAt: now
// Add site configuration to the array
siteConfigurations.push({
siteId: site.siteId,
name: site.name,
// relayEndpoint: relayEndpoint, // this can be undefined now if not relayed // lets not do this for now because it would conflict with the hole punch testing
endpoint: site.endpoint,
publicKey: site.publicKey,
serverIP: site.address,
serverPort: site.listenPort,
remoteSubnets: generateRemoteSubnets(
allSiteResources.map(({ siteResources }) => siteResources)
),
aliases: generateAliasConfig(
allSiteResources.map(({ siteResources }) => siteResources)
)
});
}

View File

@@ -3,10 +3,9 @@ export * from "./getOlmToken";
export * from "./createUserOlm";
export * from "./handleOlmRelayMessage";
export * from "./handleOlmPingMessage";
export * from "./archiveUserOlm";
export * from "./unarchiveUserOlm";
export * from "./deleteUserOlm";
export * from "./listUserOlms";
export * from "./deleteUserOlm";
export * from "./getUserOlm";
export * from "./handleOlmServerPeerAddMessage";
export * from "./handleOlmUnRelayMessage";
export * from "./recoverOlmWithFingerprint";

View File

@@ -1,5 +1,5 @@
import { NextFunction, Request, Response } from "express";
import { db, fingerprints } from "@server/db";
import { db } from "@server/db";
import { olms } from "@server/db";
import { eq, count, desc } from "drizzle-orm";
import HttpCode from "@server/types/HttpCode";
@@ -9,7 +9,6 @@ import { z } from "zod";
import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi";
import { getUserDeviceName } from "@server/db/names";
const querySchema = z.object({
limit: z
@@ -52,7 +51,6 @@ export type ListUserOlmsResponse = {
name: string | null;
clientId: number | null;
userId: string | null;
archived: boolean;
}>;
pagination: {
total: number;
@@ -91,7 +89,7 @@ export async function listUserOlms(
const { userId } = parsedParams.data;
// Get total count (including archived OLMs)
// Get total count
const [totalCountResult] = await db
.select({ count: count() })
.from(olms)
@@ -99,31 +97,22 @@ export async function listUserOlms(
const total = totalCountResult?.count || 0;
// Get OLMs for the current user (including archived OLMs)
const list = await db
.select()
// Get OLMs for the current user
const userOlms = await db
.select({
olmId: olms.olmId,
dateCreated: olms.dateCreated,
version: olms.version,
name: olms.name,
clientId: olms.clientId,
userId: olms.userId
})
.from(olms)
.where(eq(olms.userId, userId))
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
.orderBy(desc(olms.dateCreated))
.limit(limit)
.offset(offset);
const userOlms = list.map((item) => {
const model = item.fingerprints?.deviceModel || null;
const newName = getUserDeviceName(model, item.olms.name);
return {
olmId: item.olms.olmId,
dateCreated: item.olms.dateCreated,
version: item.olms.version,
name: newName,
clientId: item.olms.clientId,
userId: item.olms.userId,
archived: item.olms.archived
};
});
return response<ListUserOlmsResponse>(res, {
data: {
olms: userOlms,

View File

@@ -32,24 +32,20 @@ export async function addPeer(
olmId = olm.olmId;
}
await sendToClient(
olmId,
{
type: "olm/wg/peer/add",
data: {
siteId: peer.siteId,
name: peer.name,
publicKey: peer.publicKey,
endpoint: peer.endpoint,
relayEndpoint: peer.relayEndpoint,
serverIP: peer.serverIP,
serverPort: peer.serverPort,
remoteSubnets: peer.remoteSubnets, // optional, comma-separated list of subnets that this site can access
aliases: peer.aliases
}
},
{ incrementConfigVersion: true }
).catch((error) => {
await sendToClient(olmId, {
type: "olm/wg/peer/add",
data: {
siteId: peer.siteId,
name: peer.name,
publicKey: peer.publicKey,
endpoint: peer.endpoint,
relayEndpoint: peer.relayEndpoint,
serverIP: peer.serverIP,
serverPort: peer.serverPort,
remoteSubnets: peer.remoteSubnets, // optional, comma-separated list of subnets that this site can access
aliases: peer.aliases
}
}).catch((error) => {
logger.warn(`Error sending message:`, error);
});
@@ -74,17 +70,13 @@ export async function deletePeer(
olmId = olm.olmId;
}
await sendToClient(
olmId,
{
type: "olm/wg/peer/remove",
data: {
publicKey,
siteId: siteId
}
},
{ incrementConfigVersion: true }
).catch((error) => {
await sendToClient(olmId, {
type: "olm/wg/peer/remove",
data: {
publicKey,
siteId: siteId
}
}).catch((error) => {
logger.warn(`Error sending message:`, error);
});
@@ -117,23 +109,19 @@ export async function updatePeer(
olmId = olm.olmId;
}
await sendToClient(
olmId,
{
type: "olm/wg/peer/update",
data: {
siteId: peer.siteId,
publicKey: peer.publicKey,
endpoint: peer.endpoint,
relayEndpoint: peer.relayEndpoint,
serverIP: peer.serverIP,
serverPort: peer.serverPort,
remoteSubnets: peer.remoteSubnets,
aliases: peer.aliases
}
},
{ incrementConfigVersion: true }
).catch((error) => {
await sendToClient(olmId, {
type: "olm/wg/peer/update",
data: {
siteId: peer.siteId,
publicKey: peer.publicKey,
endpoint: peer.endpoint,
relayEndpoint: peer.relayEndpoint,
serverIP: peer.serverIP,
serverPort: peer.serverPort,
remoteSubnets: peer.remoteSubnets,
aliases: peer.aliases
}
}).catch((error) => {
logger.warn(`Error sending message:`, error);
});
@@ -163,21 +151,17 @@ export async function initPeerAddHandshake(
olmId = olm.olmId;
}
await sendToClient(
olmId,
{
type: "olm/wg/peer/holepunch/site/add",
data: {
siteId: peer.siteId,
exitNode: {
publicKey: peer.exitNode.publicKey,
relayPort: config.getRawConfig().gerbil.clients_start_port,
endpoint: peer.exitNode.endpoint
}
await sendToClient(olmId, {
type: "olm/wg/peer/holepunch/site/add",
data: {
siteId: peer.siteId,
exitNode: {
publicKey: peer.exitNode.publicKey,
relayPort: config.getRawConfig().gerbil.clients_start_port,
endpoint: peer.exitNode.endpoint
}
},
{ incrementConfigVersion: true }
).catch((error) => {
}
}).catch((error) => {
logger.warn(`Error sending message:`, error);
});

View File

@@ -1,120 +0,0 @@
import { db, fingerprints, olms } from "@server/db";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import { and, eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import response from "@server/lib/response";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import { generateId } from "@server/auth/sessions/app";
import { hashPassword } from "@server/auth/password";
const paramsSchema = z
.object({
userId: z.string()
})
.strict();
const bodySchema = z
.object({
platformFingerprint: z.string()
})
.strict();
export async function recoverOlmWithFingerprint(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { userId } = parsedParams.data;
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { platformFingerprint } = parsedBody.data;
const result = await db
.select({
olm: olms,
fingerprint: fingerprints
})
.from(olms)
.innerJoin(fingerprints, eq(fingerprints.olmId, olms.olmId))
.where(
and(
eq(olms.userId, userId),
eq(olms.archived, false),
eq(fingerprints.platformFingerprint, platformFingerprint)
)
)
.orderBy(fingerprints.lastSeen);
if (!result || result.length == 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"corresponding olm with this fingerprint not found"
)
);
}
if (result.length > 1) {
return next(
createHttpError(
HttpCode.CONFLICT,
"multiple matching fingerprints found, not resetting secrets"
)
);
}
const [{ olm: foundOlm }] = result;
const newSecret = generateId(48);
const newSecretHash = await hashPassword(newSecret);
await db
.update(olms)
.set({
secretHash: newSecretHash
})
.where(eq(olms.olmId, foundOlm.olmId));
return response(res, {
data: {
olmId: foundOlm.olmId,
secret: newSecret
},
success: true,
error: false,
message: "Successfully retrieved olm",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to recover olm using provided fingerprint input"
)
);
}
}

View File

@@ -1,80 +0,0 @@
import { Client, db, exitNodes, Olm, sites, clientSitesAssociationsCache } from "@server/db";
import { buildSiteConfigurationForOlmClient } from "./buildConfiguration";
import { sendToClient } from "#dynamic/routers/ws";
import logger from "@server/logger";
import { eq, inArray } from "drizzle-orm";
import config from "@server/lib/config";
export async function sendOlmSyncMessage(olm: Olm, client: Client) {
// NOTE: WE ARE HARDCODING THE RELAY PARAMETER TO FALSE HERE BUT IN THE REGISTER MESSAGE ITS DEFINED BY THE CLIENT
const siteConfigurations = await buildSiteConfigurationForOlmClient(
client,
client.pubKey,
false
);
// Get all exit nodes from sites where the client has peers
const clientSites = await db
.select()
.from(clientSitesAssociationsCache)
.innerJoin(
sites,
eq(sites.siteId, clientSitesAssociationsCache.siteId)
)
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
// Extract unique exit node IDs
const exitNodeIds = Array.from(
new Set(
clientSites
.map(({ sites: site }) => site.exitNodeId)
.filter((id): id is number => id !== null)
)
);
let exitNodesData: {
publicKey: string;
relayPort: number;
endpoint: string;
siteIds: number[];
}[] = [];
if (exitNodeIds.length > 0) {
const allExitNodes = await db
.select()
.from(exitNodes)
.where(inArray(exitNodes.exitNodeId, exitNodeIds));
// Map exitNodeId to siteIds
const exitNodeIdToSiteIds: Record<number, number[]> = {};
for (const { sites: site } of clientSites) {
if (site.exitNodeId !== null) {
if (!exitNodeIdToSiteIds[site.exitNodeId]) {
exitNodeIdToSiteIds[site.exitNodeId] = [];
}
exitNodeIdToSiteIds[site.exitNodeId].push(site.siteId);
}
}
exitNodesData = allExitNodes.map((exitNode) => {
return {
publicKey: exitNode.publicKey,
relayPort: config.getRawConfig().gerbil.clients_start_port,
endpoint: exitNode.endpoint,
siteIds: exitNodeIdToSiteIds[exitNode.exitNodeId] ?? []
};
});
}
logger.debug("sendOlmSyncMessage: sending sync message")
await sendToClient(olm.olmId, {
type: "olm/sync",
data: {
sites: siteConfigurations,
exitNodes: exitNodesData
}
}).catch((error) => {
logger.warn(`Error sending olm sync message:`, error);
});
}

View File

@@ -1,84 +0,0 @@
import { NextFunction, Request, Response } from "express";
import { db } from "@server/db";
import { olms } from "@server/db";
import { eq } from "drizzle-orm";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import response from "@server/lib/response";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import logger from "@server/logger";
const paramsSchema = z
.object({
userId: z.string(),
olmId: z.string()
})
.strict();
export async function unarchiveUserOlm(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { olmId } = parsedParams.data;
// Check if OLM exists and is archived
const [olm] = await db
.select()
.from(olms)
.where(eq(olms.olmId, olmId))
.limit(1);
if (!olm) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`OLM with ID ${olmId} not found`
)
);
}
if (!olm.archived) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`OLM with ID ${olmId} is not archived`
)
);
}
// Unarchive the OLM (set archived to false)
await db
.update(olms)
.set({ archived: false })
.where(eq(olms.olmId, olmId));
return response(res, {
data: null,
success: true,
error: false,
message: "Device unarchived successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to unarchive device"
)
);
}
}

View File

@@ -10,8 +10,6 @@ import { fromError } from "zod-validation-error";
import { ActionsEnum } from "@server/auth/actions";
import { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
import { build } from "@server/build";
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
const createRoleParamsSchema = z.strictObject({
orgId: z.string()
@@ -19,8 +17,7 @@ const createRoleParamsSchema = z.strictObject({
const createRoleSchema = z.strictObject({
name: z.string().min(1).max(255),
description: z.string().optional(),
requireDeviceApproval: z.boolean().optional()
description: z.string().optional()
});
export const defaultRoleAllowedActions: ActionsEnum[] = [
@@ -100,11 +97,6 @@ export async function createRole(
);
}
const isLicensed = await isLicensedOrSubscribed(orgId);
if (build === "oss" || !isLicensed) {
roleData.requireDeviceApproval = undefined;
}
await db.transaction(async (trx) => {
const newRole = await trx
.insert(roles)

View File

@@ -1,13 +1,15 @@
import { db, orgs, roles } from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi";
import HttpCode from "@server/types/HttpCode";
import { eq, sql } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { roles, orgs } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import { sql, eq } from "drizzle-orm";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import stoi from "@server/lib/stoi";
import { OpenAPITags, registry } from "@server/openApi";
const listRolesParamsSchema = z.strictObject({
orgId: z.string()
@@ -36,8 +38,7 @@ async function queryRoles(orgId: string, limit: number, offset: number) {
isAdmin: roles.isAdmin,
name: roles.name,
description: roles.description,
orgName: orgs.name,
requireDeviceApproval: roles.requireDeviceApproval
orgName: orgs.name
})
.from(roles)
.leftJoin(orgs, eq(roles.orgId, orgs.orgId))

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