mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-11 18:09:05 +00:00
Compare commits
127 Commits
msg-delive
...
603b66780d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
603b66780d | ||
|
|
6bb72a7447 | ||
|
|
a679588d3b | ||
|
|
56114cb11a | ||
|
|
ccc6a64332 | ||
|
|
aee8d0131e | ||
|
|
20fc347182 | ||
|
|
6462b3c482 | ||
|
|
c3d620a9b2 | ||
|
|
1cfab08262 | ||
|
|
e70919b9e4 | ||
|
|
157b673b64 | ||
|
|
2d71293a7c | ||
|
|
ac30d28d7e | ||
|
|
23cf7bf745 | ||
|
|
d63de9ba40 | ||
|
|
e7ac5c34a2 | ||
|
|
1f88876e3c | ||
|
|
e0425e2458 | ||
|
|
10e9f017fb | ||
|
|
6d2cb69e45 | ||
|
|
2fab9b65a8 | ||
|
|
47a7a3f230 | ||
|
|
61835ca0e6 | ||
|
|
f4b22c5b31 | ||
|
|
65afe23dd8 | ||
|
|
371e44e235 | ||
|
|
d349795995 | ||
|
|
7d106294bc | ||
|
|
8c3e094534 | ||
|
|
b20a7231bc | ||
|
|
84998fdb6e | ||
|
|
0d9ece1329 | ||
|
|
eb7107016b | ||
|
|
a7608424b7 | ||
|
|
6710657789 | ||
|
|
b25069ab49 | ||
|
|
6e8d1f9149 | ||
|
|
825da82e56 | ||
|
|
8d28063724 | ||
|
|
a7eed886ab | ||
|
|
4e965f1e83 | ||
|
|
ea3011afca | ||
|
|
b20e5b4af1 | ||
|
|
577660c656 | ||
|
|
f9a9007ed9 | ||
|
|
19873a0b3d | ||
|
|
4f13267bc8 | ||
|
|
c820fcc518 | ||
|
|
c612972f38 | ||
|
|
395c4eff53 | ||
|
|
6aa40bfd61 | ||
|
|
f5744d0dcd | ||
|
|
a975b33b7e | ||
|
|
92be668a55 | ||
|
|
15153d5e3a | ||
|
|
212800d365 | ||
|
|
24ac9a1623 | ||
|
|
78bcfed668 | ||
|
|
bf8be14c3a | ||
|
|
fe1f7bee60 | ||
|
|
fbe14acdd1 | ||
|
|
11c0afd9ab | ||
|
|
52e6b36c2d | ||
|
|
dcce15036c | ||
|
|
fd27682bbb | ||
|
|
0cffeda5da | ||
|
|
aff486ca57 | ||
|
|
d3eb950888 | ||
|
|
1b98f44588 | ||
|
|
8a5a0c7c18 | ||
|
|
d2f0825498 | ||
|
|
ac88b79066 | ||
|
|
9160d94c7b | ||
|
|
de6ce7aa10 | ||
|
|
91a4b11632 | ||
|
|
99ed4ea683 | ||
|
|
1a0d4870ed | ||
|
|
f1bd315a96 | ||
|
|
42d8e932ff | ||
|
|
658ec87d1b | ||
|
|
e9bbee13c0 | ||
|
|
99164bf7ab | ||
|
|
6a3cc578a7 | ||
|
|
720c3d7c41 | ||
|
|
72733a9b77 | ||
|
|
1295141eaf | ||
|
|
e8099795b3 | ||
|
|
d65fbcc28b | ||
|
|
ad0a17f642 | ||
|
|
c2ecabab33 | ||
|
|
7170b14b1f | ||
|
|
343ba64eb4 | ||
|
|
242c314e42 | ||
|
|
16f9191bc5 | ||
|
|
6ec51e50d1 | ||
|
|
f1d73c5f8c | ||
|
|
6093add470 | ||
|
|
4af6bd43ef | ||
|
|
99ad7c6bef | ||
|
|
fc192d955b | ||
|
|
914f1b41c6 | ||
|
|
0601eacfd5 | ||
|
|
5481eae606 | ||
|
|
d868c85ebe | ||
|
|
ff0f0aa4e0 | ||
|
|
d4f9f20ef9 | ||
|
|
30808ce2db | ||
|
|
db5f7ed731 | ||
|
|
6fd69f1293 | ||
|
|
9f1dd42a85 | ||
|
|
7b55ef1cc8 | ||
|
|
b988f21b15 | ||
|
|
a688757f5c | ||
|
|
0ed1852a35 | ||
|
|
a5c3702b8d | ||
|
|
b25f43da9a | ||
|
|
ea0f0de802 | ||
|
|
476131859d | ||
|
|
8573c38332 | ||
|
|
bc9d2835be | ||
|
|
df582b0409 | ||
|
|
e3865dcd4d | ||
|
|
0eba5cdb0b | ||
|
|
2284f2cafc | ||
|
|
80215a7de0 | ||
|
|
ddd829064a |
149
.github/workflows/cicd.yml
vendored
149
.github/workflows/cicd.yml
vendored
@@ -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:
|
||||
|
||||
426
.github/workflows/cicd.yml.backup
vendored
426
.github/workflows/cicd.yml.backup
vendored
@@ -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"
|
||||
125
.github/workflows/saas.yml
vendored
125
.github/workflows/saas.yml
vendored
@@ -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
3
.gitignore
vendored
@@ -50,5 +50,4 @@ dynamic/
|
||||
*.mmdb
|
||||
scratch/
|
||||
tsconfig.json
|
||||
hydrateSaas.ts
|
||||
CLAUDE.md
|
||||
hydrateSaas.ts
|
||||
23
Dockerfile
23
Dockerfile
@@ -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
205
Makefile
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -43,12 +43,9 @@ entryPoints:
|
||||
http:
|
||||
tls:
|
||||
certResolver: "letsencrypt"
|
||||
encodedCharacters:
|
||||
allowEncodedSlash: true
|
||||
allowEncodedQuestionMark: true
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
|
||||
ping:
|
||||
entryPoint: "web"
|
||||
entryPoint: "web"
|
||||
@@ -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)", "")
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -13,4 +13,3 @@ export * from "./verifyApiKeyIsRoot";
|
||||
export * from "./verifyApiKeyApiKeyAccess";
|
||||
export * from "./verifyApiKeyClientAccess";
|
||||
export * from "./verifyApiKeySiteResourceAccess";
|
||||
export * from "./verifyApiKeyIdpAccess";
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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({}),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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";
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -17,4 +17,3 @@ export * from "./securityKey";
|
||||
export * from "./startDeviceWebAuth";
|
||||
export * from "./verifyDeviceWebAuth";
|
||||
export * from "./pollDeviceWebAuth";
|
||||
export * from "./lookupUser";
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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)}`
|
||||
|
||||
@@ -6,4 +6,3 @@ export * from "./handleGetConfigMessage";
|
||||
export * from "./handleSocketMessages";
|
||||
export * from "./handleNewtPingRequestMessage";
|
||||
export * from "./handleApplyBlueprintMessage";
|
||||
export * from "./handleNewtPingMessage";
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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 });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user