mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-18 03:26:34 +02:00
Compare commits
169 Commits
1.17.0
..
c207372f75
| Author | SHA1 | Date | |
|---|---|---|---|
| c207372f75 | |||
| 6606ec0abb | |||
| d17c821b77 | |||
| 08681fceff | |||
| cc8dd44d92 | |||
| 761666b0f1 | |||
| 71b483e95e | |||
| 4f5ceb054a | |||
| 560b5b48dc | |||
| eb5d7f1c97 | |||
| ae0245b35f | |||
| 0fdbea622c | |||
| a69a29ef70 | |||
| d7089403f6 | |||
| 37f943edd2 | |||
| a437b8937e | |||
| fa9de6db42 | |||
| 533bd1b68a | |||
| eba84db036 | |||
| 07d848a40e | |||
| 9f54e2efff | |||
| 77f1172640 | |||
| f35946860a | |||
| 5d28c3e50c | |||
| 6d37c419c2 | |||
| bcc0658a20 | |||
| 24e49d85e9 | |||
| cc06503fbe | |||
| 984799b854 | |||
| 141d3db56c | |||
| 1d46a151ff | |||
| b03cb24d1d | |||
| ffbd3931be | |||
| 47f812616a | |||
| b1d7d56a7a | |||
| 4bceb70d87 | |||
| d23fbf2810 | |||
| efb373503c | |||
| 98d8c91f82 | |||
| 04697abc41 | |||
| 6f63292b26 | |||
| c416dd275f | |||
| 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 |
+2
-4
@@ -28,9 +28,7 @@ LICENSE
|
||||
CONTRIBUTING.md
|
||||
dist
|
||||
.git
|
||||
server/migrations/
|
||||
migrations/
|
||||
config/
|
||||
build.ts
|
||||
tsconfig.json
|
||||
Dockerfile*
|
||||
drizzle.config.ts
|
||||
tsconfig.json
|
||||
+12
-2
@@ -44,9 +44,19 @@ updates:
|
||||
schedule:
|
||||
interval: "daily"
|
||||
groups:
|
||||
patch-updates:
|
||||
dev-patch-updates:
|
||||
dependency-type: "development"
|
||||
update-types:
|
||||
- "patch"
|
||||
minor-updates:
|
||||
dev-minor-updates:
|
||||
dependency-type: "development"
|
||||
update-types:
|
||||
- "minor"
|
||||
prod-patch-updates:
|
||||
dependency-type: "production"
|
||||
update-types:
|
||||
- "patch"
|
||||
prod-minor-updates:
|
||||
dependency-type: "production"
|
||||
update-types:
|
||||
- "minor"
|
||||
+51
-211
@@ -1,4 +1,4 @@
|
||||
name: Public CICD Pipeline
|
||||
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.
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
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
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Monitor storage space
|
||||
run: |
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Monitor storage space
|
||||
run: |
|
||||
@@ -149,7 +149,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
@@ -201,10 +201,10 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Extract tag name
|
||||
id: get-tag
|
||||
@@ -264,9 +264,9 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
|
||||
with:
|
||||
go-version: 1.25
|
||||
go-version: 1.24
|
||||
|
||||
- name: Update version in package.json
|
||||
run: |
|
||||
@@ -289,17 +289,25 @@ jobs:
|
||||
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 \
|
||||
PANGOLIN_VERSION=${{ env.TAG }} \
|
||||
GERBIL_VERSION=${{ env.LATEST_GERBIL_TAG }} \
|
||||
BADGER_VERSION=${{ env.LATEST_BADGER_TAG }}
|
||||
shell: bash
|
||||
make go-build-release
|
||||
|
||||
- name: Upload artifacts from /install/bin
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: install-bin
|
||||
path: install/bin/
|
||||
@@ -321,93 +329,24 @@ 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 [[ "$TAG" == *"-rc."* ]]; 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)
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -415,7 +354,7 @@ jobs:
|
||||
|
||||
- name: Install cosign
|
||||
# cosign is used to sign and verify container images (key and keyless)
|
||||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||
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,
|
||||
@@ -432,127 +371,28 @@ jobs:
|
||||
issuer="https://token.actions.githubusercontent.com"
|
||||
id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
|
||||
|
||||
# Track failures
|
||||
FAILED_TAGS=()
|
||||
SUCCESSFUL_TAGS=()
|
||||
for IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
|
||||
echo "Processing ${IMAGE}:${TAG}"
|
||||
|
||||
# Determine if this is an RC release
|
||||
IS_RC="false"
|
||||
if [[ "$TAG" == *"-rc."* ]]; then
|
||||
IS_RC="true"
|
||||
fi
|
||||
DIGEST="$(skopeo inspect --retry-times 3 docker://${IMAGE}:${TAG} | jq -r '.Digest')"
|
||||
REF="${IMAGE}@${DIGEST}"
|
||||
echo "Resolved digest: ${REF}"
|
||||
|
||||
# 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
|
||||
echo "==> cosign sign (keyless) --recursive ${REF}"
|
||||
cosign sign --recursive "${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}"
|
||||
TAG_FAILED=false
|
||||
echo "==> cosign sign (key) --recursive ${REF}"
|
||||
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
|
||||
|
||||
# Wrap the entire tag processing in error handling
|
||||
(
|
||||
set -e
|
||||
DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${IMAGE_TAG} | jq -r '.Digest')"
|
||||
REF="${BASE_IMAGE}@${DIGEST}"
|
||||
echo "Resolved digest: ${REF}"
|
||||
echo "==> cosign verify (public key) ${REF}"
|
||||
cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text
|
||||
|
||||
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}"
|
||||
|
||||
# Retry wrapper for verification to handle registry propagation delays
|
||||
retry_verify() {
|
||||
local cmd="$1"
|
||||
local attempts=6
|
||||
local delay=5
|
||||
local i=1
|
||||
until eval "$cmd"; do
|
||||
if [ $i -ge $attempts ]; then
|
||||
echo "Verification failed after $attempts attempts"
|
||||
return 1
|
||||
fi
|
||||
echo "Verification not yet available. Retry $i/$attempts after ${delay}s..."
|
||||
sleep $delay
|
||||
i=$((i+1))
|
||||
delay=$((delay*2))
|
||||
# Cap the delay to avoid very long waits
|
||||
if [ $delay -gt 60 ]; then delay=60; fi
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
echo "==> cosign verify (public key) ${REF}"
|
||||
if retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${REF}' -o text"; then
|
||||
VERIFIED_INDEX=true
|
||||
else
|
||||
VERIFIED_INDEX=false
|
||||
fi
|
||||
|
||||
echo "==> cosign verify (keyless policy) ${REF}"
|
||||
if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${REF}' -o text"; then
|
||||
VERIFIED_INDEX_KEYLESS=true
|
||||
else
|
||||
VERIFIED_INDEX_KEYLESS=false
|
||||
fi
|
||||
|
||||
# Check if verification succeeded
|
||||
if [ "${VERIFIED_INDEX}" != "true" ] && [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then
|
||||
echo "⚠️ WARNING: Verification not available for ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||
echo "This may be due to registry propagation delays. Continuing anyway."
|
||||
fi
|
||||
) || TAG_FAILED=true
|
||||
|
||||
if [ "$TAG_FAILED" = "true" ]; then
|
||||
echo "⚠️ WARNING: Failed to sign/verify ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||
FAILED_TAGS+=("${BASE_IMAGE}:${IMAGE_TAG}")
|
||||
else
|
||||
echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||
SUCCESSFUL_TAGS+=("${BASE_IMAGE}:${IMAGE_TAG}")
|
||||
fi
|
||||
done
|
||||
echo "==> cosign verify (keyless policy) ${REF}"
|
||||
cosign verify \
|
||||
--certificate-oidc-issuer "${issuer}" \
|
||||
--certificate-identity-regexp "${id_regex}" \
|
||||
"${REF}" -o text
|
||||
done
|
||||
|
||||
# Report summary
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Sign and Verify Summary"
|
||||
echo "=========================================="
|
||||
echo "Successful: ${#SUCCESSFUL_TAGS[@]}"
|
||||
echo "Failed: ${#FAILED_TAGS[@]}"
|
||||
echo ""
|
||||
|
||||
if [ ${#FAILED_TAGS[@]} -gt 0 ]; then
|
||||
echo "Failed tags:"
|
||||
for tag in "${FAILED_TAGS[@]}"; do
|
||||
echo " - $tag"
|
||||
done
|
||||
echo ""
|
||||
echo "⚠️ WARNING: Some tags failed to sign/verify, but continuing anyway"
|
||||
else
|
||||
echo "✓ All images signed and verified successfully!"
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
post-run:
|
||||
@@ -570,7 +410,7 @@ jobs:
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
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
|
||||
|
||||
@@ -21,12 +21,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '22'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
skopeo --version
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
|
||||
- name: Input check
|
||||
run: |
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
name: SAAS 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@v6
|
||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Download MaxMind GeoLite2 databases
|
||||
env:
|
||||
MAXMIND_LICENSE_KEY: ${{ secrets.MAXMIND_LICENSE_KEY }}
|
||||
run: |
|
||||
echo "Downloading MaxMind GeoLite2 databases..."
|
||||
|
||||
# Download GeoLite2-Country
|
||||
curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz" \
|
||||
-o GeoLite2-Country.tar.gz
|
||||
|
||||
# Download GeoLite2-ASN
|
||||
curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz" \
|
||||
-o GeoLite2-ASN.tar.gz
|
||||
|
||||
# Extract the .mmdb files
|
||||
tar -xzf GeoLite2-Country.tar.gz --strip-components=1 --wildcards '*.mmdb'
|
||||
tar -xzf GeoLite2-ASN.tar.gz --strip-components=1 --wildcards '*.mmdb'
|
||||
|
||||
# Verify files exist
|
||||
if [ ! -f "GeoLite2-Country.mmdb" ]; then
|
||||
echo "ERROR: Failed to download GeoLite2-Country.mmdb"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "GeoLite2-ASN.mmdb" ]; then
|
||||
echo "ERROR: Failed to download GeoLite2-ASN.mmdb"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up tar files
|
||||
rm -f GeoLite2-Country.tar.gz GeoLite2-ASN.tar.gz
|
||||
|
||||
echo "MaxMind databases downloaded successfully"
|
||||
ls -lh GeoLite2-*.mmdb
|
||||
|
||||
- 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@v6
|
||||
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@v6
|
||||
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"
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
with:
|
||||
days-before-stale: 14
|
||||
days-before-close: 14
|
||||
|
||||
@@ -14,12 +14,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '22'
|
||||
|
||||
- name: Copy config file
|
||||
run: cp config/config.example.yml config/config.yml
|
||||
@@ -34,10 +34,10 @@ jobs:
|
||||
run: npm run set:oss
|
||||
|
||||
- name: Generate database migrations
|
||||
run: npm run db:generate
|
||||
run: npm run db:sqlite:generate
|
||||
|
||||
- name: Apply database migrations
|
||||
run: npm run db:push
|
||||
run: npm run db:sqlite:push
|
||||
|
||||
- name: Test with tsc
|
||||
run: npx tsc --noEmit
|
||||
@@ -62,7 +62,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Copy config file
|
||||
run: cp config/config.example.yml config/config.yml
|
||||
|
||||
- name: Build Docker image sqlite
|
||||
run: make dev-build-sqlite
|
||||
@@ -71,7 +74,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Copy config file
|
||||
run: cp config/config.example.yml config/config.yml
|
||||
|
||||
- name: Build Docker image pg
|
||||
run: make dev-build-pg
|
||||
|
||||
+1
-4
@@ -50,7 +50,4 @@ dynamic/
|
||||
*.mmdb
|
||||
scratch/
|
||||
tsconfig.json
|
||||
hydrateSaas.ts
|
||||
CLAUDE.md
|
||||
drizzle.config.ts
|
||||
server/setup/migrations.ts
|
||||
hydrateSaas.ts
|
||||
Vendored
+2
-2
@@ -4,7 +4,7 @@
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
@@ -19,4 +19,4 @@
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
}
|
||||
|
||||
+48
-69
@@ -1,93 +1,72 @@
|
||||
# FROM node:24-slim AS base
|
||||
FROM public.ecr.aws/docker/library/node:24-slim AS base
|
||||
FROM node:24-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
FROM base AS builder-dev
|
||||
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
ARG BUILD=oss
|
||||
ARG DATABASE=sqlite
|
||||
|
||||
RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi && \
|
||||
npm run set:$DATABASE && \
|
||||
npm run set:$BUILD && \
|
||||
npm run db:generate && \
|
||||
npm run build && \
|
||||
npm run build:cli && \
|
||||
test -f dist/server.mjs
|
||||
RUN apk add --no-cache curl tzdata python3 make g++
|
||||
|
||||
# Create placeholder files for MaxMind databases to avoid COPY errors
|
||||
# Real files should be present for saas builds, placeholders for oss builds
|
||||
RUN touch /app/GeoLite2-Country.mmdb /app/GeoLite2-ASN.mmdb
|
||||
# COPY package.json package-lock.json ./
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
FROM base AS builder
|
||||
COPY . .
|
||||
|
||||
RUN npm ci --omit=dev
|
||||
RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts
|
||||
RUN echo "export const driver: \"pg\" | \"sqlite\" = \"$DATABASE\";" >> server/db/index.ts
|
||||
|
||||
# FROM node:24-slim AS runner
|
||||
FROM public.ecr.aws/docker/library/node:24-slim AS runner
|
||||
RUN echo "export const build = \"$BUILD\" as \"saas\" | \"enterprise\" | \"oss\";" > server/build.ts
|
||||
|
||||
# Copy the appropriate TypeScript configuration based on build type
|
||||
RUN if [ "$BUILD" = "oss" ]; then cp tsconfig.oss.json tsconfig.json; \
|
||||
elif [ "$BUILD" = "saas" ]; then cp tsconfig.saas.json tsconfig.json; \
|
||||
elif [ "$BUILD" = "enterprise" ]; then cp tsconfig.enterprise.json tsconfig.json; \
|
||||
fi
|
||||
|
||||
# if the build is oss then remove the server/private directory
|
||||
RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi
|
||||
|
||||
RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema --out init; fi
|
||||
|
||||
RUN mkdir -p dist
|
||||
RUN npm run next:build
|
||||
RUN node esbuild.mjs -e server/index.ts -o dist/server.mjs -b $BUILD
|
||||
RUN if [ "$DATABASE" = "pg" ]; then \
|
||||
node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs; \
|
||||
else \
|
||||
node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs; \
|
||||
fi
|
||||
|
||||
# test to make sure the build output is there and error if not
|
||||
RUN test -f dist/server.mjs
|
||||
|
||||
RUN npm run build:cli
|
||||
|
||||
# Prune dev dependencies and clean up to prepare for copy to runner
|
||||
RUN npm prune --omit=dev && npm cache clean --force
|
||||
|
||||
FROM node:24-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y curl tzdata && rm -rf /var/lib/apt/lists/*
|
||||
# Only curl and tzdata needed at runtime - no build tools!
|
||||
RUN apk add --no-cache curl tzdata
|
||||
|
||||
# Copy pre-built node_modules from builder (already pruned to production only)
|
||||
# This includes the compiled native modules like better-sqlite3
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
|
||||
COPY --from=builder-dev /app/.next/standalone ./
|
||||
COPY --from=builder-dev /app/.next/static ./.next/static
|
||||
COPY --from=builder-dev /app/dist ./dist
|
||||
COPY --from=builder-dev /app/server/migrations ./dist/init
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/init ./dist/init
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
|
||||
COPY ./cli/wrapper.sh /usr/local/bin/pangctl
|
||||
RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
|
||||
|
||||
COPY server/db/names.json ./dist/names.json
|
||||
COPY server/db/ios_models.json ./dist/ios_models.json
|
||||
COPY server/db/mac_models.json ./dist/mac_models.json
|
||||
COPY public ./public
|
||||
|
||||
# Copy MaxMind databases for SaaS builds
|
||||
ARG BUILD=oss
|
||||
|
||||
RUN mkdir -p ./maxmind
|
||||
|
||||
# Copy MaxMind databases (placeholders exist for oss builds, real files for saas)
|
||||
COPY --from=builder-dev /app/GeoLite2-Country.mmdb ./maxmind/GeoLite2-Country.mmdb
|
||||
COPY --from=builder-dev /app/GeoLite2-ASN.mmdb ./maxmind/GeoLite2-ASN.mmdb
|
||||
|
||||
# Remove MaxMind databases for non-saas builds (keep only for saas)
|
||||
RUN if [ "$BUILD" != "saas" ]; then rm -rf ./maxmind; fi
|
||||
|
||||
# OCI Image Labels - Build Args for dynamic values
|
||||
ARG VERSION="dev"
|
||||
ARG REVISION=""
|
||||
ARG CREATED=""
|
||||
ARG LICENSE="AGPL-3.0"
|
||||
|
||||
# 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"
|
||||
|
||||
# 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"]
|
||||
|
||||
+1
-3
@@ -1,9 +1,7 @@
|
||||
FROM node:24-alpine
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,15 +35,9 @@
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.pangolin.net/careers/join-us">
|
||||
<img src="https://img.shields.io/badge/🚀_We're_Hiring!-Join_Our_Team-brightgreen?style=for-the-badge" alt="We're Hiring!" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<strong>
|
||||
Get started with Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
|
||||
Start testing Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
@@ -60,9 +54,9 @@ Pangolin is an open-source, identity-based remote access platform built on WireG
|
||||
|
||||
| <img width=500 /> | Description |
|
||||
|-----------------|--------------|
|
||||
| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing - no infrastructure required. Or, self-host your own [remote node](https://docs.pangolin.net/manage/remote-node/understanding-nodes) and connect to our control plane. |
|
||||
| **Self-Host: Community Edition** | Free, open source, and licensed under AGPL-3. |
|
||||
| **Self-Host: Enterprise Edition** | Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses earning under \$100K USD annually. |
|
||||
| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing — no infrastructure required. Or, self-host your own [remote node](https://docs.pangolin.net/manage/remote-node/nodes) and connect to our control plane. |
|
||||
|
||||
## Key Features
|
||||
|
||||
@@ -80,21 +74,20 @@ Download the Pangolin client for your platform:
|
||||
- [Mac](https://pangolin.net/downloads/mac)
|
||||
- [Windows](https://pangolin.net/downloads/windows)
|
||||
- [Linux](https://pangolin.net/downloads/linux)
|
||||
- [iOS](https://pangolin.net/downloads/ios)
|
||||
- [Android](https://pangolin.net/downloads/android)
|
||||
|
||||
## Get Started
|
||||
|
||||
### Sign up now
|
||||
|
||||
Create an account at [app.pangolin.net](https://app.pangolin.net) to get started with Pangolin Cloud. A generous free tier is available.
|
||||
|
||||
### Check out the docs
|
||||
|
||||
We encourage everyone to read the full documentation first, which is
|
||||
available at [docs.pangolin.net](https://docs.pangolin.net). This README provides only a very brief subset of
|
||||
the docs to illustrate some basic ideas.
|
||||
|
||||
### Sign up and try now
|
||||
|
||||
For Pangolin's managed service, you will first need to create an account at
|
||||
[app.pangolin.net](https://app.pangolin.net). We have a generous free tier to get started.
|
||||
|
||||
## Licensing
|
||||
|
||||
Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://pangolin.net/fcl.html). For inquiries about commercial licensing, please contact us at [contact@pangolin.net](mailto:contact@pangolin.net).
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
If you discover a security vulnerability, please follow the steps below to responsibly disclose it to us:
|
||||
|
||||
1. **Do not create a public GitHub issue or discussion post.** This could put the security of other users at risk.
|
||||
2. Send a detailed report to [security@pangolin.net](mailto:security@pangolin.net) with the following information:
|
||||
2. Send a detailed report to [security@pangolin.net](mailto:security@pangolin.net) or send a **private** message to a maintainer on [Discord](https://discord.gg/HCJR8Xhme4). Include:
|
||||
|
||||
- Description and location of the vulnerability.
|
||||
- Potential impact of the vulnerability.
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import requests
|
||||
import yaml
|
||||
import json
|
||||
import base64
|
||||
|
||||
# The file path for the YAML file to be read
|
||||
# You can change this to the path of your YAML file
|
||||
YAML_FILE_PATH = 'blueprint.yaml'
|
||||
|
||||
# The API endpoint and headers from the curl request
|
||||
API_URL = 'http://api.pangolin.net/v1/org/test/blueprint'
|
||||
HEADERS = {
|
||||
'accept': '*/*',
|
||||
'Authorization': 'Bearer <your_token_here>',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
def convert_and_send(file_path, url, headers):
|
||||
"""
|
||||
Reads a YAML file, converts its content to a JSON payload,
|
||||
and sends it via a PUT request to a specified URL.
|
||||
"""
|
||||
try:
|
||||
# Read the YAML file content
|
||||
with open(file_path, 'r') as file:
|
||||
yaml_content = file.read()
|
||||
|
||||
# Parse the YAML string to a Python dictionary
|
||||
# This will be used to ensure the YAML is valid before sending
|
||||
parsed_yaml = yaml.safe_load(yaml_content)
|
||||
|
||||
# convert the parsed YAML to a JSON string
|
||||
json_payload = json.dumps(parsed_yaml)
|
||||
print("Converted JSON payload:")
|
||||
print(json_payload)
|
||||
|
||||
# Encode the JSON string to Base64
|
||||
encoded_json = base64.b64encode(json_payload.encode('utf-8')).decode('utf-8')
|
||||
|
||||
# Create the final payload with the base64 encoded data
|
||||
final_payload = {
|
||||
"blueprint": encoded_json
|
||||
}
|
||||
|
||||
print("Sending the following Base64 encoded JSON payload:")
|
||||
print(final_payload)
|
||||
print("-" * 20)
|
||||
|
||||
# Make the PUT request with the base64 encoded payload
|
||||
response = requests.put(url, headers=headers, json=final_payload)
|
||||
|
||||
# Print the API response for debugging
|
||||
print(f"API Response Status Code: {response.status_code}")
|
||||
print("API Response Content:")
|
||||
print(response.text)
|
||||
|
||||
# Raise an exception for bad status codes (4xx or 5xx)
|
||||
response.raise_for_status()
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Error: The file '{file_path}' was not found.")
|
||||
except yaml.YAMLError as e:
|
||||
print(f"Error parsing YAML file: {e}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"An error occurred during the API request: {e}")
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred: {e}")
|
||||
|
||||
# Run the function
|
||||
if __name__ == "__main__":
|
||||
convert_and_send(YAML_FILE_PATH, API_URL, HEADERS)
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
client-resources:
|
||||
client-resource-nice-id-uno:
|
||||
name: this is my resource
|
||||
protocol: tcp
|
||||
proxy-port: 3001
|
||||
hostname: localhost
|
||||
internal-port: 3000
|
||||
site: lively-yosemite-toad
|
||||
client-resource-nice-id-duce:
|
||||
name: this is my resource
|
||||
protocol: udp
|
||||
proxy-port: 3000
|
||||
hostname: localhost
|
||||
internal-port: 3000
|
||||
site: lively-yosemite-toad
|
||||
|
||||
proxy-resources:
|
||||
resource-nice-id-uno:
|
||||
name: this is my resource
|
||||
protocol: http
|
||||
full-domain: duce.test.example.com
|
||||
host-header: example.com
|
||||
tls-server-name: example.com
|
||||
# auth:
|
||||
# pincode: 123456
|
||||
# password: sadfasdfadsf
|
||||
# sso-enabled: true
|
||||
# sso-roles:
|
||||
# - Member
|
||||
# sso-users:
|
||||
# - owen@pangolin.net
|
||||
# whitelist-users:
|
||||
# - owen@pangolin.net
|
||||
# auto-login-idp: 1
|
||||
headers:
|
||||
- name: X-Example-Header
|
||||
value: example-value
|
||||
- name: X-Another-Header
|
||||
value: another-value
|
||||
rules:
|
||||
- action: allow
|
||||
match: ip
|
||||
value: 1.1.1.1
|
||||
- action: deny
|
||||
match: cidr
|
||||
value: 2.2.2.2/32
|
||||
- action: pass
|
||||
match: path
|
||||
value: /admin
|
||||
targets:
|
||||
- site: lively-yosemite-toad
|
||||
path: /path
|
||||
pathMatchType: prefix
|
||||
hostname: localhost
|
||||
method: http
|
||||
port: 8000
|
||||
- site: slim-alpine-chipmunk
|
||||
hostname: localhost
|
||||
path: /yoman
|
||||
pathMatchType: exact
|
||||
method: http
|
||||
port: 8001
|
||||
resource-nice-id-duce:
|
||||
name: this is other resource
|
||||
protocol: tcp
|
||||
proxy-port: 3000
|
||||
targets:
|
||||
- site: lively-yosemite-toad
|
||||
hostname: localhost
|
||||
port: 3000
|
||||
@@ -1,36 +0,0 @@
|
||||
import { CommandModule } from "yargs";
|
||||
import { db, licenseKey } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
type ClearLicenseKeysArgs = { };
|
||||
|
||||
export const clearLicenseKeys: CommandModule<
|
||||
{},
|
||||
ClearLicenseKeysArgs
|
||||
> = {
|
||||
command: "clear-license-keys",
|
||||
describe:
|
||||
"Clear all license keys from the database",
|
||||
// no args
|
||||
builder: (yargs) => {
|
||||
return yargs;
|
||||
},
|
||||
handler: async (argv: {}) => {
|
||||
try {
|
||||
|
||||
console.log(`Clearing all license keys from the database`);
|
||||
|
||||
// Delete all license keys
|
||||
const deletedCount = await db
|
||||
.delete(licenseKey)
|
||||
.where(eq(licenseKey.licenseKeyId, licenseKey.licenseKeyId)) .returning();; // delete all
|
||||
|
||||
console.log(`Deleted ${deletedCount.length} license key(s) from the database`);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,123 +0,0 @@
|
||||
import { CommandModule } from "yargs";
|
||||
import { db, clients, olms, currentFingerprint, userClients, approvals } from "@server/db";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
|
||||
type DeleteClientArgs = {
|
||||
orgId: string;
|
||||
niceId: string;
|
||||
};
|
||||
|
||||
export const deleteClient: CommandModule<{}, DeleteClientArgs> = {
|
||||
command: "delete-client",
|
||||
describe:
|
||||
"Delete a client and all associated data (OLMs, current fingerprint, userClients, approvals). Snapshots are preserved.",
|
||||
builder: (yargs) => {
|
||||
return yargs
|
||||
.option("orgId", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
describe: "The organization ID"
|
||||
})
|
||||
.option("niceId", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
describe: "The client niceId (identifier)"
|
||||
});
|
||||
},
|
||||
handler: async (argv: { orgId: string; niceId: string }) => {
|
||||
try {
|
||||
const { orgId, niceId } = argv;
|
||||
|
||||
console.log(
|
||||
`Deleting client with orgId: ${orgId}, niceId: ${niceId}...`
|
||||
);
|
||||
|
||||
// Find the client
|
||||
const [client] = await db
|
||||
.select()
|
||||
.from(clients)
|
||||
.where(and(eq(clients.orgId, orgId), eq(clients.niceId, niceId)))
|
||||
.limit(1);
|
||||
|
||||
if (!client) {
|
||||
console.error(
|
||||
`Error: Client with orgId "${orgId}" and niceId "${niceId}" not found.`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const clientId = client.clientId;
|
||||
console.log(`Found client with clientId: ${clientId}`);
|
||||
|
||||
// Find all OLMs associated with this client
|
||||
const associatedOlms = await db
|
||||
.select()
|
||||
.from(olms)
|
||||
.where(eq(olms.clientId, clientId));
|
||||
|
||||
console.log(`Found ${associatedOlms.length} OLM(s) associated with this client`);
|
||||
|
||||
// Delete in a transaction to ensure atomicity
|
||||
await db.transaction(async (trx) => {
|
||||
// Delete currentFingerprint entries for the associated OLMs
|
||||
// Note: We delete these explicitly before deleting OLMs to ensure
|
||||
// we have control, even though cascade would handle it
|
||||
let fingerprintCount = 0;
|
||||
if (associatedOlms.length > 0) {
|
||||
const olmIds = associatedOlms.map((olm) => olm.olmId);
|
||||
const deletedFingerprints = await trx
|
||||
.delete(currentFingerprint)
|
||||
.where(inArray(currentFingerprint.olmId, olmIds))
|
||||
.returning();
|
||||
fingerprintCount = deletedFingerprints.length;
|
||||
}
|
||||
console.log(`Deleted ${fingerprintCount} current fingerprint(s)`);
|
||||
|
||||
// Delete OLMs
|
||||
// Note: OLMs have onDelete: "set null" for clientId, so we need to delete them explicitly
|
||||
const deletedOlms = await trx
|
||||
.delete(olms)
|
||||
.where(eq(olms.clientId, clientId))
|
||||
.returning();
|
||||
console.log(`Deleted ${deletedOlms.length} OLM(s)`);
|
||||
|
||||
// Delete approvals
|
||||
// Note: Approvals have onDelete: "cascade" but we delete explicitly for clarity
|
||||
const deletedApprovals = await trx
|
||||
.delete(approvals)
|
||||
.where(eq(approvals.clientId, clientId))
|
||||
.returning();
|
||||
console.log(`Deleted ${deletedApprovals.length} approval(s)`);
|
||||
|
||||
// Delete userClients
|
||||
// Note: userClients have onDelete: "cascade" but we delete explicitly for clarity
|
||||
const deletedUserClients = await trx
|
||||
.delete(userClients)
|
||||
.where(eq(userClients.clientId, clientId))
|
||||
.returning();
|
||||
console.log(`Deleted ${deletedUserClients.length} userClient association(s)`);
|
||||
|
||||
// Finally, delete the client itself
|
||||
const deletedClients = await trx
|
||||
.delete(clients)
|
||||
.where(eq(clients.clientId, clientId))
|
||||
.returning();
|
||||
console.log(`Deleted client: ${deletedClients[0]?.name || niceId}`);
|
||||
});
|
||||
|
||||
console.log("\nClient deletion completed successfully!");
|
||||
console.log("\nSummary:");
|
||||
console.log(` - Client: ${niceId} (clientId: ${clientId})`);
|
||||
console.log(` - Olm(s): ${associatedOlms.length}`);
|
||||
console.log(` - Current fingerprints: deleted`);
|
||||
console.log(` - Approvals: deleted`);
|
||||
console.log(` - UserClients: deleted`);
|
||||
console.log(` - Snapshots: preserved (not deleted)`);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error deleting client:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,121 +0,0 @@
|
||||
import { CommandModule } from "yargs";
|
||||
import { db, orgs } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||
import { generateCA } from "@server/lib/sshCA";
|
||||
import fs from "fs";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
type GenerateOrgCaKeysArgs = {
|
||||
orgId: string;
|
||||
secret?: string;
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
export const generateOrgCaKeys: CommandModule<{}, GenerateOrgCaKeysArgs> = {
|
||||
command: "generate-org-ca-keys",
|
||||
describe:
|
||||
"Generate SSH CA public/private key pair for an organization and store them in the database (private key encrypted with server secret)",
|
||||
builder: (yargs) => {
|
||||
return yargs
|
||||
.option("orgId", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
describe: "The organization ID"
|
||||
})
|
||||
.option("secret", {
|
||||
type: "string",
|
||||
describe:
|
||||
"Server secret used to encrypt the CA private key. If omitted, read from config file (config.yml or config.yaml)."
|
||||
})
|
||||
.option("force", {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe:
|
||||
"Overwrite existing CA keys for the org if they already exist"
|
||||
});
|
||||
},
|
||||
handler: async (argv: {
|
||||
orgId: string;
|
||||
secret?: string;
|
||||
force?: boolean;
|
||||
}) => {
|
||||
try {
|
||||
const { orgId, force } = argv;
|
||||
let secret = argv.secret;
|
||||
|
||||
if (!secret) {
|
||||
const configPath = fs.existsSync(configFilePath1)
|
||||
? configFilePath1
|
||||
: fs.existsSync(configFilePath2)
|
||||
? configFilePath2
|
||||
: null;
|
||||
|
||||
if (!configPath) {
|
||||
console.error(
|
||||
"Error: No server secret provided and config file not found. " +
|
||||
"Expected config.yml or config.yaml in the config directory, or pass --secret."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const configContent = fs.readFileSync(configPath, "utf8");
|
||||
const config = yaml.load(configContent) as {
|
||||
server?: { secret?: string };
|
||||
};
|
||||
|
||||
if (!config?.server?.secret) {
|
||||
console.error(
|
||||
"Error: No server.secret in config file. Pass --secret or set server.secret in config."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
secret = config.server.secret;
|
||||
}
|
||||
|
||||
const [org] = await db
|
||||
.select({
|
||||
orgId: orgs.orgId,
|
||||
sshCaPrivateKey: orgs.sshCaPrivateKey,
|
||||
sshCaPublicKey: orgs.sshCaPublicKey
|
||||
})
|
||||
.from(orgs)
|
||||
.where(eq(orgs.orgId, orgId))
|
||||
.limit(1);
|
||||
|
||||
if (!org) {
|
||||
console.error(`Error: Organization with orgId "${orgId}" not found.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (org.sshCaPrivateKey != null || org.sshCaPublicKey != null) {
|
||||
if (!force) {
|
||||
console.error(
|
||||
"Error: This organization already has CA keys. Use --force to overwrite."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const ca = generateCA(`pangolin-ssh-ca-${orgId}`);
|
||||
const encryptedPrivateKey = encrypt(ca.privateKeyPem, secret);
|
||||
|
||||
await db
|
||||
.update(orgs)
|
||||
.set({
|
||||
sshCaPrivateKey: encryptedPrivateKey,
|
||||
sshCaPublicKey: ca.publicKeyOpenSSH
|
||||
})
|
||||
.where(eq(orgs.orgId, orgId));
|
||||
|
||||
console.log("SSH CA keys generated and stored for org:", orgId);
|
||||
console.log("\nPublic key (OpenSSH format):");
|
||||
console.log(ca.publicKeyOpenSSH);
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error generating org CA keys:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -6,9 +6,6 @@ import { setAdminCredentials } from "@cli/commands/setAdminCredentials";
|
||||
import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys";
|
||||
import { clearExitNodes } from "./commands/clearExitNodes";
|
||||
import { rotateServerSecret } from "./commands/rotateServerSecret";
|
||||
import { clearLicenseKeys } from "./commands/clearLicenseKeys";
|
||||
import { deleteClient } from "./commands/deleteClient";
|
||||
import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
|
||||
|
||||
yargs(hideBin(process.argv))
|
||||
.scriptName("pangctl")
|
||||
@@ -16,8 +13,5 @@ yargs(hideBin(process.argv))
|
||||
.command(resetUserSecurityKeys)
|
||||
.command(clearExitNodes)
|
||||
.command(rotateServerSecret)
|
||||
.command(clearLicenseKeys)
|
||||
.command(deleteClient)
|
||||
.command(generateOrgCaKeys)
|
||||
.demandCommand()
|
||||
.help().argv;
|
||||
|
||||
+18
-21
@@ -1,30 +1,27 @@
|
||||
# To see all available options, please visit the docs:
|
||||
# https://docs.pangolin.net/
|
||||
|
||||
gerbil:
|
||||
start_port: 51820
|
||||
base_endpoint: "{{.DashboardDomain}}"
|
||||
# https://docs.pangolin.net/self-host/advanced/config-file
|
||||
|
||||
app:
|
||||
dashboard_url: "https://{{.DashboardDomain}}"
|
||||
log_level: "info"
|
||||
telemetry:
|
||||
anonymous_usage: true
|
||||
dashboard_url: http://localhost:3002
|
||||
log_level: debug
|
||||
|
||||
domains:
|
||||
domain1:
|
||||
base_domain: "{{.BaseDomain}}"
|
||||
domain1:
|
||||
base_domain: example.com
|
||||
|
||||
server:
|
||||
secret: "{{.Secret}}"
|
||||
cors:
|
||||
origins: ["https://{{.DashboardDomain}}"]
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||
credentials: false
|
||||
secret: my_secret_key
|
||||
|
||||
gerbil:
|
||||
base_endpoint: example.com
|
||||
|
||||
orgs:
|
||||
block_size: 24
|
||||
subnet_group: 100.90.137.0/20
|
||||
|
||||
flags:
|
||||
require_email_verification: false
|
||||
disable_signup_without_invite: true
|
||||
disable_user_create_org: false
|
||||
allow_raw_resources: true
|
||||
require_email_verification: false
|
||||
disable_signup_without_invite: true
|
||||
disable_user_create_org: true
|
||||
allow_raw_resources: true
|
||||
enable_integration_api: true
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
http:
|
||||
middlewares:
|
||||
badger:
|
||||
plugin:
|
||||
badger:
|
||||
disableForwardAuth: true
|
||||
redirect-to-https:
|
||||
redirectScheme:
|
||||
scheme: https
|
||||
@@ -17,16 +13,14 @@ http:
|
||||
- web
|
||||
middlewares:
|
||||
- redirect-to-https
|
||||
- badger
|
||||
|
||||
# Next.js router (handles everything except API and WebSocket paths)
|
||||
next-router:
|
||||
rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)"
|
||||
rule: "Host(`{{.DashboardDomain}}`)"
|
||||
service: next-service
|
||||
priority: 10
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- badger
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -34,10 +28,9 @@ http:
|
||||
api-router:
|
||||
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
|
||||
service: api-service
|
||||
priority: 100
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- badger
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -51,12 +44,3 @@ http:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://pangolin:3000" # API/WebSocket server
|
||||
|
||||
tcp:
|
||||
serversTransports:
|
||||
pp-transport-v1:
|
||||
proxyProtocol:
|
||||
version: 1
|
||||
pp-transport-v2:
|
||||
proxyProtocol:
|
||||
version: 2
|
||||
|
||||
@@ -3,52 +3,32 @@ api:
|
||||
dashboard: true
|
||||
|
||||
providers:
|
||||
http:
|
||||
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
||||
pollInterval: "5s"
|
||||
file:
|
||||
filename: "/etc/traefik/dynamic_config.yml"
|
||||
directory: "/var/dynamic"
|
||||
watch: true
|
||||
|
||||
experimental:
|
||||
plugins:
|
||||
badger:
|
||||
moduleName: "github.com/fosrl/badger"
|
||||
version: "{{.BadgerVersion}}"
|
||||
version: "v1.3.0"
|
||||
|
||||
log:
|
||||
level: "INFO"
|
||||
level: "DEBUG"
|
||||
format: "common"
|
||||
maxSize: 100
|
||||
maxBackups: 3
|
||||
maxAge: 3
|
||||
compress: true
|
||||
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
acme:
|
||||
httpChallenge:
|
||||
entryPoint: web
|
||||
email: "{{.LetsEncryptEmail}}"
|
||||
storage: "/letsencrypt/acme.json"
|
||||
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
websecure:
|
||||
address: ":443"
|
||||
address: ":9443"
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: "30m"
|
||||
http:
|
||||
tls:
|
||||
certResolver: "letsencrypt"
|
||||
encodedCharacters:
|
||||
allowEncodedSlash: true
|
||||
allowEncodedQuestionMark: true
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
|
||||
ping:
|
||||
entryPoint: "web"
|
||||
|
||||
@@ -4,12 +4,6 @@ services:
|
||||
image: fosrl/pangolin:latest
|
||||
container_name: pangolin
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1g
|
||||
reservations:
|
||||
memory: 256m
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
healthcheck:
|
||||
|
||||
@@ -7,8 +7,8 @@ services:
|
||||
POSTGRES_DB: postgres # Default database name
|
||||
POSTGRES_USER: postgres # Default user
|
||||
POSTGRES_PASSWORD: password # Default password (change for production!)
|
||||
# volumes:
|
||||
# - ./config/postgres:/var/lib/postgresql/data
|
||||
volumes:
|
||||
- ./config/postgres:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432" # Map host port 5432 to container port 5432
|
||||
restart: no
|
||||
|
||||
+2
-8
@@ -6,12 +6,6 @@ import path from "path";
|
||||
import fs from "fs";
|
||||
// import { glob } from "glob";
|
||||
|
||||
// Read default build type from server/build.ts
|
||||
let build = "oss";
|
||||
const buildFile = fs.readFileSync(path.resolve("server/build.ts"), "utf8");
|
||||
const m = buildFile.match(/export\s+const\s+build\s*=\s*["'](oss|saas|enterprise)["']/);
|
||||
if (m) build = m[1];
|
||||
|
||||
const banner = `
|
||||
// patch __dirname
|
||||
// import { fileURLToPath } from "url";
|
||||
@@ -43,7 +37,7 @@ const argv = yargs(hideBin(process.argv))
|
||||
describe: "Build type (oss, saas, enterprise)",
|
||||
type: "string",
|
||||
choices: ["oss", "saas", "enterprise"],
|
||||
default: build
|
||||
default: "oss"
|
||||
})
|
||||
.help()
|
||||
.alias("help", "h").argv;
|
||||
@@ -281,7 +275,7 @@ esbuild
|
||||
})
|
||||
],
|
||||
sourcemap: "inline",
|
||||
target: "node24"
|
||||
target: "node22"
|
||||
})
|
||||
.then((result) => {
|
||||
// Check if there were any errors in the build result
|
||||
|
||||
+34
-17
@@ -1,24 +1,41 @@
|
||||
all: go-build-release
|
||||
|
||||
# Build with version injection via ldflags
|
||||
# Versions can be passed via: make go-build-release PANGOLIN_VERSION=x.x.x GERBIL_VERSION=x.x.x BADGER_VERSION=x.x.x
|
||||
# Or fetched automatically if not provided (requires curl and jq)
|
||||
|
||||
PANGOLIN_VERSION ?= $(shell curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name')
|
||||
GERBIL_VERSION ?= $(shell curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name')
|
||||
BADGER_VERSION ?= $(shell curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name')
|
||||
|
||||
LDFLAGS = -X main.pangolinVersion=$(PANGOLIN_VERSION) \
|
||||
-X main.gerbilVersion=$(GERBIL_VERSION) \
|
||||
-X main.badgerVersion=$(BADGER_VERSION)
|
||||
all: update-versions go-build-release put-back
|
||||
dev-all: dev-update-versions dev-build dev-clean
|
||||
|
||||
go-build-release:
|
||||
@echo "Building with versions - Pangolin: $(PANGOLIN_VERSION), Gerbil: $(GERBIL_VERSION), Badger: $(BADGER_VERSION)"
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/installer_linux_amd64
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/installer_linux_arm64
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/installer_linux_amd64
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/installer_linux_arm64
|
||||
|
||||
clean:
|
||||
rm -f bin/installer_linux_amd64
|
||||
rm -f bin/installer_linux_arm64
|
||||
|
||||
.PHONY: all go-build-release clean
|
||||
update-versions:
|
||||
@echo "Fetching latest versions..."
|
||||
cp main.go main.go.bak && \
|
||||
$(MAKE) dev-update-versions
|
||||
|
||||
put-back:
|
||||
mv main.go.bak main.go
|
||||
|
||||
dev-update-versions:
|
||||
if [ -z "$(tag)" ]; then \
|
||||
PANGOLIN_VERSION=$$(curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name'); \
|
||||
else \
|
||||
PANGOLIN_VERSION=$(tag); \
|
||||
fi && \
|
||||
GERBIL_VERSION=$$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name') && \
|
||||
BADGER_VERSION=$$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name') && \
|
||||
echo "Latest versions - Pangolin: $$PANGOLIN_VERSION, Gerbil: $$GERBIL_VERSION, Badger: $$BADGER_VERSION" && \
|
||||
sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"$$PANGOLIN_VERSION\"/" main.go && \
|
||||
sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"$$GERBIL_VERSION\"/" main.go && \
|
||||
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$$BADGER_VERSION\"/" main.go && \
|
||||
echo "Updated main.go with latest versions"
|
||||
|
||||
dev-build: go-build-release
|
||||
|
||||
dev-clean:
|
||||
@echo "Restoring version values ..."
|
||||
sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"replaceme\"/" main.go && \
|
||||
sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"replaceme\"/" main.go && \
|
||||
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"replaceme\"/" main.go
|
||||
@echo "Restored version strings in main.go"
|
||||
|
||||
+26
-25
@@ -99,6 +99,11 @@ func ReadAppConfig(configPath string) (*AppConfigValues, error) {
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// findPattern finds the start of a pattern in a string
|
||||
func findPattern(s, pattern string) int {
|
||||
return bytes.Index([]byte(s), []byte(pattern))
|
||||
}
|
||||
|
||||
func copyDockerService(sourceFile, destFile, serviceName string) error {
|
||||
// Read source file
|
||||
sourceData, err := os.ReadFile(sourceFile)
|
||||
@@ -113,19 +118,19 @@ func copyDockerService(sourceFile, destFile, serviceName string) error {
|
||||
}
|
||||
|
||||
// Parse source Docker Compose YAML
|
||||
var sourceCompose map[string]any
|
||||
var sourceCompose map[string]interface{}
|
||||
if err := yaml.Unmarshal(sourceData, &sourceCompose); err != nil {
|
||||
return fmt.Errorf("error parsing source Docker Compose file: %w", err)
|
||||
}
|
||||
|
||||
// Parse destination Docker Compose YAML
|
||||
var destCompose map[string]any
|
||||
var destCompose map[string]interface{}
|
||||
if err := yaml.Unmarshal(destData, &destCompose); err != nil {
|
||||
return fmt.Errorf("error parsing destination Docker Compose file: %w", err)
|
||||
}
|
||||
|
||||
// Get services section from source
|
||||
sourceServices, ok := sourceCompose["services"].(map[string]any)
|
||||
sourceServices, ok := sourceCompose["services"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("services section not found in source file or has invalid format")
|
||||
}
|
||||
@@ -137,10 +142,10 @@ func copyDockerService(sourceFile, destFile, serviceName string) error {
|
||||
}
|
||||
|
||||
// Get or create services section in destination
|
||||
destServices, ok := destCompose["services"].(map[string]any)
|
||||
destServices, ok := destCompose["services"].(map[string]interface{})
|
||||
if !ok {
|
||||
// If services section doesn't exist, create it
|
||||
destServices = make(map[string]any)
|
||||
destServices = make(map[string]interface{})
|
||||
destCompose["services"] = destServices
|
||||
}
|
||||
|
||||
@@ -182,21 +187,17 @@ func backupConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func MarshalYAMLWithIndent(data any, indent int) (resp []byte, err error) {
|
||||
func MarshalYAMLWithIndent(data interface{}, indent int) ([]byte, error) {
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := yaml.NewEncoder(buffer)
|
||||
encoder.SetIndent(indent)
|
||||
|
||||
if err := encoder.Encode(data); err != nil {
|
||||
err := encoder.Encode(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if cerr := encoder.Close(); cerr != nil && err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
|
||||
defer encoder.Close()
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
@@ -208,7 +209,7 @@ func replaceInFile(filepath, oldStr, newStr string) error {
|
||||
}
|
||||
|
||||
// Replace the string
|
||||
newContent := strings.ReplaceAll(string(content), oldStr, newStr)
|
||||
newContent := strings.Replace(string(content), oldStr, newStr, -1)
|
||||
|
||||
// Write the modified content back to the file
|
||||
err = os.WriteFile(filepath, []byte(newContent), 0644)
|
||||
@@ -227,28 +228,28 @@ func CheckAndAddTraefikLogVolume(composePath string) error {
|
||||
}
|
||||
|
||||
// Parse YAML into a generic map
|
||||
var compose map[string]any
|
||||
var compose map[string]interface{}
|
||||
if err := yaml.Unmarshal(data, &compose); err != nil {
|
||||
return fmt.Errorf("error parsing compose file: %w", err)
|
||||
}
|
||||
|
||||
// Get services section
|
||||
services, ok := compose["services"].(map[string]any)
|
||||
services, ok := compose["services"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("services section not found or invalid")
|
||||
}
|
||||
|
||||
// Get traefik service
|
||||
traefik, ok := services["traefik"].(map[string]any)
|
||||
traefik, ok := services["traefik"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("traefik service not found or invalid")
|
||||
}
|
||||
|
||||
// Check volumes
|
||||
logVolume := "./config/traefik/logs:/var/log/traefik"
|
||||
var volumes []any
|
||||
var volumes []interface{}
|
||||
|
||||
if existingVolumes, ok := traefik["volumes"].([]any); ok {
|
||||
if existingVolumes, ok := traefik["volumes"].([]interface{}); ok {
|
||||
// Check if volume already exists
|
||||
for _, v := range existingVolumes {
|
||||
if v.(string) == logVolume {
|
||||
@@ -294,13 +295,13 @@ func MergeYAML(baseFile, overlayFile string) error {
|
||||
}
|
||||
|
||||
// Parse base YAML into a map
|
||||
var baseMap map[string]any
|
||||
var baseMap map[string]interface{}
|
||||
if err := yaml.Unmarshal(baseContent, &baseMap); err != nil {
|
||||
return fmt.Errorf("error parsing base YAML: %v", err)
|
||||
}
|
||||
|
||||
// Parse overlay YAML into a map
|
||||
var overlayMap map[string]any
|
||||
var overlayMap map[string]interface{}
|
||||
if err := yaml.Unmarshal(overlayContent, &overlayMap); err != nil {
|
||||
return fmt.Errorf("error parsing overlay YAML: %v", err)
|
||||
}
|
||||
@@ -323,8 +324,8 @@ func MergeYAML(baseFile, overlayFile string) error {
|
||||
}
|
||||
|
||||
// mergeMap recursively merges two maps
|
||||
func mergeMap(base, overlay map[string]any) map[string]any {
|
||||
result := make(map[string]any)
|
||||
func mergeMap(base, overlay map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
// Copy all key-values from base map
|
||||
for k, v := range base {
|
||||
@@ -335,8 +336,8 @@ func mergeMap(base, overlay map[string]any) map[string]any {
|
||||
for k, v := range overlay {
|
||||
// If both maps have the same key and both values are maps, merge recursively
|
||||
if baseVal, ok := base[k]; ok {
|
||||
if baseMap, isBaseMap := baseVal.(map[string]any); isBaseMap {
|
||||
if overlayMap, isOverlayMap := v.(map[string]any); isOverlayMap {
|
||||
if baseMap, isBaseMap := baseVal.(map[string]interface{}); isBaseMap {
|
||||
if overlayMap, isOverlayMap := v.(map[string]interface{}); isOverlayMap {
|
||||
result[k] = mergeMap(baseMap, overlayMap)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -81,19 +81,11 @@ entryPoints:
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: "30m"
|
||||
http3:
|
||||
advertisedPort: 443
|
||||
http:
|
||||
tls:
|
||||
certResolver: "letsencrypt"
|
||||
middlewares:
|
||||
middlewares:
|
||||
- crowdsec@file
|
||||
encodedCharacters:
|
||||
allowEncodedSlash: true
|
||||
allowEncodedQuestionMark: true
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
|
||||
ping:
|
||||
entryPoint: "web"
|
||||
insecureSkipVerify: true
|
||||
@@ -4,12 +4,6 @@ services:
|
||||
image: docker.io/fosrl/pangolin:{{if .IsEnterprise}}ee-{{end}}{{.PangolinVersion}}
|
||||
container_name: pangolin
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1g
|
||||
reservations:
|
||||
memory: 256m
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
healthcheck:
|
||||
@@ -38,14 +32,15 @@ services:
|
||||
- 51820:51820/udp
|
||||
- 21820:21820/udp
|
||||
- 443:443
|
||||
- 443:443/udp # For http3 QUIC if desired
|
||||
- 80:80
|
||||
{{end}}
|
||||
traefik:
|
||||
image: docker.io/traefik:v3.6
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
{{if .InstallGerbil}} network_mode: service:gerbil # Ports appear on the gerbil service{{end}}{{if not .InstallGerbil}}
|
||||
{{if .InstallGerbil}}
|
||||
network_mode: service:gerbil # Ports appear on the gerbil service
|
||||
{{end}}{{if not .InstallGerbil}}
|
||||
ports:
|
||||
- 443:443
|
||||
- 80:80
|
||||
|
||||
@@ -40,17 +40,12 @@ entryPoints:
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: "30m"
|
||||
http3:
|
||||
advertisedPort: 443
|
||||
http:
|
||||
tls:
|
||||
certResolver: "letsencrypt"
|
||||
encodedCharacters:
|
||||
allowEncodedSlash: true
|
||||
allowEncodedQuestionMark: true
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
|
||||
ping:
|
||||
entryPoint: "web"
|
||||
entryPoint: "web"
|
||||
+6
-48
@@ -144,13 +144,12 @@ func installDocker() error {
|
||||
}
|
||||
|
||||
func startDockerService() error {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
if runtime.GOOS == "linux" {
|
||||
cmd := exec.Command("systemctl", "enable", "--now", "docker")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
case "darwin":
|
||||
} else if runtime.GOOS == "darwin" {
|
||||
// On macOS, Docker is usually started via the Docker Desktop application
|
||||
fmt.Println("Please start Docker Desktop manually on macOS.")
|
||||
return nil
|
||||
@@ -211,47 +210,6 @@ func isDockerRunning() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func isPodmanRunning() bool {
|
||||
cmd := exec.Command("podman", "info")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// detectContainerType detects whether the system is currently using Docker or Podman
|
||||
// by checking which container runtime is running and has containers
|
||||
func detectContainerType() SupportedContainer {
|
||||
// Check if we have running containers with podman
|
||||
if isPodmanRunning() {
|
||||
cmd := exec.Command("podman", "ps", "-q")
|
||||
output, err := cmd.Output()
|
||||
if err == nil && len(strings.TrimSpace(string(output))) > 0 {
|
||||
return Podman
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have running containers with docker
|
||||
if isDockerRunning() {
|
||||
cmd := exec.Command("docker", "ps", "-q")
|
||||
output, err := cmd.Output()
|
||||
if err == nil && len(strings.TrimSpace(string(output))) > 0 {
|
||||
return Docker
|
||||
}
|
||||
}
|
||||
|
||||
// If no containers are running, check which one is installed and running
|
||||
if isPodmanRunning() && isPodmanInstalled() {
|
||||
return Podman
|
||||
}
|
||||
|
||||
if isDockerRunning() && isDockerInstalled() {
|
||||
return Docker
|
||||
}
|
||||
|
||||
return Undefined
|
||||
}
|
||||
|
||||
// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied
|
||||
func executeDockerComposeCommandWithArgs(args ...string) error {
|
||||
var cmd *exec.Cmd
|
||||
@@ -303,7 +261,7 @@ func pullContainers(containerType SupportedContainer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("unsupported container type: %s", containerType)
|
||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
||||
}
|
||||
|
||||
// startContainers starts the containers using the appropriate command.
|
||||
@@ -326,7 +284,7 @@ func startContainers(containerType SupportedContainer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("unsupported container type: %s", containerType)
|
||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
||||
}
|
||||
|
||||
// stopContainers stops the containers using the appropriate command.
|
||||
@@ -348,7 +306,7 @@ func stopContainers(containerType SupportedContainer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("unsupported container type: %s", containerType)
|
||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
||||
}
|
||||
|
||||
// restartContainer restarts a specific container using the appropriate command.
|
||||
@@ -370,5 +328,5 @@ func restartContainer(container string, containerType SupportedContainer) error
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("unsupported container type: %s", containerType)
|
||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
||||
}
|
||||
|
||||
+12
-21
@@ -27,18 +27,9 @@ func installCrowdsec(config Config) error {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll("config/crowdsec/db", 0755); err != nil {
|
||||
fmt.Printf("Error creating config files: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := os.MkdirAll("config/crowdsec/acquis.d", 0755); err != nil {
|
||||
fmt.Printf("Error creating config files: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := os.MkdirAll("config/traefik/logs", 0755); err != nil {
|
||||
fmt.Printf("Error creating config files: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.MkdirAll("config/crowdsec/db", 0755)
|
||||
os.MkdirAll("config/crowdsec/acquis.d", 0755)
|
||||
os.MkdirAll("config/traefik/logs", 0755)
|
||||
|
||||
if err := copyDockerService("config/crowdsec/docker-compose.yml", "docker-compose.yml", "crowdsec"); err != nil {
|
||||
fmt.Printf("Error copying docker service: %v\n", err)
|
||||
@@ -102,7 +93,7 @@ func installCrowdsec(config Config) error {
|
||||
|
||||
if checkIfTextInFile("config/traefik/dynamic_config.yml", "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK") {
|
||||
fmt.Println("Failed to replace bouncer key! Please retrieve the key and replace it in the config/traefik/dynamic_config.yml file using the following command:")
|
||||
fmt.Printf(" %s exec crowdsec cscli bouncers add traefik-bouncer\n", config.InstallationContainerType)
|
||||
fmt.Println(" docker exec crowdsec cscli bouncers add traefik-bouncer")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -126,7 +117,7 @@ func GetCrowdSecAPIKey(containerType SupportedContainer) (string, error) {
|
||||
}
|
||||
|
||||
// Execute the command to get the API key
|
||||
cmd := exec.Command(string(containerType), "exec", "crowdsec", "cscli", "bouncers", "add", "traefik-bouncer", "-o", "raw")
|
||||
cmd := exec.Command("docker", "exec", "crowdsec", "cscli", "bouncers", "add", "traefik-bouncer", "-o", "raw")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
|
||||
@@ -162,34 +153,34 @@ func CheckAndAddCrowdsecDependency(composePath string) error {
|
||||
}
|
||||
|
||||
// Parse YAML into a generic map
|
||||
var compose map[string]any
|
||||
var compose map[string]interface{}
|
||||
if err := yaml.Unmarshal(data, &compose); err != nil {
|
||||
return fmt.Errorf("error parsing compose file: %w", err)
|
||||
}
|
||||
|
||||
// Get services section
|
||||
services, ok := compose["services"].(map[string]any)
|
||||
services, ok := compose["services"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("services section not found or invalid")
|
||||
}
|
||||
|
||||
// Get traefik service
|
||||
traefik, ok := services["traefik"].(map[string]any)
|
||||
traefik, ok := services["traefik"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("traefik service not found or invalid")
|
||||
}
|
||||
|
||||
// Get dependencies
|
||||
dependsOn, ok := traefik["depends_on"].(map[string]any)
|
||||
dependsOn, ok := traefik["depends_on"].(map[string]interface{})
|
||||
if ok {
|
||||
// Append the new block for crowdsec
|
||||
dependsOn["crowdsec"] = map[string]any{
|
||||
dependsOn["crowdsec"] = map[string]interface{}{
|
||||
"condition": "service_healthy",
|
||||
}
|
||||
} else {
|
||||
// No dependencies exist, create it
|
||||
traefik["depends_on"] = map[string]any{
|
||||
"crowdsec": map[string]any{
|
||||
traefik["depends_on"] = map[string]interface{}{
|
||||
"crowdsec": map[string]interface{}{
|
||||
"condition": "service_healthy",
|
||||
},
|
||||
}
|
||||
|
||||
+3
-31
@@ -1,38 +1,10 @@
|
||||
module installer
|
||||
|
||||
go 1.25.0
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/huh v1.0.0
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
golang.org/x/term v0.41.0
|
||||
golang.org/x/term v0.38.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/catppuccin/go v0.3.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
|
||||
github.com/charmbracelet/bubbletea v1.3.6 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/x/ansi v0.9.3 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
)
|
||||
require golang.org/x/sys v0.39.0 // indirect
|
||||
|
||||
+4
-77
@@ -1,80 +1,7 @@
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
|
||||
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
|
||||
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw=
|
||||
github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
|
||||
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
|
||||
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
|
||||
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
+66
-182
@@ -1,208 +1,92 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// pangolinTheme is the custom theme using brand colors
|
||||
var pangolinTheme = ThemePangolin()
|
||||
|
||||
// isAccessibleMode checks if we should use accessible mode (simple prompts)
|
||||
// This is true for: non-TTY, TERM=dumb, or ACCESSIBLE env var set
|
||||
func isAccessibleMode() bool {
|
||||
// Check if stdin is not a terminal (piped input, CI, etc.)
|
||||
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
return true
|
||||
}
|
||||
// Check for dumb terminal
|
||||
if os.Getenv("TERM") == "dumb" {
|
||||
return true
|
||||
}
|
||||
// Check for explicit accessible mode request
|
||||
if os.Getenv("ACCESSIBLE") != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// handleAbort checks if the error is a user abort (Ctrl+C) and exits if so
|
||||
func handleAbort(err error) {
|
||||
if err != nil && errors.Is(err, huh.ErrUserAborted) {
|
||||
fmt.Println("\nInstallation cancelled.")
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// runField runs a single field with the Pangolin theme, handling accessible mode
|
||||
func runField(field huh.Field) error {
|
||||
if isAccessibleMode() {
|
||||
return field.RunAccessible(os.Stdout, os.Stdin)
|
||||
}
|
||||
form := huh.NewForm(huh.NewGroup(field)).WithTheme(pangolinTheme)
|
||||
return form.Run()
|
||||
}
|
||||
|
||||
func readString(prompt string, defaultValue string) string {
|
||||
var value string
|
||||
|
||||
title := prompt
|
||||
func readString(reader *bufio.Reader, prompt string, defaultValue string) string {
|
||||
if defaultValue != "" {
|
||||
title = fmt.Sprintf("%s (default: %s)", prompt, defaultValue)
|
||||
fmt.Printf("%s (default: %s): ", prompt, defaultValue)
|
||||
} else {
|
||||
fmt.Print(prompt + ": ")
|
||||
}
|
||||
|
||||
input := huh.NewInput().
|
||||
Title(title).
|
||||
Value(&value)
|
||||
|
||||
// If no default value, this field is required
|
||||
if defaultValue == "" {
|
||||
input = input.Validate(func(s string) error {
|
||||
if s == "" {
|
||||
return fmt.Errorf("this field is required")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
err := runField(input)
|
||||
handleAbort(err)
|
||||
|
||||
if value == "" {
|
||||
value = defaultValue
|
||||
}
|
||||
|
||||
// Print the answer so it remains visible in terminal history (skip in accessible mode as it already shows)
|
||||
if !isAccessibleMode() {
|
||||
fmt.Printf("%s: %s\n", prompt, value)
|
||||
}
|
||||
|
||||
return value
|
||||
return input
|
||||
}
|
||||
|
||||
func readPassword(prompt string) string {
|
||||
var value string
|
||||
func readStringNoDefault(reader *bufio.Reader, prompt string) string {
|
||||
fmt.Print(prompt + ": ")
|
||||
input, _ := reader.ReadString('\n')
|
||||
return strings.TrimSpace(input)
|
||||
}
|
||||
|
||||
func readPassword(prompt string, reader *bufio.Reader) string {
|
||||
if term.IsTerminal(int(syscall.Stdin)) {
|
||||
fmt.Print(prompt + ": ")
|
||||
// Read password without echo if we're in a terminal
|
||||
password, err := term.ReadPassword(int(syscall.Stdin))
|
||||
fmt.Println() // Add a newline since ReadPassword doesn't add one
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
input := strings.TrimSpace(string(password))
|
||||
if input == "" {
|
||||
return readPassword(prompt, reader)
|
||||
}
|
||||
return input
|
||||
} else {
|
||||
// Fallback to reading from stdin if not in a terminal
|
||||
return readString(reader, prompt, "")
|
||||
}
|
||||
}
|
||||
|
||||
func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
|
||||
defaultStr := "no"
|
||||
if defaultValue {
|
||||
defaultStr = "yes"
|
||||
}
|
||||
for {
|
||||
input := huh.NewInput().
|
||||
Title(prompt).
|
||||
Value(&value).
|
||||
EchoMode(huh.EchoModePassword).
|
||||
Validate(func(s string) error {
|
||||
if s == "" {
|
||||
return fmt.Errorf("password is required")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
err := runField(input)
|
||||
handleAbort(err)
|
||||
|
||||
if value != "" {
|
||||
// Print confirmation without revealing the password
|
||||
if !isAccessibleMode() {
|
||||
fmt.Printf("%s: %s\n", prompt, "********")
|
||||
}
|
||||
return value
|
||||
input := readString(reader, prompt+" (yes/no)", defaultStr)
|
||||
lower := strings.ToLower(input)
|
||||
if lower == "yes" {
|
||||
return true
|
||||
} else if lower == "no" {
|
||||
return false
|
||||
} else {
|
||||
fmt.Println("Please enter 'yes' or 'no'.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readBool(prompt string, defaultValue bool) bool {
|
||||
var value = defaultValue
|
||||
|
||||
confirm := huh.NewConfirm().
|
||||
Title(prompt).
|
||||
Value(&value).
|
||||
Affirmative("Yes").
|
||||
Negative("No")
|
||||
|
||||
err := runField(confirm)
|
||||
handleAbort(err)
|
||||
|
||||
// Print the answer so it remains visible in terminal history
|
||||
if !isAccessibleMode() {
|
||||
answer := "No"
|
||||
if value {
|
||||
answer = "Yes"
|
||||
func readBoolNoDefault(reader *bufio.Reader, prompt string) bool {
|
||||
for {
|
||||
input := readStringNoDefault(reader, prompt+" (yes/no)")
|
||||
lower := strings.ToLower(input)
|
||||
if lower == "yes" {
|
||||
return true
|
||||
} else if lower == "no" {
|
||||
return false
|
||||
} else {
|
||||
fmt.Println("Please enter 'yes' or 'no'.")
|
||||
}
|
||||
fmt.Printf("%s: %s\n", prompt, answer)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func readBoolNoDefault(prompt string) bool {
|
||||
var value bool
|
||||
|
||||
confirm := huh.NewConfirm().
|
||||
Title(prompt).
|
||||
Value(&value).
|
||||
Affirmative("Yes").
|
||||
Negative("No")
|
||||
|
||||
err := runField(confirm)
|
||||
handleAbort(err)
|
||||
|
||||
// Print the answer so it remains visible in terminal history
|
||||
if !isAccessibleMode() {
|
||||
answer := "No"
|
||||
if value {
|
||||
answer = "Yes"
|
||||
}
|
||||
fmt.Printf("%s: %s\n", prompt, answer)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func readInt(prompt string, defaultValue int) int {
|
||||
var value string
|
||||
|
||||
title := fmt.Sprintf("%s (default: %d)", prompt, defaultValue)
|
||||
|
||||
input := huh.NewInput().
|
||||
Title(title).
|
||||
Value(&value).
|
||||
Validate(func(s string) error {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
_, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("please enter a valid number")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
err := runField(input)
|
||||
handleAbort(err)
|
||||
|
||||
if value == "" {
|
||||
// Print the answer so it remains visible in terminal history
|
||||
if !isAccessibleMode() {
|
||||
fmt.Printf("%s: %d\n", prompt, defaultValue)
|
||||
}
|
||||
func readInt(reader *bufio.Reader, prompt string, defaultValue int) int {
|
||||
input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue))
|
||||
if input == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
result, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
if !isAccessibleMode() {
|
||||
fmt.Printf("%s: %d\n", prompt, defaultValue)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Print the answer so it remains visible in terminal history
|
||||
if !isAccessibleMode() {
|
||||
fmt.Printf("%s: %d\n", prompt, result)
|
||||
}
|
||||
|
||||
return result
|
||||
value := defaultValue
|
||||
fmt.Sscanf(input, "%d", &value)
|
||||
return value
|
||||
}
|
||||
|
||||
+103
-230
@@ -1,35 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"bufio"
|
||||
"embed"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Version variables injected at build time via -ldflags
|
||||
var (
|
||||
pangolinVersion string
|
||||
gerbilVersion string
|
||||
badgerVersion string
|
||||
)
|
||||
|
||||
// DO NOT EDIT THIS FUNCTION; IT MATCHED BY REGEX IN CICD
|
||||
func loadVersions(config *Config) {
|
||||
config.PangolinVersion = pangolinVersion
|
||||
config.GerbilVersion = gerbilVersion
|
||||
config.BadgerVersion = badgerVersion
|
||||
config.PangolinVersion = "replaceme"
|
||||
config.GerbilVersion = "replaceme"
|
||||
config.BadgerVersion = "replaceme"
|
||||
}
|
||||
|
||||
//go:embed config/*
|
||||
@@ -87,19 +81,14 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
var config Config
|
||||
var alreadyInstalled = false
|
||||
|
||||
// Determine installation directory
|
||||
installDir := findOrSelectInstallDirectory()
|
||||
if err := os.Chdir(installDir); err != nil {
|
||||
fmt.Printf("Error changing to installation directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// check if there is already a config file
|
||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
||||
config = collectUserInput()
|
||||
config = collectUserInput(reader)
|
||||
|
||||
loadVersions(&config)
|
||||
config.DoCrowdsecInstall = false
|
||||
@@ -112,10 +101,7 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := moveFile("config/docker-compose.yml", "docker-compose.yml"); err != nil {
|
||||
fmt.Printf("Error moving docker-compose.yml: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
moveFile("config/docker-compose.yml", "docker-compose.yml")
|
||||
|
||||
fmt.Println("\nConfiguration files created successfully!")
|
||||
|
||||
@@ -130,17 +116,13 @@ func main() {
|
||||
|
||||
fmt.Println("\n=== Starting installation ===")
|
||||
|
||||
if readBool("Would you like to install and start the containers?", true) {
|
||||
if readBool(reader, "Would you like to install and start the containers?", true) {
|
||||
|
||||
config.InstallationContainerType = podmanOrDocker()
|
||||
config.InstallationContainerType = podmanOrDocker(reader)
|
||||
|
||||
if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker {
|
||||
if readBool("Docker is not installed. Would you like to install it?", true) {
|
||||
if err := installDocker(); err != nil {
|
||||
fmt.Printf("Error installing Docker: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
||||
installDocker()
|
||||
// try to start docker service but ignore errors
|
||||
if err := startDockerService(); err != nil {
|
||||
fmt.Println("Error starting Docker service:", err)
|
||||
@@ -149,7 +131,7 @@ func main() {
|
||||
}
|
||||
// wait 10 seconds for docker to start checking if docker is running every 2 seconds
|
||||
fmt.Println("Waiting for Docker to start...")
|
||||
for range 5 {
|
||||
for i := 0; i < 5; i++ {
|
||||
if isDockerRunning() {
|
||||
fmt.Println("Docker is running!")
|
||||
break
|
||||
@@ -184,7 +166,7 @@ func main() {
|
||||
fmt.Println("\n=== MaxMind Database Update ===")
|
||||
if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil {
|
||||
fmt.Println("MaxMind GeoLite2 Country database found.")
|
||||
if readBool("Would you like to update the MaxMind database to the latest version?", false) {
|
||||
if readBool(reader, "Would you like to update the MaxMind database to the latest version?", false) {
|
||||
if err := downloadMaxMindDatabase(); err != nil {
|
||||
fmt.Printf("Error updating MaxMind database: %v\n", err)
|
||||
fmt.Println("You can try updating it manually later if needed.")
|
||||
@@ -192,7 +174,7 @@ func main() {
|
||||
}
|
||||
} else {
|
||||
fmt.Println("MaxMind GeoLite2 Country database not found.")
|
||||
if readBool("Would you like to download the MaxMind GeoLite2 database for geoblocking functionality?", false) {
|
||||
if readBool(reader, "Would you like to download the MaxMind GeoLite2 database for geoblocking functionality?", false) {
|
||||
if err := downloadMaxMindDatabase(); err != nil {
|
||||
fmt.Printf("Error downloading MaxMind database: %v\n", err)
|
||||
fmt.Println("You can try downloading it manually later if needed.")
|
||||
@@ -209,11 +191,11 @@ func main() {
|
||||
if !checkIsCrowdsecInstalledInCompose() {
|
||||
fmt.Println("\n=== CrowdSec Install ===")
|
||||
// check if crowdsec is installed
|
||||
if readBool("Would you like to install CrowdSec?", false) {
|
||||
if readBool(reader, "Would you like to install CrowdSec?", false) {
|
||||
fmt.Println("This installer constitutes a minimal viable CrowdSec deployment. CrowdSec will add extra complexity to your Pangolin installation and may not work to the best of its abilities out of the box. Users are expected to implement configuration adjustments on their own to achieve the best security posture. Consult the CrowdSec documentation for detailed configuration instructions.")
|
||||
|
||||
// BUG: crowdsec installation will be skipped if the user chooses to install on the first installation.
|
||||
if readBool("Are you willing to manage CrowdSec?", false) {
|
||||
if readBool(reader, "Are you willing to manage CrowdSec?", false) {
|
||||
if config.DashboardDomain == "" {
|
||||
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml")
|
||||
if err != nil {
|
||||
@@ -242,21 +224,12 @@ func main() {
|
||||
fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail)
|
||||
fmt.Printf("Badger Version: %s\n", config.BadgerVersion)
|
||||
|
||||
if !readBool("Are these values correct?", true) {
|
||||
config = collectUserInput()
|
||||
if !readBool(reader, "Are these values correct?", true) {
|
||||
config = collectUserInput(reader)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to detect container type from existing installation
|
||||
detectedType := detectContainerType()
|
||||
if detectedType == Undefined {
|
||||
// If detection fails, prompt the user
|
||||
fmt.Println("Unable to detect container type from existing installation.")
|
||||
config.InstallationContainerType = podmanOrDocker()
|
||||
} else {
|
||||
config.InstallationContainerType = detectedType
|
||||
fmt.Printf("Detected container type: %s\n", config.InstallationContainerType)
|
||||
}
|
||||
config.InstallationContainerType = podmanOrDocker(reader)
|
||||
|
||||
config.DoCrowdsecInstall = true
|
||||
err := installCrowdsec(config)
|
||||
@@ -294,119 +267,8 @@ func main() {
|
||||
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
||||
}
|
||||
|
||||
func hasExistingInstall(dir string) bool {
|
||||
configPath := filepath.Join(dir, "config", "config.yml")
|
||||
_, err := os.Stat(configPath)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func findOrSelectInstallDirectory() string {
|
||||
const defaultInstallDir = "/opt/pangolin"
|
||||
|
||||
// Get current working directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting current directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 1. Check current directory for existing install
|
||||
if hasExistingInstall(cwd) {
|
||||
fmt.Printf("Found existing Pangolin installation in current directory: %s\n", cwd)
|
||||
return cwd
|
||||
}
|
||||
|
||||
// 2. Check default location (/opt/pangolin) for existing install
|
||||
if cwd != defaultInstallDir && hasExistingInstall(defaultInstallDir) {
|
||||
fmt.Printf("\nFound existing Pangolin installation at: %s\n", defaultInstallDir)
|
||||
if readBool(fmt.Sprintf("Would you like to use the existing installation at %s?", defaultInstallDir), true) {
|
||||
return defaultInstallDir
|
||||
}
|
||||
}
|
||||
|
||||
// 3. No existing install found, prompt for installation directory
|
||||
fmt.Println("\n=== Installation Directory ===")
|
||||
fmt.Println("No existing Pangolin installation detected.")
|
||||
|
||||
installDir := readString("Enter the installation directory", defaultInstallDir)
|
||||
|
||||
// Expand ~ to home directory if present
|
||||
if strings.HasPrefix(installDir, "~") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting home directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
installDir = filepath.Join(home, installDir[1:])
|
||||
}
|
||||
|
||||
// Convert to absolute path
|
||||
absPath, err := filepath.Abs(installDir)
|
||||
if err != nil {
|
||||
fmt.Printf("Error resolving path: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
installDir = absPath
|
||||
|
||||
// Check if directory exists
|
||||
if _, err := os.Stat(installDir); os.IsNotExist(err) {
|
||||
// Directory doesn't exist, create it
|
||||
if readBool(fmt.Sprintf("Directory %s does not exist. Create it?", installDir), true) {
|
||||
if err := os.MkdirAll(installDir, 0755); err != nil {
|
||||
fmt.Printf("Error creating directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Created directory: %s\n", installDir)
|
||||
|
||||
// Offer to change ownership if running via sudo
|
||||
changeDirectoryOwnership(installDir)
|
||||
} else {
|
||||
fmt.Println("Installation cancelled.")
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Installation directory: %s\n", installDir)
|
||||
return installDir
|
||||
}
|
||||
|
||||
func changeDirectoryOwnership(dir string) {
|
||||
// Check if we're running via sudo by looking for SUDO_USER
|
||||
sudoUser := os.Getenv("SUDO_USER")
|
||||
if sudoUser == "" || os.Geteuid() != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sudoUID := os.Getenv("SUDO_UID")
|
||||
sudoGID := os.Getenv("SUDO_GID")
|
||||
|
||||
if sudoUID == "" || sudoGID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\nRunning as root via sudo (original user: %s)\n", sudoUser)
|
||||
if readBool(fmt.Sprintf("Would you like to change ownership of %s to user '%s'? This makes it easier to manage config files without sudo.", dir, sudoUser), true) {
|
||||
uid, err := strconv.Atoi(sudoUID)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Could not parse SUDO_UID: %v\n", err)
|
||||
return
|
||||
}
|
||||
gid, err := strconv.Atoi(sudoGID)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Could not parse SUDO_GID: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Chown(dir, uid, gid); err != nil {
|
||||
fmt.Printf("Warning: Could not change ownership: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Changed ownership of %s to %s\n", dir, sudoUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func podmanOrDocker() SupportedContainer {
|
||||
inputContainer := readString("Would you like to run Pangolin as Docker or Podman containers?", "docker")
|
||||
func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
||||
inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker")
|
||||
|
||||
chosenContainer := Docker
|
||||
if strings.EqualFold(inputContainer, "docker") {
|
||||
@@ -418,17 +280,16 @@ func podmanOrDocker() SupportedContainer {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
switch chosenContainer {
|
||||
case Podman:
|
||||
if chosenContainer == Podman {
|
||||
if !isPodmanInstalled() {
|
||||
fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := exec.Command("bash", "-c", "cat /etc/sysctl.d/99-podman.conf 2>/dev/null | grep 'net.ipv4.ip_unprivileged_port_start=' || cat /etc/sysctl.conf 2>/dev/null | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil {
|
||||
if err := exec.Command("bash", "-c", "cat /etc/sysctl.conf | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil {
|
||||
fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.")
|
||||
fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.")
|
||||
approved := readBool("The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' > /etc/sysctl.d/99-podman.conf && sysctl --system\". Approve?", true)
|
||||
approved := readBool(reader, "The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p\". Approve?", true)
|
||||
if approved {
|
||||
if os.Geteuid() != 0 {
|
||||
fmt.Println("You need to run the installer as root for such a configuration.")
|
||||
@@ -439,8 +300,8 @@ func podmanOrDocker() SupportedContainer {
|
||||
// container low-range ports as unprivileged ports.
|
||||
// Linux only.
|
||||
|
||||
if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' > /etc/sysctl.d/99-podman.conf && sysctl --system"); err != nil {
|
||||
fmt.Printf("Error configuring unprivileged ports: %v\n", err)
|
||||
if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p"); err != nil {
|
||||
fmt.Printf("Error configuring unprivileged ports: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
@@ -450,7 +311,7 @@ func podmanOrDocker() SupportedContainer {
|
||||
fmt.Println("Unprivileged ports have been configured.")
|
||||
}
|
||||
|
||||
case Docker:
|
||||
} else if chosenContainer == Docker {
|
||||
// check if docker is not installed and the user is root
|
||||
if !isDockerInstalled() {
|
||||
if os.Geteuid() != 0 {
|
||||
@@ -465,7 +326,7 @@ func podmanOrDocker() SupportedContainer {
|
||||
fmt.Println("The installer will not be able to run docker commands without running it as root.")
|
||||
os.Exit(1)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
// This shouldn't happen unless there's a third container runtime.
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -473,35 +334,35 @@ func podmanOrDocker() SupportedContainer {
|
||||
return chosenContainer
|
||||
}
|
||||
|
||||
func collectUserInput() Config {
|
||||
func collectUserInput(reader *bufio.Reader) Config {
|
||||
config := Config{}
|
||||
|
||||
// Basic configuration
|
||||
fmt.Println("\n=== Basic Configuration ===")
|
||||
|
||||
config.IsEnterprise = readBoolNoDefault("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("Enter your base domain (no subdomain e.g. example.com)", "")
|
||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||
|
||||
// Set default dashboard domain after base domain is collected
|
||||
defaultDashboardDomain := ""
|
||||
if config.BaseDomain != "" {
|
||||
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
||||
}
|
||||
config.DashboardDomain = readString("Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
||||
config.LetsEncryptEmail = readString("Enter email for Let's Encrypt certificates", "")
|
||||
config.InstallGerbil = readBool("Do you want to use Gerbil to allow tunneled connections", true)
|
||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
||||
|
||||
// Email configuration
|
||||
fmt.Println("\n=== Email Configuration ===")
|
||||
config.EnableEmail = readBool("Enable email functionality (SMTP)", false)
|
||||
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
||||
|
||||
if config.EnableEmail {
|
||||
config.EmailSMTPHost = readString("Enter SMTP host", "")
|
||||
config.EmailSMTPPort = readInt("Enter SMTP port (default 587)", 587)
|
||||
config.EmailSMTPUser = readString("Enter SMTP username", "")
|
||||
config.EmailSMTPPass = readPassword("Enter SMTP password")
|
||||
config.EmailNoReply = readString("Enter no-reply email address (often the same as SMTP username)", "")
|
||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
||||
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
||||
config.EmailNoReply = readString(reader, "Enter no-reply email address (often the same as SMTP username)", "")
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
@@ -522,8 +383,8 @@ func collectUserInput() Config {
|
||||
|
||||
fmt.Println("\n=== Advanced Configuration ===")
|
||||
|
||||
config.EnableIPv6 = readBool("Is your server IPv6 capable?", true)
|
||||
config.EnableGeoblocking = readBool("Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", true)
|
||||
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
|
||||
config.EnableGeoblocking = readBool(reader, "Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", true)
|
||||
|
||||
if config.DashboardDomain == "" {
|
||||
fmt.Println("Error: Dashboard Domain name is required")
|
||||
@@ -534,23 +395,15 @@ func collectUserInput() Config {
|
||||
}
|
||||
|
||||
func createConfigFiles(config Config) error {
|
||||
if err := os.MkdirAll("config", 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll("config/letsencrypt", 0755); err != nil {
|
||||
return fmt.Errorf("failed to create letsencrypt directory: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll("config/db", 0755); err != nil {
|
||||
return fmt.Errorf("failed to create db directory: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll("config/logs", 0755); err != nil {
|
||||
return fmt.Errorf("failed to create logs directory: %v", err)
|
||||
}
|
||||
os.MkdirAll("config", 0755)
|
||||
os.MkdirAll("config/letsencrypt", 0755)
|
||||
os.MkdirAll("config/db", 0755)
|
||||
os.MkdirAll("config/logs", 0755)
|
||||
|
||||
// Walk through all embedded files
|
||||
err := fs.WalkDir(configFiles, "config", func(path string, d fs.DirEntry, walkErr error) (err error) {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
err := fs.WalkDir(configFiles, "config", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Skip the root fs directory itself
|
||||
@@ -601,11 +454,7 @@ func createConfigFiles(config Config) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create %s: %v", path, err)
|
||||
}
|
||||
defer func() {
|
||||
if cerr := outFile.Close(); cerr != nil && err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
defer outFile.Close()
|
||||
|
||||
// Execute template
|
||||
if err := tmpl.Execute(outFile, config); err != nil {
|
||||
@@ -621,26 +470,18 @@ func createConfigFiles(config Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) (err error) {
|
||||
func copyFile(src, dst string) error {
|
||||
source, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if cerr := source.Close(); cerr != nil && err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
defer source.Close()
|
||||
|
||||
destination, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if cerr := destination.Close(); cerr != nil && err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
defer destination.Close()
|
||||
|
||||
_, err = io.Copy(destination, source)
|
||||
return err
|
||||
@@ -711,24 +552,22 @@ func showSetupTokenInstructions(containerType SupportedContainer, dashboardDomai
|
||||
fmt.Println("To get your setup token, you need to:")
|
||||
fmt.Println("")
|
||||
fmt.Println("1. Start the containers")
|
||||
switch containerType {
|
||||
case Docker:
|
||||
if containerType == Docker {
|
||||
fmt.Println(" docker compose up -d")
|
||||
case Podman:
|
||||
} else if containerType == Podman {
|
||||
fmt.Println(" podman-compose up -d")
|
||||
} else {
|
||||
}
|
||||
|
||||
fmt.Println("")
|
||||
fmt.Println("2. Wait for the Pangolin container to start and generate the token")
|
||||
fmt.Println("")
|
||||
fmt.Println("3. Check the container logs for the setup token")
|
||||
switch containerType {
|
||||
case Docker:
|
||||
if containerType == Docker {
|
||||
fmt.Println(" docker logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
|
||||
case Podman:
|
||||
} else if containerType == Podman {
|
||||
fmt.Println(" podman logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
|
||||
} else {
|
||||
}
|
||||
|
||||
fmt.Println("")
|
||||
fmt.Println("4. Look for output like")
|
||||
fmt.Println(" === SETUP TOKEN GENERATED ===")
|
||||
@@ -744,12 +583,43 @@ func showSetupTokenInstructions(containerType SupportedContainer, dashboardDomai
|
||||
}
|
||||
|
||||
func generateRandomSecretKey() string {
|
||||
secret := make([]byte, 32)
|
||||
_, err := rand.Read(secret)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to generate random secret key: %v", err))
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
const length = 32
|
||||
|
||||
var seededRand *rand.Rand = rand.New(
|
||||
rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[seededRand.Intn(len(charset))]
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(secret)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func getPublicIP() string {
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Get("https://ifconfig.io/ip")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
ip := strings.TrimSpace(string(body))
|
||||
|
||||
// Validate that it's a valid IP address
|
||||
if net.ParseIP(ip) != nil {
|
||||
return ip
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Run external commands with stdio/stderr attached.
|
||||
@@ -764,7 +634,10 @@ func checkPortsAvailable(port int) error {
|
||||
addr := fmt.Sprintf(":%d", port)
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ERROR: port %d is occupied or cannot be bound: %w", port, err)
|
||||
return fmt.Errorf(
|
||||
"ERROR: port %d is occupied or cannot be bound: %w\n\n",
|
||||
port, err,
|
||||
)
|
||||
}
|
||||
if closeErr := ln.Close(); closeErr != nil {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// Pangolin brand colors (converted from oklch to hex)
|
||||
var (
|
||||
// Primary orange/amber - oklch(0.6717 0.1946 41.93)
|
||||
primaryColor = lipgloss.AdaptiveColor{Light: "#D97706", Dark: "#F59E0B"}
|
||||
// Muted foreground
|
||||
mutedColor = lipgloss.AdaptiveColor{Light: "#737373", Dark: "#A3A3A3"}
|
||||
// Success green
|
||||
successColor = lipgloss.AdaptiveColor{Light: "#16A34A", Dark: "#22C55E"}
|
||||
// Error red - oklch(0.577 0.245 27.325)
|
||||
errorColor = lipgloss.AdaptiveColor{Light: "#DC2626", Dark: "#EF4444"}
|
||||
// Normal text
|
||||
normalFg = lipgloss.AdaptiveColor{Light: "#171717", Dark: "#FAFAFA"}
|
||||
)
|
||||
|
||||
// ThemePangolin returns a huh theme using Pangolin brand colors
|
||||
func ThemePangolin() *huh.Theme {
|
||||
t := huh.ThemeBase()
|
||||
|
||||
// Focused state styles
|
||||
t.Focused.Base = t.Focused.Base.BorderForeground(primaryColor)
|
||||
t.Focused.Title = t.Focused.Title.Foreground(primaryColor).Bold(true)
|
||||
t.Focused.Description = t.Focused.Description.Foreground(mutedColor)
|
||||
t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(errorColor)
|
||||
t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(errorColor)
|
||||
t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(primaryColor)
|
||||
t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(primaryColor)
|
||||
t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(primaryColor)
|
||||
t.Focused.Option = t.Focused.Option.Foreground(normalFg)
|
||||
t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(primaryColor)
|
||||
t.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(successColor).SetString("✓ ")
|
||||
t.Focused.UnselectedPrefix = lipgloss.NewStyle().Foreground(mutedColor).SetString(" ")
|
||||
t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(lipgloss.Color("#FFFFFF")).Background(primaryColor)
|
||||
t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(normalFg).Background(lipgloss.AdaptiveColor{Light: "#E5E5E5", Dark: "#404040"})
|
||||
t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(primaryColor)
|
||||
t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(primaryColor)
|
||||
|
||||
// Blurred state inherits from focused but with hidden border
|
||||
t.Blurred = t.Focused
|
||||
t.Blurred.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder())
|
||||
t.Blurred.Title = t.Blurred.Title.Foreground(mutedColor).Bold(false)
|
||||
t.Blurred.TextInput.Prompt = t.Blurred.TextInput.Prompt.Foreground(mutedColor)
|
||||
|
||||
return t
|
||||
}
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
# --- Configuration ---
|
||||
# The header text to be added to the files.
|
||||
HEADER_TEXT = """/*
|
||||
* 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.
|
||||
*/
|
||||
"""
|
||||
|
||||
def should_add_header(file_path):
|
||||
"""
|
||||
Checks if a file should receive the commercial license header.
|
||||
Returns True if 'private' is in the path or file content.
|
||||
"""
|
||||
# Check if 'private' is in the file path (case-insensitive)
|
||||
if 'server/private' in file_path.lower():
|
||||
return True
|
||||
|
||||
# Check if 'private' is in the file content (case-insensitive)
|
||||
# try:
|
||||
# with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
# content = f.read()
|
||||
# if 'private' in content.lower():
|
||||
# return True
|
||||
# except Exception as e:
|
||||
# print(f"Could not read file {file_path}: {e}")
|
||||
|
||||
return False
|
||||
|
||||
def process_directory(root_dir):
|
||||
"""
|
||||
Recursively scans a directory and adds headers to qualifying .ts or .tsx files,
|
||||
skipping any 'node_modules' directories.
|
||||
"""
|
||||
print(f"Scanning directory: {root_dir}")
|
||||
files_processed = 0
|
||||
headers_added = 0
|
||||
|
||||
for root, dirs, files in os.walk(root_dir):
|
||||
# --- MODIFICATION ---
|
||||
# Exclude 'node_modules' directories from the scan to improve performance.
|
||||
if 'node_modules' in dirs:
|
||||
dirs.remove('node_modules')
|
||||
|
||||
for file in files:
|
||||
if file.endswith('.ts') or file.endswith('.tsx'):
|
||||
file_path = os.path.join(root, file)
|
||||
files_processed += 1
|
||||
|
||||
try:
|
||||
with open(file_path, 'r+', encoding='utf-8') as f:
|
||||
original_content = f.read()
|
||||
has_header = original_content.startswith(HEADER_TEXT.strip())
|
||||
|
||||
if should_add_header(file_path):
|
||||
# Add header only if it's not already there
|
||||
if not has_header:
|
||||
f.seek(0, 0) # Go to the beginning of the file
|
||||
f.write(HEADER_TEXT.strip() + '\n\n' + original_content)
|
||||
print(f"Added header to: {file_path}")
|
||||
headers_added += 1
|
||||
else:
|
||||
print(f"Header already exists in: {file_path}")
|
||||
else:
|
||||
# Remove header if it exists but shouldn't be there
|
||||
if has_header:
|
||||
# Find the end of the header and remove it (including following newlines)
|
||||
header_with_newlines = HEADER_TEXT.strip() + '\n\n'
|
||||
if original_content.startswith(header_with_newlines):
|
||||
content_without_header = original_content[len(header_with_newlines):]
|
||||
else:
|
||||
# Handle case where there might be different newline patterns
|
||||
header_end = len(HEADER_TEXT.strip())
|
||||
# Skip any newlines after the header
|
||||
while header_end < len(original_content) and original_content[header_end] in '\n\r':
|
||||
header_end += 1
|
||||
content_without_header = original_content[header_end:]
|
||||
|
||||
f.seek(0)
|
||||
f.write(content_without_header)
|
||||
f.truncate()
|
||||
print(f"Removed header from: {file_path}")
|
||||
headers_added += 1 # Reusing counter for modifications
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing file {file_path}: {e}")
|
||||
|
||||
print("\n--- Scan Complete ---")
|
||||
print(f"Total .ts or .tsx files found: {files_processed}")
|
||||
print(f"Files modified (headers added/removed): {headers_added}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Get the target directory from the command line arguments.
|
||||
# If no directory is provided, it uses the current directory ('.').
|
||||
if len(sys.argv) > 1:
|
||||
target_directory = sys.argv[1]
|
||||
else:
|
||||
target_directory = '.' # Default to current directory
|
||||
|
||||
if not os.path.isdir(target_directory):
|
||||
print(f"Error: Directory '{target_directory}' not found.")
|
||||
sys.exit(1)
|
||||
|
||||
process_directory(os.path.abspath(target_directory))
|
||||
+151
-522
File diff suppressed because it is too large
Load Diff
+152
-523
File diff suppressed because it is too large
Load Diff
+138
-509
@@ -18,8 +18,6 @@
|
||||
"componentsMember": "Du bist Mitglied von {count, plural, =0 {keiner Organisation} one {einer Organisation} other {# Organisationen}}.",
|
||||
"componentsInvalidKey": "Ungültige oder abgelaufene Lizenzschlüssel erkannt. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.",
|
||||
"dismiss": "Verwerfen",
|
||||
"subscriptionViolationMessage": "Sie überschreiten Ihre Grenzen für Ihr aktuelles Paket. Korrigieren Sie das Problem, indem Sie Webseiten, Benutzer oder andere Ressourcen entfernen, um in Ihrem Paket zu bleiben.",
|
||||
"subscriptionViolationViewBilling": "Rechnung anzeigen",
|
||||
"componentsLicenseViolation": "Lizenzverstoß: Dieser Server benutzt {usedSites} Standorte, was das Lizenzlimit von {maxSites} Standorten überschreitet. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.",
|
||||
"componentsSupporterMessage": "Vielen Dank für die Unterstützung von Pangolin als {tier}!",
|
||||
"inviteErrorNotValid": "Es tut uns leid, aber es sieht so aus, als wäre die Einladung, auf die du zugreifen möchtest, entweder nicht angenommen worden oder nicht mehr gültig.",
|
||||
@@ -58,9 +56,9 @@
|
||||
"sitesBannerTitle": "Verbinde ein beliebiges Netzwerk",
|
||||
"sitesBannerDescription": "Ein Standort ist eine Verbindung zu einem Remote-Netzwerk, die es Pangolin ermöglicht, Zugriff auf öffentliche oder private Ressourcen für Benutzer überall zu gewähren. Installieren Sie den Site Netzwerk Connector (Newt) wo auch immer Sie eine Binärdatei oder einen Container starten können, um die Verbindung herzustellen.",
|
||||
"sitesBannerButtonText": "Standort installieren",
|
||||
"approvalsBannerTitle": "Gerätezugriff genehmigen oder verweigern",
|
||||
"approvalsBannerDescription": "Überprüfen und genehmigen oder verweigern Gerätezugriffsanfragen von Benutzern. Wenn Gerätegenehmigungen erforderlich sind, müssen Benutzer eine Administratorgenehmigung erhalten, bevor ihre Geräte sich mit den Ressourcen Ihrer Organisation verbinden können.",
|
||||
"approvalsBannerButtonText": "Mehr erfahren",
|
||||
"approvalsBannerTitle": "Approve or Deny Device Access",
|
||||
"approvalsBannerDescription": "Review and approve or deny device access requests from users. When device approvals are required, users must get admin approval before their devices can connect to your organization's resources.",
|
||||
"approvalsBannerButtonText": "Learn More",
|
||||
"siteCreate": "Standort erstellen",
|
||||
"siteCreateDescription2": "Folge den nachfolgenden Schritten, um einen neuen Standort zu erstellen und zu verbinden",
|
||||
"siteCreateDescription": "Erstellen Sie einen neuen Standort, um Ressourcen zu verbinden",
|
||||
@@ -99,7 +97,7 @@
|
||||
"siteGeneralDescription": "Allgemeine Einstellungen für diesen Standort konfigurieren",
|
||||
"siteSettingDescription": "Standorteinstellungen konfigurieren",
|
||||
"siteSetting": "{siteName} Einstellungen",
|
||||
"siteNewtTunnel": "Newt Standort (empfohlen)",
|
||||
"siteNewtTunnel": "Neuer Standort (empfohlen)",
|
||||
"siteNewtTunnelDescription": "Einfachster Weg, einen Einstiegspunkt in jedes Netzwerk zu erstellen. Keine zusätzliche Einrichtung.",
|
||||
"siteWg": "Einfacher WireGuard Tunnel",
|
||||
"siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.",
|
||||
@@ -109,7 +107,7 @@
|
||||
"siteSeeAll": "Alle Standorte anzeigen",
|
||||
"siteTunnelDescription": "Legen Sie fest, wie Sie sich mit dem Standort verbinden möchten",
|
||||
"siteNewtCredentials": "Zugangsdaten",
|
||||
"siteNewtCredentialsDescription": "So wird sich der Standort mit dem Server authentifizieren",
|
||||
"siteNewtCredentialsDescription": "So wird sich die Seite mit dem Server authentifizieren",
|
||||
"remoteNodeCredentialsDescription": "So wird sich der entfernte Node mit dem Server authentifizieren",
|
||||
"siteCredentialsSave": "Anmeldedaten speichern",
|
||||
"siteCredentialsSaveDescription": "Du kannst das nur einmal sehen. Stelle sicher, dass du es an einen sicheren Ort kopierst.",
|
||||
@@ -148,11 +146,6 @@
|
||||
"createLink": "Link erstellen",
|
||||
"resourcesNotFound": "Keine Ressourcen gefunden",
|
||||
"resourceSearch": "Suche Ressourcen",
|
||||
"machineSearch": "Maschinen suchen",
|
||||
"machinesSearch": "Suche Maschinen-Klienten...",
|
||||
"machineNotFound": "Keine Maschinen gefunden",
|
||||
"userDeviceSearch": "Benutzergeräte durchsuchen",
|
||||
"userDevicesSearch": "Benutzergeräte durchsuchen...",
|
||||
"openMenu": "Menü öffnen",
|
||||
"resource": "Ressource",
|
||||
"title": "Titel",
|
||||
@@ -180,7 +173,6 @@
|
||||
"resourceHTTPDescription": "Proxy-Anfragen über HTTPS mit einem voll qualifizierten Domain-Namen.",
|
||||
"resourceRaw": "Direkte TCP/UDP Ressource (raw)",
|
||||
"resourceRawDescription": "Proxy-Anfragen über rohes TCP/UDP mit einer Portnummer.",
|
||||
"resourceRawDescriptionCloud": "Proxy-Anfragen über rohe TCP/UDP mit Portnummer. Benötigt Sites, um sich mit einem entfernten Knoten zu verbinden.",
|
||||
"resourceCreate": "Ressource erstellen",
|
||||
"resourceCreateDescription": "Folgen Sie den Schritten unten, um eine neue Ressource zu erstellen",
|
||||
"resourceSeeAll": "Alle Ressourcen anzeigen",
|
||||
@@ -207,7 +199,6 @@
|
||||
"protocolSelect": "Wählen Sie ein Protokoll",
|
||||
"resourcePortNumber": "Portnummer",
|
||||
"resourcePortNumberDescription": "Die externe Portnummer für Proxy-Anfragen.",
|
||||
"back": "Zurück",
|
||||
"cancel": "Abbrechen",
|
||||
"resourceConfig": "Konfiguration Snippets",
|
||||
"resourceConfigDescription": "Kopieren und fügen Sie diese Konfigurations-Snippets ein, um die TCP/UDP Ressource einzurichten",
|
||||
@@ -253,17 +244,6 @@
|
||||
"orgErrorDeleteMessage": "Beim Löschen der Organisation ist ein Fehler aufgetreten.",
|
||||
"orgDeleted": "Organisation gelöscht",
|
||||
"orgDeletedMessage": "Die Organisation und ihre Daten wurden gelöscht.",
|
||||
"deleteAccount": "Konto löschen",
|
||||
"deleteAccountDescription": "Lösche dein Konto, alle Organisationen, die du besitzt, und alle Daten innerhalb dieser Organisationen. Dies kann nicht rückgängig gemacht werden.",
|
||||
"deleteAccountButton": "Konto löschen",
|
||||
"deleteAccountConfirmTitle": "Konto löschen",
|
||||
"deleteAccountConfirmMessage": "Dies wird Ihr Konto dauerhaft löschen, alle Organisationen, die Sie besitzen, und alle Daten innerhalb dieser Organisationen. Dies kann nicht rückgängig gemacht werden.",
|
||||
"deleteAccountConfirmString": "Konto löschen",
|
||||
"deleteAccountSuccess": "Konto gelöscht",
|
||||
"deleteAccountSuccessMessage": "Ihr Konto wurde gelöscht.",
|
||||
"deleteAccountError": "Konto konnte nicht gelöscht werden",
|
||||
"deleteAccountPreviewAccount": "Ihr Konto",
|
||||
"deleteAccountPreviewOrgs": "Organisationen, die Sie besitzen (und ihre Daten)",
|
||||
"orgMissing": "Organisations-ID fehlt",
|
||||
"orgMissingMessage": "Einladung kann ohne Organisations-ID nicht neu generiert werden.",
|
||||
"accessUsersManage": "Benutzer verwalten",
|
||||
@@ -280,8 +260,8 @@
|
||||
"accessRolesSearch": "Rollen suchen...",
|
||||
"accessRolesAdd": "Rolle hinzufügen",
|
||||
"accessRoleDelete": "Rolle löschen",
|
||||
"accessApprovalsManage": "Genehmigungen verwalten",
|
||||
"accessApprovalsDescription": "Zeige und verwalte ausstehende Genehmigungen für den Zugriff auf diese Organisation",
|
||||
"accessApprovalsManage": "Manage Approvals",
|
||||
"accessApprovalsDescription": "View and manage pending approvals for access to this organization",
|
||||
"description": "Beschreibung",
|
||||
"inviteTitle": "Einladungen öffnen",
|
||||
"inviteDescription": "Einladungen für andere Benutzer verwalten, der Organisation beizutreten",
|
||||
@@ -328,54 +308,6 @@
|
||||
"apiKeysDelete": "API-Schlüssel löschen",
|
||||
"apiKeysManage": "API-Schlüssel verwalten",
|
||||
"apiKeysDescription": "API-Schlüssel werden zur Authentifizierung mit der Integrations-API verwendet",
|
||||
"provisioningKeysTitle": "Bereitstellungsschlüssel",
|
||||
"provisioningKeysManage": "Bereitstellungsschlüssel verwalten",
|
||||
"provisioningKeysDescription": "Bereitstellungsschlüssel werden verwendet, um die automatisierte Bereitstellung von Seiten für Ihr Unternehmen zu authentifizieren.",
|
||||
"provisioningManage": "Bereitstellung",
|
||||
"provisioningDescription": "Bereitstellungsschlüssel verwalten und ausstehende Seiten prüfen, die noch auf Genehmigung warten.",
|
||||
"pendingSites": "Ausstehende Seiten",
|
||||
"siteApproveSuccess": "Site erfolgreich freigegeben",
|
||||
"siteApproveError": "Fehler beim Bestätigen der Seite",
|
||||
"provisioningKeys": "Bereitstellungsschlüssel",
|
||||
"searchProvisioningKeys": "Bereitstellungsschlüssel suchen...",
|
||||
"provisioningKeysAdd": "Bereitstellungsschlüssel generieren",
|
||||
"provisioningKeysErrorDelete": "Fehler beim Löschen des Bereitstellungsschlüssels",
|
||||
"provisioningKeysErrorDeleteMessage": "Fehler beim Löschen des Bereitstellungsschlüssels",
|
||||
"provisioningKeysQuestionRemove": "Sind Sie sicher, dass Sie diesen Bereitstellungsschlüssel aus der Organisation entfernen möchten?",
|
||||
"provisioningKeysMessageRemove": "Einmal entfernt, kann der Schlüssel nicht mehr für die Bereitstellung der Site verwendet werden.",
|
||||
"provisioningKeysDeleteConfirm": "Bereitstellungsschlüssel löschen bestätigen",
|
||||
"provisioningKeysDelete": "Bereitstellungsschlüssel löschen",
|
||||
"provisioningKeysCreate": "Bereitstellungsschlüssel generieren",
|
||||
"provisioningKeysCreateDescription": "Einen neuen Bereitstellungsschlüssel für die Organisation generieren",
|
||||
"provisioningKeysSeeAll": "Alle Bereitstellungsschlüssel anzeigen",
|
||||
"provisioningKeysSave": "Bereitstellungsschlüssel speichern",
|
||||
"provisioningKeysSaveDescription": "Sie können dies nur einmal sehen. Kopieren Sie es an einen sicheren Ort.",
|
||||
"provisioningKeysErrorCreate": "Fehler beim Erstellen des Bereitstellungsschlüssels",
|
||||
"provisioningKeysList": "Neuer Bereitstellungsschlüssel",
|
||||
"provisioningKeysMaxBatchSize": "Max. Batch-Größe",
|
||||
"provisioningKeysUnlimitedBatchSize": "Unbegrenzte Batch-Größe (kein Limit)",
|
||||
"provisioningKeysMaxBatchUnlimited": "Unbegrenzt",
|
||||
"provisioningKeysMaxBatchSizeInvalid": "Geben Sie eine gültige maximale Batchgröße ein (1–1.000.000).",
|
||||
"provisioningKeysValidUntil": "Gültig bis",
|
||||
"provisioningKeysValidUntilHint": "Leer lassen für keine Verjährung.",
|
||||
"provisioningKeysValidUntilInvalid": "Geben Sie ein gültiges Datum und Zeit ein.",
|
||||
"provisioningKeysNumUsed": "Verwendete Zeiten",
|
||||
"provisioningKeysLastUsed": "Zuletzt verwendet",
|
||||
"provisioningKeysNoExpiry": "Kein Ablauf",
|
||||
"provisioningKeysNeverUsed": "Nie",
|
||||
"provisioningKeysEdit": "Bereitstellungsschlüssel bearbeiten",
|
||||
"provisioningKeysEditDescription": "Aktualisieren Sie die maximale Batch-Größe und Ablaufzeit für diesen Schlüssel.",
|
||||
"provisioningKeysApproveNewSites": "Neue Seiten genehmigen",
|
||||
"provisioningKeysApproveNewSitesDescription": "Sites, die sich mit diesem Schlüssel registrieren, automatisch freigeben.",
|
||||
"provisioningKeysUpdateError": "Fehler beim Aktualisieren des Bereitstellungsschlüssels",
|
||||
"provisioningKeysUpdated": "Bereitstellungsschlüssel aktualisiert",
|
||||
"provisioningKeysUpdatedDescription": "Ihre Änderungen wurden gespeichert.",
|
||||
"provisioningKeysBannerTitle": "Website-Bereitstellungsschlüssel",
|
||||
"provisioningKeysBannerDescription": "Generieren Sie einen Bereitstellungsschlüssel und verwenden Sie ihn mit dem Newt-Connector, um Standorte beim ersten Start automatisch zu erstellen - keine Notwendigkeit, separate Anmeldedaten für jede Seite einzurichten.",
|
||||
"provisioningKeysBannerButtonText": "Mehr erfahren",
|
||||
"pendingSitesBannerTitle": "Ausstehende Seiten",
|
||||
"pendingSitesBannerDescription": "Websites, die mit einem Bereitstellungsschlüssel verbunden sind, erscheinen hier zur Überprüfung.",
|
||||
"pendingSitesBannerButtonText": "Mehr erfahren",
|
||||
"apiKeysSettings": "{apiKeyName} Einstellungen",
|
||||
"userTitle": "Alle Benutzer verwalten",
|
||||
"userDescription": "Alle Benutzer im System anzeigen und verwalten",
|
||||
@@ -523,20 +455,18 @@
|
||||
"selectDuration": "Dauer auswählen",
|
||||
"selectResource": "Ressource auswählen",
|
||||
"filterByResource": "Nach Ressource filtern",
|
||||
"selectApprovalState": "Genehmigungsstatus auswählen",
|
||||
"filterByApprovalState": "Filtern nach Genehmigungsstatus",
|
||||
"approvalListEmpty": "Keine Genehmigungen",
|
||||
"approvalState": "Genehmigungsstatus",
|
||||
"approvalLoadMore": "Mehr laden",
|
||||
"loadingApprovals": "Genehmigungen werden geladen",
|
||||
"approve": "Bestätigen",
|
||||
"approved": "Genehmigt",
|
||||
"denied": "Verweigert",
|
||||
"deniedApproval": "Genehmigung verweigert",
|
||||
"all": "Alle",
|
||||
"deny": "Leugnen",
|
||||
"viewDetails": "Details anzeigen",
|
||||
"requestingNewDeviceApproval": "hat ein neues Gerät angefordert",
|
||||
"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",
|
||||
@@ -562,12 +492,9 @@
|
||||
"userSaved": "Benutzer gespeichert",
|
||||
"userSavedDescription": "Der Benutzer wurde aktualisiert.",
|
||||
"autoProvisioned": "Automatisch bereitgestellt",
|
||||
"autoProvisionSettings": "Auto-Bereitstellungseinstellungen",
|
||||
"autoProvisionedDescription": "Erlaube diesem Benutzer die automatische Verwaltung durch Identitätsanbieter",
|
||||
"accessControlsDescription": "Verwalten Sie, worauf dieser Benutzer in der Organisation zugreifen und was er tun kann",
|
||||
"accessControlsSubmit": "Zugriffskontrollen speichern",
|
||||
"singleRolePerUserPlanNotice": "Ihr Plan unterstützt nur eine Rolle pro Benutzer.",
|
||||
"singleRolePerUserEditionNotice": "Diese Ausgabe unterstützt nur eine Rolle pro Benutzer.",
|
||||
"roles": "Rollen",
|
||||
"accessUsersRoles": "Benutzer & Rollen verwalten",
|
||||
"accessUsersRolesDescription": "Lade Benutzer ein und füge sie zu Rollen hinzu, um den Zugriff auf die Organisation zu verwalten",
|
||||
@@ -624,8 +551,6 @@
|
||||
"targetErrorInvalidPortDescription": "Bitte geben Sie eine gültige Portnummer ein",
|
||||
"targetErrorNoSite": "Kein Standort ausgewählt",
|
||||
"targetErrorNoSiteDescription": "Bitte wähle einen Standort für das Ziel aus",
|
||||
"targetTargetsCleared": "Ziele gelöscht",
|
||||
"targetTargetsClearedDescription": "Alle Ziele wurden aus dieser Ressource entfernt",
|
||||
"targetCreated": "Ziel erstellt",
|
||||
"targetCreatedDescription": "Ziel wurde erfolgreich erstellt",
|
||||
"targetErrorCreate": "Fehler beim Erstellen des Ziels",
|
||||
@@ -709,7 +634,6 @@
|
||||
"resourcesErrorUpdate": "Fehler beim Umschalten der Ressource",
|
||||
"resourcesErrorUpdateDescription": "Beim Aktualisieren der Ressource ist ein Fehler aufgetreten",
|
||||
"access": "Zugriff",
|
||||
"accessControl": "Zugriffskontrolle",
|
||||
"shareLink": "{resource} Freigabe-Link",
|
||||
"resourceSelect": "Ressource auswählen",
|
||||
"shareLinks": "Freigabe-Links",
|
||||
@@ -822,35 +746,34 @@
|
||||
"countries": "Länder",
|
||||
"accessRoleCreate": "Rolle erstellen",
|
||||
"accessRoleCreateDescription": "Erstellen Sie eine neue Rolle, um Benutzer zu gruppieren und ihre Berechtigungen zu verwalten.",
|
||||
"accessRoleEdit": "Rolle bearbeiten",
|
||||
"accessRoleEditDescription": "Rolleninformationen bearbeiten.",
|
||||
"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": "Rolle aktualisieren",
|
||||
"accessRoleUpdated": "Rolle aktualisiert",
|
||||
"accessRoleUpdatedDescription": "Die Rolle wurde erfolgreich aktualisiert.",
|
||||
"accessApprovalUpdated": "Genehmigung bearbeitet",
|
||||
"accessApprovalApprovedDescription": "Entscheidung für Genehmigungsanfrage setzen.",
|
||||
"accessApprovalDeniedDescription": "Entscheidung für Genehmigungsanfrage ablehnen.",
|
||||
"accessRoleErrorUpdate": "Fehler beim Aktualisieren der Rolle",
|
||||
"accessRoleErrorUpdateDescription": "Beim Aktualisieren der Rolle ist ein Fehler aufgetreten.",
|
||||
"accessApprovalErrorUpdate": "Genehmigung konnte nicht verarbeitet werden",
|
||||
"accessApprovalErrorUpdateDescription": "Bei der Bearbeitung der Genehmigung 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": "Du bist dabei die Rolle `{name}` zu löschen. Du kannst diese Aktion nicht rückgängig machen.",
|
||||
"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",
|
||||
"accessRoleRemoved": "Rolle entfernt",
|
||||
"accessRoleRemovedDescription": "Die Rolle wurde erfolgreich entfernt.",
|
||||
"accessRoleRequiredRemove": "Bevor Sie diese Rolle löschen, wählen Sie bitte eine neue Rolle aus, zu der die bestehenden Mitglieder übertragen werden sollen.",
|
||||
"network": "Netzwerk",
|
||||
"manage": "Verwalten",
|
||||
"sitesNotFound": "Keine Standorte gefunden.",
|
||||
"pangolinServerAdmin": "Server-Admin - Pangolin",
|
||||
@@ -866,9 +789,6 @@
|
||||
"sitestCountIncrease": "Anzahl der Standorte erhöhen",
|
||||
"idpManage": "Identitätsanbieter verwalten",
|
||||
"idpManageDescription": "Identitätsanbieter im System anzeigen und verwalten",
|
||||
"idpGlobalModeBanner": "Identitätsanbieter (IdPs) pro Organisation sind auf diesem Server deaktiviert. Es verwendet globale IdPs (geteilt über alle Organisationen). Verwalten Sie globale IdPs im <adminPanelLink>Admin-Panel</adminPanelLink>. Um IdPs pro Organisation zu aktivieren, bearbeiten Sie die Server-Konfiguration und setzen Sie den IdP-Modus auf org. <configDocsLink>Siehe Dokumentation</configDocsLink>. Wenn Sie weiterhin globale IdPs verwenden und diese in den Organisationseinstellungen verschwinden lassen wollen, setzen Sie den Modus explizit auf global in der Konfiguration.",
|
||||
"idpGlobalModeBannerUpgradeRequired": "Identitätsanbieter (IdPs) pro Organisation sind auf diesem Server deaktiviert. Es verwendet globale IdPs (geteilt in allen Organisationen). Globale IdPs im <adminPanelLink>Admin-Panel</adminPanelLink>verwalten. Um Identitätsanbieter pro Organisation nutzen zu können, müssen Sie zur Enterprise Edition upgraden.",
|
||||
"idpGlobalModeBannerLicenseRequired": "Identitätsanbieter (IdPs) pro Organisation sind auf diesem Server deaktiviert. Es verwendet globale IdPs (geteilt in allen Organisationen). Globale IdPs im <adminPanelLink>Admin-Panel</adminPanelLink>verwalten. Um Identitätsanbieter pro Organisation zu verwenden, ist eine Enterprise-Lizenz erforderlich.",
|
||||
"idpDeletedDescription": "Identitätsanbieter erfolgreich gelöscht",
|
||||
"idpOidc": "OAuth2/OIDC",
|
||||
"idpQuestionRemove": "Sind Sie sicher, dass Sie den Identitätsanbieter dauerhaft löschen möchten?",
|
||||
@@ -983,7 +903,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",
|
||||
@@ -1063,13 +983,13 @@
|
||||
"passwordExpiryDescription": "Diese Organisation erfordert, dass Sie Ihr Passwort alle {maxDays} Tage ändern.",
|
||||
"changePasswordNow": "Passwort jetzt ändern",
|
||||
"pincodeAuth": "Authentifizierungscode",
|
||||
"pincodeSubmit2": "Code einreichen",
|
||||
"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 Haupt-Login-Seite",
|
||||
"loginBack": "Go back to main login page",
|
||||
"signup": "Registrieren",
|
||||
"loginStart": "Melden Sie sich an, um zu beginnen",
|
||||
"idpOidcTokenValidating": "OIDC-Token wird validiert",
|
||||
@@ -1092,12 +1012,12 @@
|
||||
"pangolinSetup": "Einrichtung - Pangolin",
|
||||
"orgNameRequired": "Organisationsname ist erforderlich",
|
||||
"orgIdRequired": "Organisations-ID ist erforderlich",
|
||||
"orgIdMaxLength": "Organisations-ID darf höchstens 32 Zeichen lang sein",
|
||||
"orgErrorCreate": "Beim Erstellen der Organisation ist ein Fehler aufgetreten",
|
||||
"pageNotFound": "Seite nicht gefunden",
|
||||
"pageNotFoundDescription": "Hoppla! Die gesuchte Seite existiert nicht.",
|
||||
"overview": "Übersicht",
|
||||
"home": "Startseite",
|
||||
"accessControl": "Zugriffskontrolle",
|
||||
"settings": "Einstellungen",
|
||||
"usersAll": "Alle Benutzer",
|
||||
"license": "Lizenz",
|
||||
@@ -1160,12 +1080,6 @@
|
||||
"actionGetUser": "Benutzer abrufen",
|
||||
"actionGetOrgUser": "Organisationsbenutzer abrufen",
|
||||
"actionListOrgDomains": "Organisationsdomains auflisten",
|
||||
"actionGetDomain": "Domain abrufen",
|
||||
"actionCreateOrgDomain": "Domain erstellen",
|
||||
"actionUpdateOrgDomain": "Domain aktualisieren",
|
||||
"actionDeleteOrgDomain": "Domain löschen",
|
||||
"actionGetDNSRecords": "DNS-Einträge abrufen",
|
||||
"actionRestartOrgDomain": "Domain neu starten",
|
||||
"actionCreateSite": "Standort erstellen",
|
||||
"actionDeleteSite": "Standort löschen",
|
||||
"actionGetSite": "Standort abrufen",
|
||||
@@ -1177,7 +1091,6 @@
|
||||
"setupTokenDescription": "Geben Sie das Setup-Token von der Serverkonsole ein.",
|
||||
"setupTokenRequired": "Setup-Token ist erforderlich",
|
||||
"actionUpdateSite": "Standorte aktualisieren",
|
||||
"actionResetSiteBandwidth": "Organisations-Bandbreite zurücksetzen",
|
||||
"actionListSiteRoles": "Erlaubte Standort-Rollen auflisten",
|
||||
"actionCreateResource": "Ressource erstellen",
|
||||
"actionDeleteResource": "Ressource löschen",
|
||||
@@ -1207,7 +1120,6 @@
|
||||
"actionRemoveUser": "Benutzer entfernen",
|
||||
"actionListUsers": "Benutzer auflisten",
|
||||
"actionAddUserRole": "Benutzerrolle hinzufügen",
|
||||
"actionSetUserOrgRoles": "Benutzerrollen festlegen",
|
||||
"actionGenerateAccessToken": "Zugriffstoken generieren",
|
||||
"actionDeleteAccessToken": "Zugriffstoken löschen",
|
||||
"actionListAccessTokens": "Zugriffstoken auflisten",
|
||||
@@ -1235,10 +1147,10 @@
|
||||
"actionUpdateIdpOrg": "IDP-Organisation aktualisieren",
|
||||
"actionCreateClient": "Client erstellen",
|
||||
"actionDeleteClient": "Client löschen",
|
||||
"actionArchiveClient": "Client archivieren",
|
||||
"actionUnarchiveClient": "Client dearchivieren",
|
||||
"actionBlockClient": "Client sperren",
|
||||
"actionUnblockClient": "Client entsperren",
|
||||
"actionArchiveClient": "Archive Client",
|
||||
"actionUnarchiveClient": "Unarchive Client",
|
||||
"actionBlockClient": "Block Client",
|
||||
"actionUnblockClient": "Unblock Client",
|
||||
"actionUpdateClient": "Client aktualisieren",
|
||||
"actionListClients": "Clients auflisten",
|
||||
"actionGetClient": "Clients abrufen",
|
||||
@@ -1252,18 +1164,17 @@
|
||||
"actionViewLogs": "Logs anzeigen",
|
||||
"noneSelected": "Keine ausgewählt",
|
||||
"orgNotFound2": "Keine Organisationen gefunden.",
|
||||
"searchPlaceholder": "Suche...",
|
||||
"emptySearchOptions": "Keine Optionen gefunden",
|
||||
"searchProgress": "Suche...",
|
||||
"create": "Erstellen",
|
||||
"orgs": "Organisationen",
|
||||
"loginError": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
|
||||
"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 zum Passwort",
|
||||
"otpAuthBack": "Back to Password",
|
||||
"navbar": "Navigationsmenü",
|
||||
"navbarDescription": "Hauptnavigationsmenü für die Anwendung",
|
||||
"navbarDocsLink": "Dokumentation",
|
||||
@@ -1311,34 +1222,30 @@
|
||||
"sidebarOverview": "Übersicht",
|
||||
"sidebarHome": "Zuhause",
|
||||
"sidebarSites": "Standorte",
|
||||
"sidebarApprovals": "Genehmigungsanfragen",
|
||||
"sidebarApprovals": "Approval Requests",
|
||||
"sidebarResources": "Ressourcen",
|
||||
"sidebarProxyResources": "Öffentlich",
|
||||
"sidebarClientResources": "Privat",
|
||||
"sidebarAccessControl": "Zugriffskontrolle",
|
||||
"sidebarLogsAndAnalytics": "Protokolle & Analysen",
|
||||
"sidebarTeam": "Team",
|
||||
"sidebarUsers": "Benutzer",
|
||||
"sidebarAdmin": "Admin",
|
||||
"sidebarInvitations": "Einladungen",
|
||||
"sidebarRoles": "Rollen",
|
||||
"sidebarShareableLinks": "Links",
|
||||
"sidebarApiKeys": "API-Schlüssel",
|
||||
"sidebarProvisioning": "Bereitstellung",
|
||||
"sidebarSettings": "Einstellungen",
|
||||
"sidebarAllUsers": "Alle Benutzer",
|
||||
"sidebarIdentityProviders": "Identitätsanbieter",
|
||||
"sidebarLicense": "Lizenz",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarUserDevices": "Benutzer-Geräte",
|
||||
"sidebarUserDevices": "User Devices",
|
||||
"sidebarMachineClients": "Maschinen",
|
||||
"sidebarDomains": "Domänen",
|
||||
"sidebarGeneral": "Verwalten",
|
||||
"sidebarLogAndAnalytics": "Log & Analytik",
|
||||
"sidebarBluePrints": "Blaupausen",
|
||||
"sidebarOrganization": "Organisation",
|
||||
"sidebarManagement": "Management",
|
||||
"sidebarBillingAndLicenses": "Abrechnung & Lizenzen",
|
||||
"sidebarLogsAnalytics": "Analytik",
|
||||
"blueprints": "Blaupausen",
|
||||
"blueprintsDescription": "Deklarative Konfigurationen anwenden und vorherige Abläufe anzeigen",
|
||||
@@ -1360,6 +1267,7 @@
|
||||
"parsedContents": "Analysierte Inhalte (Nur lesen)",
|
||||
"enableDockerSocket": "Docker Blueprint aktivieren",
|
||||
"enableDockerSocketDescription": "Aktiviere Docker-Socket-Label-Scraping für Blueprintbeschriftungen. Der Socket-Pfad muss neu angegeben werden.",
|
||||
"enableDockerSocketLink": "Mehr erfahren",
|
||||
"viewDockerContainers": "Docker Container anzeigen",
|
||||
"containersIn": "Container in {siteName}",
|
||||
"selectContainerDescription": "Wählen Sie einen Container, der als Hostname für dieses Ziel verwendet werden soll. Klicken Sie auf einen Port, um einen Port zu verwenden.",
|
||||
@@ -1403,7 +1311,7 @@
|
||||
"setupErrorCreateAdmin": "Beim Erstellen des Server-Admin-Kontos ist ein Fehler aufgetreten.",
|
||||
"certificateStatus": "Zertifikatsstatus",
|
||||
"loading": "Laden",
|
||||
"loadingAnalytics": "Analytik wird geladen",
|
||||
"loadingAnalytics": "Loading Analytics",
|
||||
"restart": "Neustart",
|
||||
"domains": "Domänen",
|
||||
"domainsDescription": "Erstellen und verwalten der in der Organisation verfügbaren Domänen",
|
||||
@@ -1431,7 +1339,7 @@
|
||||
"refreshError": "Datenaktualisierung fehlgeschlagen",
|
||||
"verified": "Verifiziert",
|
||||
"pending": "Ausstehend",
|
||||
"pendingApproval": "Ausstehende Genehmigung",
|
||||
"pendingApproval": "Pending Approval",
|
||||
"sidebarBilling": "Abrechnung",
|
||||
"billing": "Abrechnung",
|
||||
"orgBillingDescription": "Zahlungsinformationen und Abonnements verwalten",
|
||||
@@ -1487,7 +1395,6 @@
|
||||
"domainPickerNamespace": "Namespace: {namespace}",
|
||||
"domainPickerShowMore": "Mehr anzeigen",
|
||||
"regionSelectorTitle": "Region auswählen",
|
||||
"domainPickerRemoteExitNodeWarning": "Angegebene Domains werden nicht unterstützt, wenn sich Websites mit externen Exit-Knoten verbinden. Damit Ressourcen auf entfernten Knoten verfügbar sind, verwenden Sie stattdessen eine eigene Domain.",
|
||||
"regionSelectorInfo": "Das Auswählen einer Region hilft uns, eine bessere Leistung für Ihren Standort bereitzustellen. Sie müssen sich nicht in derselben Region wie Ihr Server befinden.",
|
||||
"regionSelectorPlaceholder": "Wähle eine Region",
|
||||
"regionSelectorComingSoon": "Kommt bald",
|
||||
@@ -1497,11 +1404,10 @@
|
||||
"billingUsageLimitsOverview": "Übersicht über Nutzungsgrenzen",
|
||||
"billingMonitorUsage": "Überwachen Sie Ihren Verbrauch im Vergleich zu konfigurierten Grenzwerten. Wenn Sie eine Erhöhung der Limits benötigen, kontaktieren Sie uns bitte support@pangolin.net.",
|
||||
"billingDataUsage": "Datenverbrauch",
|
||||
"billingSites": "Seiten",
|
||||
"billingUsers": "Benutzergeräte",
|
||||
"billingDomains": "Domänen",
|
||||
"billingOrganizations": "Orden",
|
||||
"billingRemoteExitNodes": "Entfernte Knoten",
|
||||
"billingOnlineTime": "Online-Zeit der Seite",
|
||||
"billingUsers": "Aktive Benutzer",
|
||||
"billingDomains": "Aktive Domains",
|
||||
"billingRemoteExitNodes": "Aktive selbstgehostete Nodes",
|
||||
"billingNoLimitConfigured": "Kein Limit konfiguriert",
|
||||
"billingEstimatedPeriod": "Geschätzter Abrechnungszeitraum",
|
||||
"billingIncludedUsage": "Inklusive Nutzung",
|
||||
@@ -1526,24 +1432,15 @@
|
||||
"billingFailedToGetPortalUrl": "Fehler beim Abrufen der Portal-URL",
|
||||
"billingPortalError": "Portalfehler",
|
||||
"billingDataUsageInfo": "Wenn Sie mit der Cloud verbunden sind, werden alle Daten über Ihre sicheren Tunnel belastet. Dies schließt eingehenden und ausgehenden Datenverkehr über alle Ihre Websites ein. Wenn Sie Ihr Limit erreichen, werden Ihre Seiten die Verbindung trennen, bis Sie Ihr Paket upgraden oder die Nutzung verringern. Daten werden nicht belastet, wenn Sie Knoten verwenden.",
|
||||
"billingSInfo": "Anzahl der Sites die Sie verwenden können",
|
||||
"billingUsersInfo": "Wie viele Benutzer Sie verwenden können",
|
||||
"billingDomainInfo": "Wie viele Domains Sie verwenden können",
|
||||
"billingRemoteExitNodesInfo": "Wie viele entfernte Knoten Sie verwenden können",
|
||||
"billingLicenseKeys": "Lizenzschlüssel",
|
||||
"billingLicenseKeysDescription": "Verwalten Sie Ihre Lizenzschlüssel Abonnements",
|
||||
"billingLicenseSubscription": "Lizenzabonnement",
|
||||
"billingInactive": "Inaktiv",
|
||||
"billingLicenseItem": "Lizenz-Element",
|
||||
"billingQuantity": "Menge",
|
||||
"billingTotal": "gesamt",
|
||||
"billingModifyLicenses": "Lizenzabonnement ändern",
|
||||
"billingOnlineTimeInfo": "Sie werden belastet, abhängig davon, wie lange Ihre Seiten mit der Cloud verbunden bleiben. Zum Beispiel 44.640 Minuten entspricht einer Site, die 24 Stunden am Tag des Monats läuft. Wenn Sie Ihr Limit erreichen, werden Ihre Seiten die Verbindung trennen, bis Sie Ihr Paket upgraden oder die Nutzung verringern. Die Zeit wird nicht belastet, wenn Sie Knoten verwenden.",
|
||||
"billingUsersInfo": "Sie werden für jeden Benutzer in der Organisation berechnet. Die Abrechnung wird täglich anhand der Anzahl der aktiven Benutzerkonten in Ihrer Org berechnet.",
|
||||
"billingDomainInfo": "Sie werden für jede Domain in der Organisation berechnet. Die Abrechnung wird täglich anhand der Anzahl der aktiven Domain-Konten in Ihrer Org berechnet.",
|
||||
"billingRemoteExitNodesInfo": "Sie werden für jeden verwalteten Knoten in der Organisation berechnet. Die Abrechnung wird täglich anhand der Anzahl der aktiven verwalteten Knoten in Ihrer Org berechnet.",
|
||||
"domainNotFound": "Domain nicht gefunden",
|
||||
"domainNotFoundDescription": "Diese Ressource ist deaktiviert, weil die Domain nicht mehr in unserem System existiert. Bitte setzen Sie eine neue Domain für diese Ressource.",
|
||||
"failed": "Fehlgeschlagen",
|
||||
"createNewOrgDescription": "Eine neue Organisation erstellen",
|
||||
"organization": "Organisation",
|
||||
"primary": "Primär",
|
||||
"port": "Port",
|
||||
"securityKeyManage": "Sicherheitsschlüssel verwalten",
|
||||
"securityKeyDescription": "Sicherheitsschlüssel für passwortlose Authentifizierung hinzufügen oder entfernen",
|
||||
@@ -1559,7 +1456,7 @@
|
||||
"securityKeyRemoveSuccess": "Sicherheitsschlüssel erfolgreich entfernt",
|
||||
"securityKeyRemoveError": "Fehler beim Entfernen des Sicherheitsschlüssels",
|
||||
"securityKeyLoadError": "Fehler beim Laden der Sicherheitsschlüssel",
|
||||
"securityKeyLogin": "Sicherheitsschlüssel verwenden",
|
||||
"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...",
|
||||
@@ -1615,42 +1512,6 @@
|
||||
"resourcePortRequired": "Portnummer ist für nicht-HTTP-Ressourcen erforderlich",
|
||||
"resourcePortNotAllowed": "Portnummer sollte für HTTP-Ressourcen nicht gesetzt werden",
|
||||
"billingPricingCalculatorLink": "Preisrechner",
|
||||
"billingYourPlan": "Ihr Plan",
|
||||
"billingViewOrModifyPlan": "Zeige oder ändere dein aktuelles Paket",
|
||||
"billingViewPlanDetails": "Plan Details anzeigen",
|
||||
"billingUsageAndLimits": "Nutzung und Einschränkungen",
|
||||
"billingViewUsageAndLimits": "Schau dir die Grenzen und die aktuelle Nutzung deines Plans an",
|
||||
"billingCurrentUsage": "Aktuelle Nutzung",
|
||||
"billingMaximumLimits": "Maximale Grenzen",
|
||||
"billingRemoteNodes": "Entfernte Knoten",
|
||||
"billingUnlimited": "Unbegrenzt",
|
||||
"billingPaidLicenseKeys": "Bezahlte Lizenzschlüssel",
|
||||
"billingManageLicenseSubscription": "Verwalten Sie Ihr Abonnement für kostenpflichtige selbstgehostete Lizenzschlüssel",
|
||||
"billingCurrentKeys": "Aktuelle Tasten",
|
||||
"billingModifyCurrentPlan": "Aktuelles Paket ändern",
|
||||
"billingConfirmUpgrade": "Upgrade bestätigen",
|
||||
"billingConfirmDowngrade": "Downgrade bestätigen",
|
||||
"billingConfirmUpgradeDescription": "Sie sind dabei, Ihr Paket zu aktualisieren. Schauen Sie sich die neuen Limits und Preise unten an.",
|
||||
"billingConfirmDowngradeDescription": "Sie sind dabei, Ihren Plan herunterzustufen. Überprüfen Sie die neuen Limits und Preise unten.",
|
||||
"billingPlanIncludes": "Plan beinhaltet",
|
||||
"billingProcessing": "Verarbeitung...",
|
||||
"billingConfirmUpgradeButton": "Upgrade bestätigen",
|
||||
"billingConfirmDowngradeButton": "Downgrade bestätigen",
|
||||
"billingLimitViolationWarning": "Nutzung überschreitet neue Plan-Grenzen",
|
||||
"billingLimitViolationDescription": "Ihre aktuelle Nutzung überschreitet die Grenzen dieses Plans. Nach dem Downgrade werden alle Aktionen deaktiviert, bis Sie die Nutzung innerhalb der neuen Grenzen reduzieren. Bitte überprüfen Sie die Funktionen unten, die derzeit über den Grenzen liegen. Grenzwerte verletzen:",
|
||||
"billingFeatureLossWarning": "Verfügbarkeitshinweis",
|
||||
"billingFeatureLossDescription": "Durch Herabstufung werden Funktionen, die im neuen Paket nicht verfügbar sind, automatisch deaktiviert. Einige Einstellungen und Konfigurationen können verloren gehen. Bitte überprüfen Sie die Preismatrix um zu verstehen, welche Funktionen nicht mehr verfügbar sein werden.",
|
||||
"billingUsageExceedsLimit": "Aktuelle Nutzung ({current}) überschreitet das Limit ({limit})",
|
||||
"billingPastDueTitle": "Zahlung vergangene Fälligkeit",
|
||||
"billingPastDueDescription": "Ihre Zahlung ist abgelaufen. Bitte aktualisieren Sie Ihre Zahlungsmethode, um die aktuellen Funktionen Ihres Pakets weiter zu nutzen. Wenn nicht geklärt, wird Ihr Abonnement abgebrochen und Sie werden auf die kostenlose Stufe zurückgekehrt.",
|
||||
"billingUnpaidTitle": "Unbezahltes Abonnement",
|
||||
"billingUnpaidDescription": "Dein Abonnement ist unbezahlt und du wurdest auf die kostenlose Stufe zurückgekehrt. Bitte aktualisiere deine Zahlungsmethode, um dein Abonnement wiederherzustellen.",
|
||||
"billingIncompleteTitle": "Zahlung unvollständig",
|
||||
"billingIncompleteDescription": "Ihre Zahlung ist unvollständig. Bitte schließen Sie den Zahlungsvorgang ab, um Ihr Abonnement zu aktivieren.",
|
||||
"billingIncompleteExpiredTitle": "Zahlung abgelaufen",
|
||||
"billingIncompleteExpiredDescription": "Deine Zahlung wurde nie abgeschlossen und ist abgelaufen. Du wurdest zur kostenlosen Stufe zurückgekehrt. Bitte melde dich erneut an, um den Zugriff auf kostenpflichtige Funktionen wiederherzustellen.",
|
||||
"billingManageSubscription": "Verwalten Sie Ihr Abonnement",
|
||||
"billingResolvePaymentIssue": "Bitte beheben Sie Ihr Zahlungsproblem vor dem Upgrade oder Herabstufen",
|
||||
"signUpTerms": {
|
||||
"IAgreeToThe": "Ich stimme den",
|
||||
"termsOfService": "Nutzungsbedingungen zu",
|
||||
@@ -1722,26 +1583,8 @@
|
||||
"IntervalSeconds": "Gesunder Intervall",
|
||||
"timeoutSeconds": "Timeout (Sek.)",
|
||||
"timeIsInSeconds": "Zeit ist in Sekunden",
|
||||
"requireDeviceApproval": "Gerätegenehmigungen erforderlich",
|
||||
"requireDeviceApprovalDescription": "Benutzer mit dieser Rolle benötigen neue Geräte, die von einem Administrator genehmigt wurden, bevor sie sich verbinden und auf Ressourcen zugreifen können.",
|
||||
"sshAccess": "SSH-Zugriff",
|
||||
"roleAllowSsh": "SSH erlauben",
|
||||
"roleAllowSshAllow": "Erlauben",
|
||||
"roleAllowSshDisallow": "Nicht zulassen",
|
||||
"roleAllowSshDescription": "Benutzern mit dieser Rolle erlauben, sich über SSH mit Ressourcen zu verbinden. Wenn deaktiviert, kann die Rolle keinen SSH-Zugriff verwenden.",
|
||||
"sshSudoMode": "Sudo-Zugriff",
|
||||
"sshSudoModeNone": "Keine",
|
||||
"sshSudoModeNoneDescription": "Benutzer kann keine Befehle mit sudo ausführen.",
|
||||
"sshSudoModeFull": "Volles Sudo",
|
||||
"sshSudoModeFullDescription": "Benutzer kann jeden Befehl mit sudo ausführen.",
|
||||
"sshSudoModeCommands": "Befehle",
|
||||
"sshSudoModeCommandsDescription": "Benutzer kann nur die angegebenen Befehle mit sudo ausführen.",
|
||||
"sshSudo": "sudo erlauben",
|
||||
"sshSudoCommands": "Sudo-Befehle",
|
||||
"sshSudoCommandsDescription": "Kommagetrennte Liste von Befehlen, die der Benutzer mit sudo ausführen darf.",
|
||||
"sshCreateHomeDir": "Home-Verzeichnis erstellen",
|
||||
"sshUnixGroups": "Unix-Gruppen",
|
||||
"sshUnixGroupsDescription": "Durch Komma getrennte Unix-Gruppen, um den Benutzer auf dem Zielhost hinzuzufügen.",
|
||||
"requireDeviceApproval": "Require Device Approvals",
|
||||
"requireDeviceApprovalDescription": "Users with this role need new devices approved by an admin before they can connect and access resources.",
|
||||
"retryAttempts": "Wiederholungsversuche",
|
||||
"expectedResponseCodes": "Erwartete Antwortcodes",
|
||||
"expectedResponseCodesDescription": "HTTP-Statuscode, der einen gesunden Zustand anzeigt. Wenn leer gelassen, wird 200-300 als gesund angesehen.",
|
||||
@@ -1782,8 +1625,6 @@
|
||||
"resourcesTableNoInternalResourcesFound": "Keine internen Ressourcen gefunden.",
|
||||
"resourcesTableDestination": "Ziel",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAliasAddress": "Alias-Adresse",
|
||||
"resourcesTableAliasAddressInfo": "Diese Adresse ist Teil des Utility-Subnetzes der Organisation. Sie wird verwendet, um Alias-Einträge mit interner DNS-Auflösung aufzulösen.",
|
||||
"resourcesTableClients": "Clients",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "und sind nur intern zugänglich, wenn mit einem Client verbunden.",
|
||||
"resourcesTableNoTargets": "Keine Ziele",
|
||||
@@ -1950,40 +1791,6 @@
|
||||
"exitNode": "Exit-Node",
|
||||
"country": "Land",
|
||||
"rulesMatchCountry": "Derzeit basierend auf der Quell-IP",
|
||||
"region": "Region",
|
||||
"selectRegion": "Region wählen...",
|
||||
"searchRegions": "Regionen suchen...",
|
||||
"noRegionFound": "Keine Region gefunden.",
|
||||
"rulesMatchRegion": "Wählen Sie eine Regionalgruppe von Ländern",
|
||||
"rulesErrorInvalidRegion": "Ungültige Region",
|
||||
"rulesErrorInvalidRegionDescription": "Bitte wählen Sie eine gültige Region aus.",
|
||||
"regionAfrica": "Afrika",
|
||||
"regionNorthernAfrica": "Nordafrika",
|
||||
"regionEasternAfrica": "Ostafrika",
|
||||
"regionMiddleAfrica": "Zentralafrika",
|
||||
"regionSouthernAfrica": "Südliches Afrika",
|
||||
"regionWesternAfrica": "Westafrika",
|
||||
"regionAmericas": "Amerika",
|
||||
"regionCaribbean": "Karibik",
|
||||
"regionCentralAmerica": "Mittelamerika",
|
||||
"regionSouthAmerica": "Südamerika",
|
||||
"regionNorthernAmerica": "Nordamerika",
|
||||
"regionAsia": "Asien",
|
||||
"regionCentralAsia": "Zentralasien",
|
||||
"regionEasternAsia": "Ostasien",
|
||||
"regionSouthEasternAsia": "Südostasien",
|
||||
"regionSouthernAsia": "Südasien",
|
||||
"regionWesternAsia": "Westasien",
|
||||
"regionEurope": "Europa",
|
||||
"regionEasternEurope": "Osteuropa",
|
||||
"regionNorthernEurope": "Nordeuropa",
|
||||
"regionSouthernEurope": "Südeuropa",
|
||||
"regionWesternEurope": "Westeuropa",
|
||||
"regionOceania": "Ozeanien",
|
||||
"regionAustraliaAndNewZealand": "Australien und Neuseeland",
|
||||
"regionMelanesia": "Melanesien",
|
||||
"regionMicronesia": "Mikronesien",
|
||||
"regionPolynesia": "Polynesien",
|
||||
"managedSelfHosted": {
|
||||
"title": "Verwaltetes Selbsthosted",
|
||||
"description": "Zuverlässiger und wartungsarmer Pangolin Server mit zusätzlichen Glocken und Pfeifen",
|
||||
@@ -2032,25 +1839,6 @@
|
||||
"invalidValue": "Ungültiger Wert",
|
||||
"idpTypeLabel": "Identitätsanbietertyp",
|
||||
"roleMappingExpressionPlaceholder": "z. B. enthalten(Gruppen, 'admin') && 'Admin' || 'Mitglied'",
|
||||
"roleMappingModeFixedRoles": "Feste Rollen",
|
||||
"roleMappingModeMappingBuilder": "Mapping Builder",
|
||||
"roleMappingModeRawExpression": "Roher Ausdruck",
|
||||
"roleMappingFixedRolesPlaceholderSelect": "Wählen Sie eine oder mehrere Rollen",
|
||||
"roleMappingFixedRolesPlaceholderFreeform": "Rollennamen eingeben (exakte Übereinstimmung pro Organisation)",
|
||||
"roleMappingFixedRolesDescriptionSameForAll": "Weisen Sie jedem auto-provisionierten Benutzer die gleiche Rolle zu.",
|
||||
"roleMappingFixedRolesDescriptionDefaultPolicy": "Für Standardrichtlinien geben Sie Rollennamen ein, die in jeder Organisation existieren, in der Benutzer angegeben sind. Namen müssen exakt übereinstimmen.",
|
||||
"roleMappingClaimPath": "Pfad einfordern",
|
||||
"roleMappingClaimPathPlaceholder": "gruppen",
|
||||
"roleMappingClaimPathDescription": "Pfad in der Token Payload mit Quellwerten (zum Beispiel Gruppen).",
|
||||
"roleMappingMatchValue": "Match-Wert",
|
||||
"roleMappingAssignRoles": "Rollen zuweisen",
|
||||
"roleMappingAddMappingRule": "Zuordnungsregel hinzufügen",
|
||||
"roleMappingRawExpressionResultDescription": "Ausdruck muss zu einem String oder String Array ausgewertet werden.",
|
||||
"roleMappingRawExpressionResultDescriptionSingleRole": "Ausdruck muss zu einem String (einem einzigen Rollennamen) ausgewertet werden.",
|
||||
"roleMappingMatchValuePlaceholder": "Match-Wert (z. B.: Admin)",
|
||||
"roleMappingAssignRolesPlaceholderFreeform": "Rollennamen eingeben (exakt pro Ort)",
|
||||
"roleMappingBuilderFreeformRowHint": "Rollennamen müssen mit einer Rolle in jeder Zielorganisation übereinstimmen.",
|
||||
"roleMappingRemoveRule": "Entfernen",
|
||||
"idpGoogleConfiguration": "Google-Konfiguration",
|
||||
"idpGoogleConfigurationDescription": "Google OAuth2 Zugangsdaten konfigurieren",
|
||||
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
|
||||
@@ -2087,9 +1875,6 @@
|
||||
"authPageBrandingQuestionRemove": "Sind Sie sicher, dass Sie das Branding für Authentifizierungsseiten entfernen möchten?",
|
||||
"authPageBrandingDeleteConfirm": "Branding löschen bestätigen",
|
||||
"brandingLogoURL": "Logo URL",
|
||||
"brandingLogoURLOrPath": "Logo-URL oder Pfad",
|
||||
"brandingLogoPathDescription": "Geben Sie eine URL oder einen lokalen Pfad ein.",
|
||||
"brandingLogoURLDescription": "Geben Sie eine öffentlich zugängliche URL zu Ihrem Logobild ein.",
|
||||
"brandingPrimaryColor": "Primär-Farbe",
|
||||
"brandingLogoWidth": "Breite (px)",
|
||||
"brandingLogoHeight": "Höhe (px)",
|
||||
@@ -2129,7 +1914,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",
|
||||
@@ -2139,13 +1924,6 @@
|
||||
"orgAuthBackToSignIn": "Zurück zum Standard Login",
|
||||
"orgAuthNoAccount": "Sie haben noch kein Konto?",
|
||||
"subscriptionRequiredToUse": "Um diese Funktion nutzen zu können, ist ein Abonnement erforderlich.",
|
||||
"mustUpgradeToUse": "Sie müssen Ihr Abonnement aktualisieren, um diese Funktion nutzen zu können.",
|
||||
"subscriptionRequiredTierToUse": "Diese Funktion erfordert <tierLink>{tier}</tierLink> oder höher.",
|
||||
"upgradeToTierToUse": "Upgrade auf <tierLink>{tier}</tierLink> oder höher, um diese Funktion zu nutzen.",
|
||||
"subscriptionTierTier1": "Zuhause",
|
||||
"subscriptionTierTier2": "Team",
|
||||
"subscriptionTierTier3": "Geschäftlich",
|
||||
"subscriptionTierEnterprise": "Firma",
|
||||
"idpDisabled": "Identitätsanbieter sind deaktiviert.",
|
||||
"orgAuthPageDisabled": "Organisations-Authentifizierungsseite ist deaktiviert.",
|
||||
"domainRestartedDescription": "Domain-Verifizierung erfolgreich neu gestartet",
|
||||
@@ -2333,32 +2111,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"newPricingLicenseForm": {
|
||||
"title": "Lizenz erhalten",
|
||||
"description": "Wählen Sie einen Plan und teilen Sie uns mit, wie Sie Pangolin verwenden möchten.",
|
||||
"chooseTier": "Wählen Sie Ihren Plan",
|
||||
"viewPricingLink": "Siehe Preise, Funktionen und Limits",
|
||||
"tiers": {
|
||||
"starter": {
|
||||
"title": "Starter",
|
||||
"description": "Enterprise Features, 25 Benutzer, 25 Sites und Community-Unterstützung."
|
||||
},
|
||||
"scale": {
|
||||
"title": "Maßstab",
|
||||
"description": "Enterprise Features, 50 Benutzer, 50 Sites und Prioritätsunterstützung."
|
||||
}
|
||||
},
|
||||
"personalUseOnly": "Nur persönliche Nutzung (kostenlose Lizenz - kein Checkout)",
|
||||
"buttons": {
|
||||
"continueToCheckout": "Weiter zur Kasse"
|
||||
},
|
||||
"toasts": {
|
||||
"checkoutError": {
|
||||
"title": "Checkout-Fehler",
|
||||
"description": "Kasse konnte nicht gestartet werden. Bitte versuchen Sie es erneut."
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": "Priorität",
|
||||
"priorityDescription": "Die Routen mit höherer Priorität werden zuerst ausgewertet. Priorität = 100 bedeutet automatische Bestellung (Systementscheidung). Verwenden Sie eine andere Nummer, um manuelle Priorität zu erzwingen.",
|
||||
"instanceName": "Instanzname",
|
||||
@@ -2447,8 +2199,6 @@
|
||||
"logRetentionAccessDescription": "Wie lange Zugriffsprotokolle beibehalten werden sollen",
|
||||
"logRetentionActionLabel": "Aktionsprotokoll-Speicherung",
|
||||
"logRetentionActionDescription": "Dauer des Action-Logs",
|
||||
"logRetentionConnectionLabel": "Verbindungsprotokoll-Speicherung",
|
||||
"logRetentionConnectionDescription": "Wie lange Verbindungsprotokolle gespeichert werden sollen",
|
||||
"logRetentionDisabled": "Deaktiviert",
|
||||
"logRetention3Days": "3 Tage",
|
||||
"logRetention7Days": "7 Tage",
|
||||
@@ -2459,15 +2209,7 @@
|
||||
"logRetentionEndOfFollowingYear": "Ende des folgenden Jahres",
|
||||
"actionLogsDescription": "Verlauf der in dieser Organisation durchgeführten Aktionen anzeigen",
|
||||
"accessLogsDescription": "Zugriffsauth-Anfragen für Ressourcen in dieser Organisation anzeigen",
|
||||
"connectionLogs": "Verbindungsprotokolle",
|
||||
"connectionLogsDescription": "Verbindungsprotokolle für Tunnel in dieser Organisation anzeigen",
|
||||
"sidebarLogsConnection": "Verbindungsprotokolle",
|
||||
"sidebarLogsStreaming": "Streaming",
|
||||
"sourceAddress": "Quelladresse",
|
||||
"destinationAddress": "Zieladresse",
|
||||
"duration": "Dauer",
|
||||
"licenseRequiredToUse": "Eine <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> Lizenz oder <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> wird benötigt, um diese Funktion nutzen zu können. <bookADemoLink>Buchen Sie eine Demo oder POC Testversion</bookADemoLink>.",
|
||||
"ossEnterpriseEditionRequired": "Die <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> wird benötigt, um diese Funktion nutzen zu können. Diese Funktion ist auch in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>verfügbar. <bookADemoLink>Buchen Sie eine Demo oder POC Testversion</bookADemoLink>.",
|
||||
"licenseRequiredToUse": "Um diese Funktion nutzen zu können, ist eine Enterprise-Lizenz erforderlich.",
|
||||
"certResolver": "Zertifikatsauflöser",
|
||||
"certResolverDescription": "Wählen Sie den Zertifikatslöser aus, der für diese Ressource verwendet werden soll.",
|
||||
"selectCertResolver": "Zertifikatsauflöser auswählen",
|
||||
@@ -2528,8 +2270,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": "Überprüfe Gerätecode...",
|
||||
"deviceCodeVerifying": "Geräteautorisierung wird überprüft...",
|
||||
"deviceCodeValidating": "Validating device code...",
|
||||
"deviceCodeVerifying": "Verifying device authorization...",
|
||||
"signedInAs": "Angemeldet als",
|
||||
"deviceCodeEnterPrompt": "Geben Sie den auf dem Gerät angezeigten Code ein",
|
||||
"continue": "Weiter",
|
||||
@@ -2604,14 +2346,11 @@
|
||||
"identifier": "Identifier",
|
||||
"deviceLoginUseDifferentAccount": "Nicht du? Verwenden Sie ein anderes Konto.",
|
||||
"deviceLoginDeviceRequestingAccessToAccount": "Ein Gerät fordert Zugriff auf dieses Konto an.",
|
||||
"loginSelectAuthenticationMethod": "Wählen Sie eine Authentifizierungsmethode aus, um fortzufahren.",
|
||||
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
|
||||
"noData": "Keine Daten",
|
||||
"machineClients": "Maschinen-Clients",
|
||||
"install": "Installieren",
|
||||
"run": "Ausführen",
|
||||
"envFile": "Umgebungsdatei",
|
||||
"serviceFile": "Servicedatei",
|
||||
"enableAndStart": "Aktivieren und Starten",
|
||||
"clientNameDescription": "Der Anzeigename des Clients, der später geändert werden kann.",
|
||||
"clientAddress": "Clientadresse (Erweitert)",
|
||||
"setupFailedToFetchSubnet": "Fehler beim Abrufen des Standard-Subnetzes",
|
||||
@@ -2667,17 +2406,6 @@
|
||||
"editInternalResourceDialogAccessControl": "Zugriffskontrolle",
|
||||
"editInternalResourceDialogAccessControlDescription": "Kontrollieren Sie, welche Rollen, Benutzer und Maschinen-Clients Zugriff auf diese Ressource haben, wenn sie verbunden sind. Admins haben immer Zugriff.",
|
||||
"editInternalResourceDialogPortRangeValidationError": "Der Port-Bereich muss \"*\" für alle Ports sein, oder eine kommaseparierte Liste von Ports und Bereichen (z.B. \"80,443.8000-9000\"). Ports müssen zwischen 1 und 65535 liegen.",
|
||||
"internalResourceAuthDaemonStrategy": "SSH Auth-Daemon Standort",
|
||||
"internalResourceAuthDaemonStrategyDescription": "Wählen Sie aus, wo der SSH-Authentifizierungs-Daemon läuft: auf der Site (Newt) oder auf einem entfernten Host.",
|
||||
"internalResourceAuthDaemonDescription": "Der SSH-Authentifizierungs-Daemon verarbeitet SSH-Schlüsselsignaturen und PAM-Authentifizierung für diese Ressource. Wählen Sie, ob sie auf der Website (Newt) oder auf einem separaten entfernten Host ausgeführt wird. Siehe <docsLink>die Dokumentation</docsLink> für mehr.",
|
||||
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||
"internalResourceAuthDaemonStrategyPlaceholder": "Strategie auswählen",
|
||||
"internalResourceAuthDaemonStrategyLabel": "Standort",
|
||||
"internalResourceAuthDaemonSite": "Vor Ort",
|
||||
"internalResourceAuthDaemonSiteDescription": "Der Auth Daemon läuft auf der Seite (Newt).",
|
||||
"internalResourceAuthDaemonRemote": "Entfernter Host",
|
||||
"internalResourceAuthDaemonRemoteDescription": "Der Auth Daemon läuft auf einem Host, der nicht die Site ist.",
|
||||
"internalResourceAuthDaemonPort": "Daemon-Port (optional)",
|
||||
"orgAuthWhatsThis": "Wo finde ich meine Organisations-ID?",
|
||||
"learnMore": "Mehr erfahren",
|
||||
"backToHome": "Zurück zur Startseite",
|
||||
@@ -2720,178 +2448,79 @@
|
||||
"deviceArchiveConfirm": "Gerät archivieren",
|
||||
"archiveDevice": "Gerät archivieren",
|
||||
"archive": "Archiv",
|
||||
"deviceUnarchived": "Gerät nicht archiviert",
|
||||
"deviceUnarchivedDescription": "Das Gerät wurde erfolgreich deinstalliert.",
|
||||
"errorUnarchivingDevice": "Fehler beim Entarchivieren des Geräts",
|
||||
"failedToUnarchiveDevice": "Fehler beim Entfernen des Geräts",
|
||||
"unarchive": "Archivieren",
|
||||
"archiveClient": "Client archivieren",
|
||||
"archiveClientQuestion": "Sind Sie sicher, dass Sie diesen Client archivieren möchten?",
|
||||
"archiveClientMessage": "Der Client wird archiviert und aus der Liste Ihrer aktiven Clients entfernt.",
|
||||
"archiveClientConfirm": "Client archivieren",
|
||||
"blockClient": "Client sperren",
|
||||
"blockClientQuestion": "Sind Sie sicher, dass Sie diesen Client blockieren möchten?",
|
||||
"blockClientMessage": "Das Gerät wird gezwungen, die Verbindung zu trennen, wenn es gerade verbunden ist. Sie können das Gerät später entsperren.",
|
||||
"blockClientConfirm": "Client sperren",
|
||||
"active": "Aktiv",
|
||||
"usernameOrEmail": "Benutzername oder E-Mail",
|
||||
"selectYourOrganization": "Wählen Sie Ihre Organisation",
|
||||
"signInTo": "Einloggen in",
|
||||
"signInWithPassword": "Mit Passwort fortfahren",
|
||||
"noAuthMethodsAvailable": "Keine Authentifizierungsmethoden für diese Organisation verfügbar.",
|
||||
"enterPassword": "Geben Sie Ihr Passwort ein",
|
||||
"enterMfaCode": "Geben Sie den Code aus Ihrer Authentifizierungs-App ein",
|
||||
"securityKeyRequired": "Bitte verwenden Sie Ihren Sicherheitsschlüssel zum Anmelden.",
|
||||
"needToUseAnotherAccount": "Benötigen Sie ein anderes Konto?",
|
||||
"loginLegalDisclaimer": "Indem Sie auf die Buttons unten klicken, bestätigen Sie, dass Sie gelesen haben, verstehen, und stimmen den <termsOfService>Nutzungsbedingungen</termsOfService> und <privacyPolicy>Datenschutzrichtlinien</privacyPolicy> zu.",
|
||||
"termsOfService": "Nutzungsbedingungen",
|
||||
"privacyPolicy": "Datenschutzerklärung",
|
||||
"userNotFoundWithUsername": "Kein Benutzer mit diesem Benutzernamen gefunden.",
|
||||
"verify": "Überprüfen",
|
||||
"signIn": "Anmelden",
|
||||
"forgotPassword": "Passwort vergessen?",
|
||||
"orgSignInTip": "Wenn Sie sich vorher angemeldet haben, können Sie Ihren Benutzernamen oder Ihre E-Mail-Adresse eingeben, um sich stattdessen beim Identifikationsprovider Ihrer Organisation zu authentifizieren. Es ist einfacher!",
|
||||
"continueAnyway": "Trotzdem fortfahren",
|
||||
"dontShowAgain": "Nicht mehr anzeigen",
|
||||
"orgSignInNotice": "Wussten Sie schon?",
|
||||
"signupOrgNotice": "Versucht sich anzumelden?",
|
||||
"signupOrgTip": "Versuchen Sie, sich über den Identitätsanbieter Ihrer Organisation anzumelden?",
|
||||
"signupOrgLink": "Melden Sie sich an oder melden Sie sich stattdessen bei Ihrer Organisation an",
|
||||
"verifyEmailLogInWithDifferentAccount": "Anderes Konto verwenden",
|
||||
"logIn": "Anmelden",
|
||||
"deviceInformation": "Geräteinformationen",
|
||||
"deviceInformationDescription": "Informationen über das Gerät und den Agent",
|
||||
"deviceSecurity": "Gerätesicherheit",
|
||||
"deviceSecurityDescription": "Informationen zur Gerätesicherheit",
|
||||
"platform": "Plattform",
|
||||
"macosVersion": "macOS-Version",
|
||||
"windowsVersion": "Windows-Version",
|
||||
"iosVersion": "iOS-Version",
|
||||
"androidVersion": "Android-Version",
|
||||
"osVersion": "OS-Version",
|
||||
"kernelVersion": "Kernel-Version",
|
||||
"deviceModel": "Gerätemodell",
|
||||
"serialNumber": "Seriennummer",
|
||||
"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",
|
||||
"deviceInformation": "Device Information",
|
||||
"deviceInformationDescription": "Information about the device and agent",
|
||||
"platform": "Platform",
|
||||
"macosVersion": "macOS Version",
|
||||
"windowsVersion": "Windows Version",
|
||||
"iosVersion": "iOS Version",
|
||||
"androidVersion": "Android Version",
|
||||
"osVersion": "OS Version",
|
||||
"kernelVersion": "Kernel Version",
|
||||
"deviceModel": "Device Model",
|
||||
"serialNumber": "Serial Number",
|
||||
"hostname": "Hostname",
|
||||
"firstSeen": "Zuerst gesehen",
|
||||
"lastSeen": "Zuletzt gesehen",
|
||||
"biometricsEnabled": "Biometrie aktiviert",
|
||||
"diskEncrypted": "Festplatte verschlüsselt",
|
||||
"firewallEnabled": "Firewall aktiviert",
|
||||
"autoUpdatesEnabled": "Automatische Updates aktiviert",
|
||||
"tpmAvailable": "TPM verfügbar",
|
||||
"windowsAntivirusEnabled": "Antivirus aktiviert",
|
||||
"macosSipEnabled": "Schutz der Systemintegrität (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Firewall Stealth-Modus",
|
||||
"linuxAppArmorEnabled": "AppRüstung",
|
||||
"linuxSELinuxEnabled": "SELinux",
|
||||
"deviceSettingsDescription": "Geräteinformationen und -einstellungen anzeigen",
|
||||
"devicePendingApprovalDescription": "Dieses Gerät wartet auf Freigabe",
|
||||
"deviceBlockedDescription": "Dieses Gerät ist derzeit gesperrt. Es kann keine Verbindung zu anderen Ressourcen herstellen, es sei denn, es entsperrt.",
|
||||
"unblockClient": "Client entsperren",
|
||||
"unblockClientDescription": "Das Gerät wurde entsperrt",
|
||||
"unarchiveClient": "Client dearchivieren",
|
||||
"unarchiveClientDescription": "Das Gerät wurde nicht archiviert",
|
||||
"block": "Blockieren",
|
||||
"unblock": "Entsperren",
|
||||
"deviceActions": "Geräte-Aktionen",
|
||||
"deviceActionsDescription": "Gerätestatus und Zugriff verwalten",
|
||||
"devicePendingApprovalBannerDescription": "Dieses Gerät wartet auf Genehmigung. Es kann sich erst mit Ressourcen verbinden.",
|
||||
"connected": "Verbunden",
|
||||
"disconnected": "Verbindung getrennt",
|
||||
"approvalsEmptyStateTitle": "Gerätezulassungen nicht aktiviert",
|
||||
"approvalsEmptyStateDescription": "Aktiviere Gerätegenehmigungen für Rollen, um Administratorgenehmigungen zu benötigen, bevor Benutzer neue Geräte verbinden können.",
|
||||
"approvalsEmptyStateStep1Title": "Gehe zu Rollen",
|
||||
"approvalsEmptyStateStep1Description": "Navigieren Sie zu den Rolleneinstellungen Ihrer Organisation, um die Gerätefreigaben zu konfigurieren.",
|
||||
"approvalsEmptyStateStep2Title": "Gerätegenehmigungen aktivieren",
|
||||
"approvalsEmptyStateStep2Description": "Bearbeite eine Rolle und aktiviere die Option 'Gerätegenehmigung erforderlich'. Benutzer mit dieser Rolle benötigen Administrator-Genehmigung für neue Geräte.",
|
||||
"approvalsEmptyStatePreviewDescription": "Vorschau: Wenn aktiviert, werden ausstehende Geräteanfragen hier zur Überprüfung angezeigt",
|
||||
"approvalsEmptyStateButtonText": "Rollen verwalten",
|
||||
"domainErrorTitle": "Wir haben Probleme mit der Überprüfung deiner Domain",
|
||||
"idpAdminAutoProvisionPoliciesTabHint": "Konfigurieren Sie Rollenzuordnungs- und Organisationsrichtlinien auf der Registerkarte <policiesTabLink>Auto-Bereitstellungseinstellungen</policiesTabLink>.",
|
||||
"streamingTitle": "Event Streaming",
|
||||
"streamingDescription": "Streamen Sie Events aus Ihrem Unternehmen in Echtzeit zu externen Zielen.",
|
||||
"streamingUnnamedDestination": "Unbenanntes Ziel",
|
||||
"streamingNoUrlConfigured": "Keine URL konfiguriert",
|
||||
"streamingAddDestination": "Ziel hinzufügen",
|
||||
"streamingHttpWebhookTitle": "HTTP Webhook",
|
||||
"streamingHttpWebhookDescription": "Sende Ereignisse an jeden HTTP-Endpunkt mit flexibler Authentifizierung und Vorlage.",
|
||||
"streamingS3Title": "Amazon S3",
|
||||
"streamingS3Description": "Streame Ereignisse in eine S3-kompatible Objekt-Speicher-Eimer. Kommt bald.",
|
||||
"streamingDatadogTitle": "Datadog",
|
||||
"streamingDatadogDescription": "Events direkt an Ihr Datadog Konto weiterleiten. Kommen Sie bald.",
|
||||
"streamingTypePickerDescription": "Wählen Sie einen Zieltyp aus, um loszulegen.",
|
||||
"streamingFailedToLoad": "Fehler beim Laden der Ziele",
|
||||
"streamingUnexpectedError": "Ein unerwarteter Fehler ist aufgetreten.",
|
||||
"streamingFailedToUpdate": "Fehler beim Aktualisieren des Ziels",
|
||||
"streamingDeletedSuccess": "Ziel erfolgreich gelöscht",
|
||||
"streamingFailedToDelete": "Fehler beim Löschen des Ziels",
|
||||
"streamingDeleteTitle": "Ziel löschen",
|
||||
"streamingDeleteButtonText": "Ziel löschen",
|
||||
"streamingDeleteDialogAreYouSure": "Sind Sie sicher, dass Sie löschen möchten",
|
||||
"streamingDeleteDialogThisDestination": "dieses Ziel",
|
||||
"streamingDeleteDialogPermanentlyRemoved": "? Alle Konfiguration wird dauerhaft entfernt.",
|
||||
"httpDestEditTitle": "Ziel bearbeiten",
|
||||
"httpDestAddTitle": "HTTP-Ziel hinzufügen",
|
||||
"httpDestEditDescription": "Aktualisiere die Konfiguration für dieses HTTP-Streaming-Ziel.",
|
||||
"httpDestAddDescription": "Konfigurieren Sie einen neuen HTTP-Endpunkt, um die Ereignisse Ihrer Organisation zu empfangen.",
|
||||
"httpDestTabSettings": "Einstellungen",
|
||||
"httpDestTabHeaders": "Kopfzeilen",
|
||||
"httpDestTabBody": "Körper",
|
||||
"httpDestTabLogs": "Logs",
|
||||
"httpDestNamePlaceholder": "Mein HTTP-Ziel",
|
||||
"httpDestUrlLabel": "Ziel-URL",
|
||||
"httpDestUrlErrorHttpRequired": "URL muss http oder https verwenden",
|
||||
"httpDestUrlErrorHttpsRequired": "HTTPS wird für Cloud-Deployment benötigt",
|
||||
"httpDestUrlErrorInvalid": "Geben Sie eine gültige URL ein (z.B. https://example.com/webhook)",
|
||||
"httpDestAuthTitle": "Authentifizierung",
|
||||
"httpDestAuthDescription": "Legen Sie fest, wie Anfragen an Ihren Endpunkt authentifiziert werden.",
|
||||
"httpDestAuthNoneTitle": "Keine Authentifizierung",
|
||||
"httpDestAuthNoneDescription": "Sendet Anfragen ohne Autorisierungs-Header.",
|
||||
"httpDestAuthBearerTitle": "Bären-Token",
|
||||
"httpDestAuthBearerDescription": "Fügt jedem Anfrage-Header eine \"Authorization: Bearer '<token>'\" hinzu.",
|
||||
"httpDestAuthBearerPlaceholder": "Ihr API-Schlüssel oder Token",
|
||||
"httpDestAuthBasicTitle": "Einfacher Auth",
|
||||
"httpDestAuthBasicDescription": "Fügt einen \"Authorization: Basic '<credentials>'\"-Header hinzu. Geben Sie die Anmeldedaten als Benutzername:Passwort an.",
|
||||
"httpDestAuthBasicPlaceholder": "benutzername:password",
|
||||
"httpDestAuthCustomTitle": "Eigene Kopfzeile",
|
||||
"httpDestAuthCustomDescription": "Geben Sie einen eigenen HTTP-Header-Namen und einen Wert für die Authentifizierung an (z.B. X-API-Key).",
|
||||
"httpDestAuthCustomHeaderNamePlaceholder": "Headername (z.B. X-API-Key)",
|
||||
"httpDestAuthCustomHeaderValuePlaceholder": "Header-Wert",
|
||||
"httpDestCustomHeadersTitle": "Eigene HTTP-Header",
|
||||
"httpDestCustomHeadersDescription": "Fügen Sie jeder ausgehenden Anfrage benutzerdefinierte Kopfzeilen hinzu. Nützlich für statische Tokens oder einen benutzerdefinierten Content-Typ. Standardmäßig wird Content-Type: application/json gesendet.",
|
||||
"httpDestNoHeadersConfigured": "Keine benutzerdefinierten Header konfiguriert. Klicken Sie auf \"Header hinzufügen\", um einen hinzuzufügen.",
|
||||
"httpDestHeaderNamePlaceholder": "Header-Name",
|
||||
"httpDestHeaderValuePlaceholder": "Wert",
|
||||
"httpDestAddHeader": "Header hinzufügen",
|
||||
"httpDestBodyTemplateTitle": "Eigene Body-Vorlage",
|
||||
"httpDestBodyTemplateDescription": "Steuere die JSON-Payload-Struktur, die an deinen Endpunkt gesendet wurde. Wenn deaktiviert, wird für jede Veranstaltung ein Standard-JSON-Objekt gesendet.",
|
||||
"httpDestEnableBodyTemplate": "Eigene Körpervorlage aktivieren",
|
||||
"httpDestBodyTemplateLabel": "Body-Vorlage (JSON)",
|
||||
"httpDestBodyTemplateHint": "Verwenden Sie Template-Variablen, um Ereignisfelder in Ihrer Payload zu referenzieren.",
|
||||
"httpDestPayloadFormatTitle": "Payload-Format",
|
||||
"httpDestPayloadFormatDescription": "Wie Ereignisse in jedes Anfragegremium serialisiert werden.",
|
||||
"httpDestFormatJsonArrayTitle": "JSON Array",
|
||||
"httpDestFormatJsonArrayDescription": "Eine Anfrage pro Stapel ist ein JSON-Array. Kompatibel mit den meisten generischen Webhooks und Datadog.",
|
||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||
"httpDestFormatNdjsonDescription": "Eine Anfrage pro Batch, der Körper ist newline-getrenntes JSON — ein Objekt pro Zeile, kein äußeres Array. Benötigt von Splunk HEC, Elastic / OpenSearch, und Grafana Loki.",
|
||||
"httpDestFormatSingleTitle": "Ein Ereignis pro Anfrage",
|
||||
"httpDestFormatSingleDescription": "Sendet eine separate HTTP-POST für jedes einzelne Ereignis. Nur für Endpunkte, die Batches nicht handhaben können.",
|
||||
"httpDestLogTypesTitle": "Log-Typen",
|
||||
"httpDestLogTypesDescription": "Wählen Sie, welche Log-Typen an dieses Ziel weitergeleitet werden. Nur aktivierte Log-Typen werden gestreamt.",
|
||||
"httpDestAccessLogsTitle": "Zugriffsprotokolle",
|
||||
"httpDestAccessLogsDescription": "Ressourcenzugriffe, einschließlich authentifizierter und abgelehnter Anfragen.",
|
||||
"httpDestActionLogsTitle": "Aktionsprotokolle",
|
||||
"httpDestActionLogsDescription": "Administrative Maßnahmen, die von Benutzern innerhalb der Organisation durchgeführt werden.",
|
||||
"httpDestConnectionLogsTitle": "Verbindungsprotokolle",
|
||||
"httpDestConnectionLogsDescription": "Site- und Tunnelverbindungen, einschließlich Verbindungen und Trennungen.",
|
||||
"httpDestRequestLogsTitle": "Logs anfordern",
|
||||
"httpDestRequestLogsDescription": "HTTP-Request-Protokolle für proxiierte Ressourcen, einschließlich Methode, Pfad und Antwort-Code.",
|
||||
"httpDestSaveChanges": "Änderungen speichern",
|
||||
"httpDestCreateDestination": "Ziel erstellen",
|
||||
"httpDestUpdatedSuccess": "Ziel erfolgreich aktualisiert",
|
||||
"httpDestCreatedSuccess": "Ziel erfolgreich erstellt",
|
||||
"httpDestUpdateFailed": "Fehler beim Aktualisieren des Ziels",
|
||||
"httpDestCreateFailed": "Fehler beim Erstellen des Ziels"
|
||||
"firstSeen": "First Seen",
|
||||
"lastSeen": "Last Seen",
|
||||
"deviceSettingsDescription": "View device information and settings",
|
||||
"devicePendingApprovalDescription": "This device is waiting for approval",
|
||||
"deviceBlockedDescription": "This device is currently blocked. It won't be able to connect to any resources unless unblocked.",
|
||||
"unblockClient": "Unblock Client",
|
||||
"unblockClientDescription": "The device has been unblocked",
|
||||
"unarchiveClient": "Unarchive Client",
|
||||
"unarchiveClientDescription": "The device has been unarchived",
|
||||
"block": "Block",
|
||||
"unblock": "Unblock",
|
||||
"deviceActions": "Device Actions",
|
||||
"deviceActionsDescription": "Manage device status and access",
|
||||
"devicePendingApprovalBannerDescription": "This device is pending approval. It won't be able to connect to resources until approved.",
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected",
|
||||
"approvalsEmptyStateTitle": "Device Approvals Not Enabled",
|
||||
"approvalsEmptyStateDescription": "Enable device approvals for roles to require admin approval before users can connect new devices.",
|
||||
"approvalsEmptyStateStep1Title": "Go to Roles",
|
||||
"approvalsEmptyStateStep1Description": "Navigate to your organization's roles settings to configure device approvals.",
|
||||
"approvalsEmptyStateStep2Title": "Enable Device Approvals",
|
||||
"approvalsEmptyStateStep2Description": "Edit a role and enable the 'Require Device Approvals' option. Users with this role will need admin approval for new devices.",
|
||||
"approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review",
|
||||
"approvalsEmptyStateButtonText": "Manage Roles"
|
||||
}
|
||||
|
||||
+36
-535
@@ -18,8 +18,6 @@
|
||||
"componentsMember": "You're a member of {count, plural, =0 {no organization} one {one organization} other {# organizations}}.",
|
||||
"componentsInvalidKey": "Invalid or expired license keys detected. Follow license terms to continue using all features.",
|
||||
"dismiss": "Dismiss",
|
||||
"subscriptionViolationMessage": "You're beyond your limits for your current plan. Correct the problem by removing sites, users, or other resources to stay within your plan.",
|
||||
"subscriptionViolationViewBilling": "View billing",
|
||||
"componentsLicenseViolation": "License Violation: This server is using {usedSites} sites which exceeds its licensed limit of {maxSites} sites. Follow license terms to continue using all features.",
|
||||
"componentsSupporterMessage": "Thank you for supporting Pangolin as a {tier}!",
|
||||
"inviteErrorNotValid": "We're sorry, but it looks like the invite you're trying to access has not been accepted or is no longer valid.",
|
||||
@@ -57,10 +55,7 @@
|
||||
"siteDescription": "Create and manage sites to enable connectivity to private networks",
|
||||
"sitesBannerTitle": "Connect Any Network",
|
||||
"sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector (Newt) anywhere you can run a binary or container to establish the connection.",
|
||||
"sitesBannerButtonText": "Install Site Connector",
|
||||
"approvalsBannerTitle": "Approve or Deny Device Access",
|
||||
"approvalsBannerDescription": "Review and approve or deny device access requests from users. When device approvals are required, users must get admin approval before their devices can connect to your organization's resources.",
|
||||
"approvalsBannerButtonText": "Learn More",
|
||||
"sitesBannerButtonText": "Install Site",
|
||||
"siteCreate": "Create Site",
|
||||
"siteCreateDescription2": "Follow the steps below to create and connect a new site",
|
||||
"siteCreateDescription": "Create a new site to start connecting resources",
|
||||
@@ -81,8 +76,8 @@
|
||||
"siteConfirmCopy": "I have copied the config",
|
||||
"searchSitesProgress": "Search sites...",
|
||||
"siteAdd": "Add Site",
|
||||
"siteInstallNewt": "Install Site",
|
||||
"siteInstallNewtDescription": "Install the site connector for your system",
|
||||
"siteInstallNewt": "Install Newt",
|
||||
"siteInstallNewtDescription": "Get Newt running on your system",
|
||||
"WgConfiguration": "WireGuard Configuration",
|
||||
"WgConfigurationDescription": "Use the following configuration to connect to the network",
|
||||
"operatingSystem": "Operating System",
|
||||
@@ -148,11 +143,6 @@
|
||||
"createLink": "Create Link",
|
||||
"resourcesNotFound": "No resources found",
|
||||
"resourceSearch": "Search resources",
|
||||
"machineSearch": "Search machines",
|
||||
"machinesSearch": "Search machine clients...",
|
||||
"machineNotFound": "No machines found",
|
||||
"userDeviceSearch": "Search user devices",
|
||||
"userDevicesSearch": "Search user devices...",
|
||||
"openMenu": "Open menu",
|
||||
"resource": "Resource",
|
||||
"title": "Title",
|
||||
@@ -180,7 +170,6 @@
|
||||
"resourceHTTPDescription": "Proxy requests over HTTPS using a fully qualified domain name.",
|
||||
"resourceRaw": "Raw TCP/UDP Resource",
|
||||
"resourceRawDescription": "Proxy requests over raw TCP/UDP using a port number.",
|
||||
"resourceRawDescriptionCloud": "Proxy requests over raw TCP/UDP using a port number. Requires sites to connect to a remote node.",
|
||||
"resourceCreate": "Create Resource",
|
||||
"resourceCreateDescription": "Follow the steps below to create a new resource",
|
||||
"resourceSeeAll": "See All Resources",
|
||||
@@ -207,7 +196,6 @@
|
||||
"protocolSelect": "Select a protocol",
|
||||
"resourcePortNumber": "Port Number",
|
||||
"resourcePortNumberDescription": "The external port number to proxy requests.",
|
||||
"back": "Back",
|
||||
"cancel": "Cancel",
|
||||
"resourceConfig": "Configuration Snippets",
|
||||
"resourceConfigDescription": "Copy and paste these configuration snippets to set up the TCP/UDP resource",
|
||||
@@ -253,17 +241,6 @@
|
||||
"orgErrorDeleteMessage": "An error occurred while deleting the organization.",
|
||||
"orgDeleted": "Organization deleted",
|
||||
"orgDeletedMessage": "The organization and its data has been deleted.",
|
||||
"deleteAccount": "Delete Account",
|
||||
"deleteAccountDescription": "Permanently delete your account, all organizations you own, and all data within those organizations. This cannot be undone.",
|
||||
"deleteAccountButton": "Delete Account",
|
||||
"deleteAccountConfirmTitle": "Delete Account",
|
||||
"deleteAccountConfirmMessage": "This will permanently wipe your account, all organizations you own, and all data within those organizations. This cannot be undone.",
|
||||
"deleteAccountConfirmString": "delete account",
|
||||
"deleteAccountSuccess": "Account Deleted",
|
||||
"deleteAccountSuccessMessage": "Your account has been deleted.",
|
||||
"deleteAccountError": "Failed to delete account",
|
||||
"deleteAccountPreviewAccount": "Your Account",
|
||||
"deleteAccountPreviewOrgs": "Organizations you own (and all their data)",
|
||||
"orgMissing": "Organization ID Missing",
|
||||
"orgMissingMessage": "Unable to regenerate invitation without an organization ID.",
|
||||
"accessUsersManage": "Manage Users",
|
||||
@@ -280,8 +257,6 @@
|
||||
"accessRolesSearch": "Search roles...",
|
||||
"accessRolesAdd": "Add Role",
|
||||
"accessRoleDelete": "Delete Role",
|
||||
"accessApprovalsManage": "Manage Approvals",
|
||||
"accessApprovalsDescription": "View and manage pending approvals for access to this organization",
|
||||
"description": "Description",
|
||||
"inviteTitle": "Open Invitations",
|
||||
"inviteDescription": "Manage invitations for other users to join the organization",
|
||||
@@ -328,54 +303,6 @@
|
||||
"apiKeysDelete": "Delete API Key",
|
||||
"apiKeysManage": "Manage API Keys",
|
||||
"apiKeysDescription": "API keys are used to authenticate with the integration API",
|
||||
"provisioningKeysTitle": "Provisioning Key",
|
||||
"provisioningKeysManage": "Manage Provisioning Keys",
|
||||
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
|
||||
"provisioningManage": "Provisioning",
|
||||
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
|
||||
"pendingSites": "Pending Sites",
|
||||
"siteApproveSuccess": "Site approved successfully",
|
||||
"siteApproveError": "Error approving site",
|
||||
"provisioningKeys": "Provisioning Keys",
|
||||
"searchProvisioningKeys": "Search provisioning keys...",
|
||||
"provisioningKeysAdd": "Generate Provisioning Key",
|
||||
"provisioningKeysErrorDelete": "Error deleting provisioning key",
|
||||
"provisioningKeysErrorDeleteMessage": "Error deleting provisioning key",
|
||||
"provisioningKeysQuestionRemove": "Are you sure you want to remove this provisioning key from the organization?",
|
||||
"provisioningKeysMessageRemove": "Once removed, the key can no longer be used for site provisioning.",
|
||||
"provisioningKeysDeleteConfirm": "Confirm Delete Provisioning Key",
|
||||
"provisioningKeysDelete": "Delete Provisioning key",
|
||||
"provisioningKeysCreate": "Generate Provisioning Key",
|
||||
"provisioningKeysCreateDescription": "Generate a new provisioning key for the organization",
|
||||
"provisioningKeysSeeAll": "See all provisioning keys",
|
||||
"provisioningKeysSave": "Save the provisioning key",
|
||||
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
|
||||
"provisioningKeysErrorCreate": "Error creating provisioning key",
|
||||
"provisioningKeysList": "New provisioning key",
|
||||
"provisioningKeysMaxBatchSize": "Max batch size",
|
||||
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
|
||||
"provisioningKeysMaxBatchUnlimited": "Unlimited",
|
||||
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (1–1,000,000).",
|
||||
"provisioningKeysValidUntil": "Valid until",
|
||||
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
|
||||
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
|
||||
"provisioningKeysNumUsed": "Times used",
|
||||
"provisioningKeysLastUsed": "Last used",
|
||||
"provisioningKeysNoExpiry": "No expiration",
|
||||
"provisioningKeysNeverUsed": "Never",
|
||||
"provisioningKeysEdit": "Edit Provisioning Key",
|
||||
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
|
||||
"provisioningKeysApproveNewSites": "Approve new sites",
|
||||
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
|
||||
"provisioningKeysUpdateError": "Error updating provisioning key",
|
||||
"provisioningKeysUpdated": "Provisioning key updated",
|
||||
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
|
||||
"provisioningKeysBannerTitle": "Site Provisioning Keys",
|
||||
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup - no need to set up separate credentials for each site.",
|
||||
"provisioningKeysBannerButtonText": "Learn More",
|
||||
"pendingSitesBannerTitle": "Pending Sites",
|
||||
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review.",
|
||||
"pendingSitesBannerButtonText": "Learn More",
|
||||
"apiKeysSettings": "{apiKeyName} Settings",
|
||||
"userTitle": "Manage All Users",
|
||||
"userDescription": "View and manage all users in the system",
|
||||
@@ -523,20 +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",
|
||||
"approvalLoadMore": "Load more",
|
||||
"loadingApprovals": "Loading Approvals",
|
||||
"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",
|
||||
@@ -562,12 +475,9 @@
|
||||
"userSaved": "User saved",
|
||||
"userSavedDescription": "The user has been updated.",
|
||||
"autoProvisioned": "Auto Provisioned",
|
||||
"autoProvisionSettings": "Auto Provision Settings",
|
||||
"autoProvisionedDescription": "Allow this user to be automatically managed by identity provider",
|
||||
"accessControlsDescription": "Manage what this user can access and do in the organization",
|
||||
"accessControlsSubmit": "Save Access Controls",
|
||||
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
|
||||
"singleRolePerUserEditionNotice": "This edition only supports one role per user.",
|
||||
"roles": "Roles",
|
||||
"accessUsersRoles": "Manage Users & Roles",
|
||||
"accessUsersRolesDescription": "Invite users and add them to roles to manage access to the organization",
|
||||
@@ -624,8 +534,6 @@
|
||||
"targetErrorInvalidPortDescription": "Please enter a valid port number",
|
||||
"targetErrorNoSite": "No site selected",
|
||||
"targetErrorNoSiteDescription": "Please select a site for the target",
|
||||
"targetTargetsCleared": "Targets cleared",
|
||||
"targetTargetsClearedDescription": "All targets have been removed from this resource",
|
||||
"targetCreated": "Target created",
|
||||
"targetCreatedDescription": "Target has been created successfully",
|
||||
"targetErrorCreate": "Failed to create target",
|
||||
@@ -709,7 +617,6 @@
|
||||
"resourcesErrorUpdate": "Failed to toggle resource",
|
||||
"resourcesErrorUpdateDescription": "An error occurred while updating the resource",
|
||||
"access": "Access",
|
||||
"accessControl": "Access Control",
|
||||
"shareLink": "{resource} Share Link",
|
||||
"resourceSelect": "Select resource",
|
||||
"shareLinks": "Share Links",
|
||||
@@ -822,35 +729,22 @@
|
||||
"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",
|
||||
"accessRoleRemoved": "Role removed",
|
||||
"accessRoleRemovedDescription": "The role has been successfully removed.",
|
||||
"accessRoleRequiredRemove": "Before deleting this role, please select a new role to transfer existing members to.",
|
||||
"network": "Network",
|
||||
"manage": "Manage",
|
||||
"sitesNotFound": "No sites found.",
|
||||
"pangolinServerAdmin": "Server Admin - Pangolin",
|
||||
@@ -866,9 +760,6 @@
|
||||
"sitestCountIncrease": "Increase site count",
|
||||
"idpManage": "Manage Identity Providers",
|
||||
"idpManageDescription": "View and manage identity providers in the system",
|
||||
"idpGlobalModeBanner": "Identity providers (IdPs) per organization are disabled on this server. It is using global IdPs (shared across all organizations). Manage global IdPs in the <adminPanelLink>admin panel</adminPanelLink>. To enable IdPs per organization, edit the server config and set IdP mode to org. <configDocsLink>See the docs</configDocsLink>. If you want to continue using global IdPs and make this disappear from the organization settings, explicitly set the mode to global in the config.",
|
||||
"idpGlobalModeBannerUpgradeRequired": "Identity providers (IdPs) per organization are disabled on this server. It is using global IdPs (shared across all organizations). Manage global IdPs in the <adminPanelLink>admin panel</adminPanelLink>. To use identity providers per organization, you must upgrade to the Enterprise edition.",
|
||||
"idpGlobalModeBannerLicenseRequired": "Identity providers (IdPs) per organization are disabled on this server. It is using global IdPs (shared across all organizations). Manage global IdPs in the <adminPanelLink>admin panel</adminPanelLink>. To use identity providers per organization, an Enterprise license is required.",
|
||||
"idpDeletedDescription": "Identity provider deleted successfully",
|
||||
"idpOidc": "OAuth2/OIDC",
|
||||
"idpQuestionRemove": "Are you sure you want to permanently delete the identity provider?",
|
||||
@@ -945,7 +836,7 @@
|
||||
"defaultMappingsRole": "Default Role Mapping",
|
||||
"defaultMappingsRoleDescription": "The result of this expression must return the role name as defined in the organization as a string.",
|
||||
"defaultMappingsOrg": "Default Organization Mapping",
|
||||
"defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
|
||||
"defaultMappingsOrgDescription": "This expression must return the org ID or true for the user to be allowed to access the organization.",
|
||||
"defaultMappingsSubmit": "Save Default Mappings",
|
||||
"orgPoliciesEdit": "Edit Organization Policy",
|
||||
"org": "Organization",
|
||||
@@ -983,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",
|
||||
@@ -1063,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",
|
||||
@@ -1092,12 +983,12 @@
|
||||
"pangolinSetup": "Setup - Pangolin",
|
||||
"orgNameRequired": "Organization name is required",
|
||||
"orgIdRequired": "Organization ID is required",
|
||||
"orgIdMaxLength": "Organization ID must be at most 32 characters",
|
||||
"orgErrorCreate": "An error occurred while creating org",
|
||||
"pageNotFound": "Page Not Found",
|
||||
"pageNotFoundDescription": "Oops! The page you're looking for doesn't exist.",
|
||||
"overview": "Overview",
|
||||
"home": "Home",
|
||||
"accessControl": "Access Control",
|
||||
"settings": "Settings",
|
||||
"usersAll": "All Users",
|
||||
"license": "License",
|
||||
@@ -1160,12 +1051,6 @@
|
||||
"actionGetUser": "Get User",
|
||||
"actionGetOrgUser": "Get Organization User",
|
||||
"actionListOrgDomains": "List Organization Domains",
|
||||
"actionGetDomain": "Get Domain",
|
||||
"actionCreateOrgDomain": "Create Domain",
|
||||
"actionUpdateOrgDomain": "Update Domain",
|
||||
"actionDeleteOrgDomain": "Delete Domain",
|
||||
"actionGetDNSRecords": "Get DNS Records",
|
||||
"actionRestartOrgDomain": "Restart Domain",
|
||||
"actionCreateSite": "Create Site",
|
||||
"actionDeleteSite": "Delete Site",
|
||||
"actionGetSite": "Get Site",
|
||||
@@ -1177,7 +1062,6 @@
|
||||
"setupTokenDescription": "Enter the setup token from the server console.",
|
||||
"setupTokenRequired": "Setup token is required",
|
||||
"actionUpdateSite": "Update Site",
|
||||
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
|
||||
"actionListSiteRoles": "List Allowed Site Roles",
|
||||
"actionCreateResource": "Create Resource",
|
||||
"actionDeleteResource": "Delete Resource",
|
||||
@@ -1207,7 +1091,6 @@
|
||||
"actionRemoveUser": "Remove User",
|
||||
"actionListUsers": "List Users",
|
||||
"actionAddUserRole": "Add User Role",
|
||||
"actionSetUserOrgRoles": "Set User Roles",
|
||||
"actionGenerateAccessToken": "Generate Access Token",
|
||||
"actionDeleteAccessToken": "Delete Access Token",
|
||||
"actionListAccessTokens": "List Access Tokens",
|
||||
@@ -1235,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",
|
||||
@@ -1252,18 +1131,17 @@
|
||||
"actionViewLogs": "View Logs",
|
||||
"noneSelected": "None selected",
|
||||
"orgNotFound2": "No organizations found.",
|
||||
"searchPlaceholder": "Search...",
|
||||
"emptySearchOptions": "No options found",
|
||||
"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",
|
||||
@@ -1311,34 +1189,29 @@
|
||||
"sidebarOverview": "Overview",
|
||||
"sidebarHome": "Home",
|
||||
"sidebarSites": "Sites",
|
||||
"sidebarApprovals": "Approval Requests",
|
||||
"sidebarResources": "Resources",
|
||||
"sidebarProxyResources": "Public",
|
||||
"sidebarClientResources": "Private",
|
||||
"sidebarAccessControl": "Access Control",
|
||||
"sidebarLogsAndAnalytics": "Logs & Analytics",
|
||||
"sidebarTeam": "Team",
|
||||
"sidebarUsers": "Users",
|
||||
"sidebarAdmin": "Admin",
|
||||
"sidebarInvitations": "Invitations",
|
||||
"sidebarRoles": "Roles",
|
||||
"sidebarShareableLinks": "Links",
|
||||
"sidebarApiKeys": "API Keys",
|
||||
"sidebarProvisioning": "Provisioning",
|
||||
"sidebarSettings": "Settings",
|
||||
"sidebarAllUsers": "All Users",
|
||||
"sidebarIdentityProviders": "Identity Providers",
|
||||
"sidebarLicense": "License",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarUserDevices": "User Devices",
|
||||
"sidebarUserDevices": "Users",
|
||||
"sidebarMachineClients": "Machines",
|
||||
"sidebarDomains": "Domains",
|
||||
"sidebarGeneral": "Manage",
|
||||
"sidebarLogAndAnalytics": "Log & Analytics",
|
||||
"sidebarBluePrints": "Blueprints",
|
||||
"sidebarOrganization": "Organization",
|
||||
"sidebarManagement": "Management",
|
||||
"sidebarBillingAndLicenses": "Billing & Licenses",
|
||||
"sidebarLogsAnalytics": "Analytics",
|
||||
"blueprints": "Blueprints",
|
||||
"blueprintsDescription": "Apply declarative configurations and view previous runs",
|
||||
@@ -1359,7 +1232,8 @@
|
||||
"contents": "Contents",
|
||||
"parsedContents": "Parsed Contents (Read Only)",
|
||||
"enableDockerSocket": "Enable Docker Blueprint",
|
||||
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt. Read about how this works in <docsLink>the documentation</docsLink>.",
|
||||
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
|
||||
"enableDockerSocketLink": "Learn More",
|
||||
"viewDockerContainers": "View Docker Containers",
|
||||
"containersIn": "Containers in {siteName}",
|
||||
"selectContainerDescription": "Select any container to use as a hostname for this target. Click a port to use a port.",
|
||||
@@ -1403,7 +1277,6 @@
|
||||
"setupErrorCreateAdmin": "An error occurred while creating the server admin account.",
|
||||
"certificateStatus": "Certificate Status",
|
||||
"loading": "Loading",
|
||||
"loadingAnalytics": "Loading Analytics",
|
||||
"restart": "Restart",
|
||||
"domains": "Domains",
|
||||
"domainsDescription": "Create and manage domains available in the organization",
|
||||
@@ -1431,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",
|
||||
@@ -1487,7 +1359,6 @@
|
||||
"domainPickerNamespace": "Namespace: {namespace}",
|
||||
"domainPickerShowMore": "Show More",
|
||||
"regionSelectorTitle": "Select Region",
|
||||
"domainPickerRemoteExitNodeWarning": "Provided domains are not supported when sites connect to remote exit nodes. For resources to be available on remote nodes, use a custom domain instead.",
|
||||
"regionSelectorInfo": "Selecting a region helps us provide better performance for your location. You do not have to be in the same region as your server.",
|
||||
"regionSelectorPlaceholder": "Choose a region",
|
||||
"regionSelectorComingSoon": "Coming Soon",
|
||||
@@ -1497,11 +1368,10 @@
|
||||
"billingUsageLimitsOverview": "Usage Limits Overview",
|
||||
"billingMonitorUsage": "Monitor your usage against configured limits. If you need limits increased please contact us support@pangolin.net.",
|
||||
"billingDataUsage": "Data Usage",
|
||||
"billingSites": "Sites",
|
||||
"billingUsers": "Users",
|
||||
"billingDomains": "Domains",
|
||||
"billingOrganizations": "Orgs",
|
||||
"billingRemoteExitNodes": "Remote Nodes",
|
||||
"billingOnlineTime": "Site Online Time",
|
||||
"billingUsers": "Active Users",
|
||||
"billingDomains": "Active Domains",
|
||||
"billingRemoteExitNodes": "Active Self-hosted Nodes",
|
||||
"billingNoLimitConfigured": "No limit configured",
|
||||
"billingEstimatedPeriod": "Estimated Billing Period",
|
||||
"billingIncludedUsage": "Included Usage",
|
||||
@@ -1526,24 +1396,15 @@
|
||||
"billingFailedToGetPortalUrl": "Failed to get portal URL",
|
||||
"billingPortalError": "Portal Error",
|
||||
"billingDataUsageInfo": "You're charged for all data transferred through your secure tunnels when connected to the cloud. This includes both incoming and outgoing traffic across all your sites. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Data is not charged when using nodes.",
|
||||
"billingSInfo": "How many sites you can use",
|
||||
"billingUsersInfo": "How many users you can use",
|
||||
"billingDomainInfo": "How many domains you can use",
|
||||
"billingRemoteExitNodesInfo": "How many remote nodes you can use",
|
||||
"billingLicenseKeys": "License Keys",
|
||||
"billingLicenseKeysDescription": "Manage your license key subscriptions",
|
||||
"billingLicenseSubscription": "License Subscription",
|
||||
"billingInactive": "Inactive",
|
||||
"billingLicenseItem": "License Item",
|
||||
"billingQuantity": "Quantity",
|
||||
"billingTotal": "total",
|
||||
"billingModifyLicenses": "Modify License Subscription",
|
||||
"billingOnlineTimeInfo": "You're charged based on how long your sites stay connected to the cloud. For example, 44,640 minutes equals one site running 24/7 for a full month. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Time is not charged when using nodes.",
|
||||
"billingUsersInfo": "You're charged for each user in the organization. Billing is calculated daily based on the number of active user accounts in your org.",
|
||||
"billingDomainInfo": "You're charged for each domain in the organization. Billing is calculated daily based on the number of active domain accounts in your org.",
|
||||
"billingRemoteExitNodesInfo": "You're charged for each managed Node in the organization. Billing is calculated daily based on the number of active managed Nodes in your org.",
|
||||
"domainNotFound": "Domain Not Found",
|
||||
"domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.",
|
||||
"failed": "Failed",
|
||||
"createNewOrgDescription": "Create a new organization",
|
||||
"organization": "Organization",
|
||||
"primary": "Primary",
|
||||
"port": "Port",
|
||||
"securityKeyManage": "Manage Security Keys",
|
||||
"securityKeyDescription": "Add or remove security keys for passwordless authentication",
|
||||
@@ -1559,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...",
|
||||
@@ -1615,42 +1476,6 @@
|
||||
"resourcePortRequired": "Port number is required for non-HTTP resources",
|
||||
"resourcePortNotAllowed": "Port number should not be set for HTTP resources",
|
||||
"billingPricingCalculatorLink": "Pricing Calculator",
|
||||
"billingYourPlan": "Your Plan",
|
||||
"billingViewOrModifyPlan": "View or modify your current plan",
|
||||
"billingViewPlanDetails": "View Plan Details",
|
||||
"billingUsageAndLimits": "Usage and Limits",
|
||||
"billingViewUsageAndLimits": "View your plan's limits and current usage",
|
||||
"billingCurrentUsage": "Current Usage",
|
||||
"billingMaximumLimits": "Maximum Limits",
|
||||
"billingRemoteNodes": "Remote Nodes",
|
||||
"billingUnlimited": "Unlimited",
|
||||
"billingPaidLicenseKeys": "Paid License Keys",
|
||||
"billingManageLicenseSubscription": "Manage your subscription for paid self-hosted license keys",
|
||||
"billingCurrentKeys": "Current Keys",
|
||||
"billingModifyCurrentPlan": "Modify Current Plan",
|
||||
"billingConfirmUpgrade": "Confirm Upgrade",
|
||||
"billingConfirmDowngrade": "Confirm Downgrade",
|
||||
"billingConfirmUpgradeDescription": "You are about to upgrade your plan. Review the new limits and pricing below.",
|
||||
"billingConfirmDowngradeDescription": "You are about to downgrade your plan. Review the new limits and pricing below.",
|
||||
"billingPlanIncludes": "Plan Includes",
|
||||
"billingProcessing": "Processing...",
|
||||
"billingConfirmUpgradeButton": "Confirm Upgrade",
|
||||
"billingConfirmDowngradeButton": "Confirm Downgrade",
|
||||
"billingLimitViolationWarning": "Usage Exceeds New Plan Limits",
|
||||
"billingLimitViolationDescription": "Your current usage exceeds the limits of this plan. After downgrading, all actions will be disabled until you reduce usage within the new limits. Please review the features below that are currently over the limits. Limits in violation:",
|
||||
"billingFeatureLossWarning": "Feature Availability Notice",
|
||||
"billingFeatureLossDescription": "By downgrading, features not available in the new plan will be automatically disabled. Some settings and configurations may be lost. Please review the pricing matrix to understand which features will no longer be available.",
|
||||
"billingUsageExceedsLimit": "Current usage ({current}) exceeds limit ({limit})",
|
||||
"billingPastDueTitle": "Payment Past Due",
|
||||
"billingPastDueDescription": "Your payment is past due. Please update your payment method to continue using your current plan features. If not resolved, your subscription will be canceled and you'll be reverted to the free tier.",
|
||||
"billingUnpaidTitle": "Subscription Unpaid",
|
||||
"billingUnpaidDescription": "Your subscription is unpaid and you have been reverted to the free tier. Please update your payment method to restore your subscription.",
|
||||
"billingIncompleteTitle": "Payment Incomplete",
|
||||
"billingIncompleteDescription": "Your payment is incomplete. Please complete the payment process to activate your subscription.",
|
||||
"billingIncompleteExpiredTitle": "Payment Expired",
|
||||
"billingIncompleteExpiredDescription": "Your payment was never completed and has expired. You have been reverted to the free tier. Please subscribe again to restore access to paid features.",
|
||||
"billingManageSubscription": "Manage your subscription",
|
||||
"billingResolvePaymentIssue": "Please resolve your payment issue before upgrading or downgrading",
|
||||
"signUpTerms": {
|
||||
"IAgreeToThe": "I agree to the",
|
||||
"termsOfService": "terms of service",
|
||||
@@ -1675,8 +1500,8 @@
|
||||
"addressDescription": "The internal address of the client. Must fall within the organization's subnet.",
|
||||
"selectSites": "Select sites",
|
||||
"sitesDescription": "The client will have connectivity to the selected sites",
|
||||
"clientInstallOlm": "Install Machine Client",
|
||||
"clientInstallOlmDescription": "Install the machine client for your system",
|
||||
"clientInstallOlm": "Install Olm",
|
||||
"clientInstallOlmDescription": "Get Olm running on your system",
|
||||
"clientOlmCredentials": "Credentials",
|
||||
"clientOlmCredentialsDescription": "This is how the client will authenticate with the server",
|
||||
"olmEndpoint": "Endpoint",
|
||||
@@ -1722,26 +1547,6 @@
|
||||
"IntervalSeconds": "Healthy Interval",
|
||||
"timeoutSeconds": "Timeout (sec)",
|
||||
"timeIsInSeconds": "Time is in seconds",
|
||||
"requireDeviceApproval": "Require Device Approvals",
|
||||
"requireDeviceApprovalDescription": "Users with this role need new devices approved by an admin before they can connect and access resources.",
|
||||
"sshAccess": "SSH Access",
|
||||
"roleAllowSsh": "Allow SSH",
|
||||
"roleAllowSshAllow": "Allow",
|
||||
"roleAllowSshDisallow": "Disallow",
|
||||
"roleAllowSshDescription": "Allow users with this role to connect to resources via SSH. When disabled, the role cannot use SSH access.",
|
||||
"sshSudoMode": "Sudo Access",
|
||||
"sshSudoModeNone": "None",
|
||||
"sshSudoModeNoneDescription": "User cannot run commands with sudo.",
|
||||
"sshSudoModeFull": "Full Sudo",
|
||||
"sshSudoModeFullDescription": "User can run any command with sudo.",
|
||||
"sshSudoModeCommands": "Commands",
|
||||
"sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.",
|
||||
"sshSudo": "Allow sudo",
|
||||
"sshSudoCommands": "Sudo Commands",
|
||||
"sshSudoCommandsDescription": "Comma separated list of commands the user is allowed to run with sudo.",
|
||||
"sshCreateHomeDir": "Create Home Directory",
|
||||
"sshUnixGroups": "Unix Groups",
|
||||
"sshUnixGroupsDescription": "Comma separated Unix groups to add the user to on the target host.",
|
||||
"retryAttempts": "Retry Attempts",
|
||||
"expectedResponseCodes": "Expected Response Codes",
|
||||
"expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.",
|
||||
@@ -1782,8 +1587,6 @@
|
||||
"resourcesTableNoInternalResourcesFound": "No internal resources found.",
|
||||
"resourcesTableDestination": "Destination",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAliasAddress": "Alias Address",
|
||||
"resourcesTableAliasAddressInfo": "This address is part of the organization's utility subnet. It's used to resolve alias records using internal DNS resolution.",
|
||||
"resourcesTableClients": "Clients",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.",
|
||||
"resourcesTableNoTargets": "No targets",
|
||||
@@ -1950,40 +1753,6 @@
|
||||
"exitNode": "Exit Node",
|
||||
"country": "Country",
|
||||
"rulesMatchCountry": "Currently based on source IP",
|
||||
"region": "Region",
|
||||
"selectRegion": "Select region",
|
||||
"searchRegions": "Search regions...",
|
||||
"noRegionFound": "No region found.",
|
||||
"rulesMatchRegion": "Select a regional grouping of countries",
|
||||
"rulesErrorInvalidRegion": "Invalid region",
|
||||
"rulesErrorInvalidRegionDescription": "Please select a valid region.",
|
||||
"regionAfrica": "Africa",
|
||||
"regionNorthernAfrica": "Northern Africa",
|
||||
"regionEasternAfrica": "Eastern Africa",
|
||||
"regionMiddleAfrica": "Middle Africa",
|
||||
"regionSouthernAfrica": "Southern Africa",
|
||||
"regionWesternAfrica": "Western Africa",
|
||||
"regionAmericas": "Americas",
|
||||
"regionCaribbean": "Caribbean",
|
||||
"regionCentralAmerica": "Central America",
|
||||
"regionSouthAmerica": "South America",
|
||||
"regionNorthernAmerica": "Northern America",
|
||||
"regionAsia": "Asia",
|
||||
"regionCentralAsia": "Central Asia",
|
||||
"regionEasternAsia": "Eastern Asia",
|
||||
"regionSouthEasternAsia": "South-Eastern Asia",
|
||||
"regionSouthernAsia": "Southern Asia",
|
||||
"regionWesternAsia": "Western Asia",
|
||||
"regionEurope": "Europe",
|
||||
"regionEasternEurope": "Eastern Europe",
|
||||
"regionNorthernEurope": "Northern Europe",
|
||||
"regionSouthernEurope": "Southern Europe",
|
||||
"regionWesternEurope": "Western Europe",
|
||||
"regionOceania": "Oceania",
|
||||
"regionAustraliaAndNewZealand": "Australia and New Zealand",
|
||||
"regionMelanesia": "Melanesia",
|
||||
"regionMicronesia": "Micronesia",
|
||||
"regionPolynesia": "Polynesia",
|
||||
"managedSelfHosted": {
|
||||
"title": "Managed Self-Hosted",
|
||||
"description": "More reliable and low-maintenance self-hosted Pangolin server with extra bells and whistles",
|
||||
@@ -2032,25 +1801,6 @@
|
||||
"invalidValue": "Invalid value",
|
||||
"idpTypeLabel": "Identity Provider Type",
|
||||
"roleMappingExpressionPlaceholder": "e.g., contains(groups, 'admin') && 'Admin' || 'Member'",
|
||||
"roleMappingModeFixedRoles": "Fixed Roles",
|
||||
"roleMappingModeMappingBuilder": "Mapping Builder",
|
||||
"roleMappingModeRawExpression": "Raw Expression",
|
||||
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
|
||||
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
|
||||
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
|
||||
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
|
||||
"roleMappingClaimPath": "Claim Path",
|
||||
"roleMappingClaimPathPlaceholder": "groups",
|
||||
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
|
||||
"roleMappingMatchValue": "Match Value",
|
||||
"roleMappingAssignRoles": "Assign Roles",
|
||||
"roleMappingAddMappingRule": "Add Mapping Rule",
|
||||
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
|
||||
"roleMappingRawExpressionResultDescriptionSingleRole": "Expression must evaluate to a string (a single role name).",
|
||||
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
|
||||
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
|
||||
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
|
||||
"roleMappingRemoveRule": "Remove",
|
||||
"idpGoogleConfiguration": "Google Configuration",
|
||||
"idpGoogleConfigurationDescription": "Configure the Google OAuth2 credentials",
|
||||
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
|
||||
@@ -2087,9 +1837,6 @@
|
||||
"authPageBrandingQuestionRemove": "Are you sure you want to remove the branding for Auth Pages ?",
|
||||
"authPageBrandingDeleteConfirm": "Confirm Delete Branding",
|
||||
"brandingLogoURL": "Logo URL",
|
||||
"brandingLogoURLOrPath": "Logo URL or Path",
|
||||
"brandingLogoPathDescription": "Enter a URL or a local path.",
|
||||
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
|
||||
"brandingPrimaryColor": "Primary Color",
|
||||
"brandingLogoWidth": "Width (px)",
|
||||
"brandingLogoHeight": "Height (px)",
|
||||
@@ -2139,13 +1886,6 @@
|
||||
"orgAuthBackToSignIn": "Back to standard sign in",
|
||||
"orgAuthNoAccount": "Don't have an account?",
|
||||
"subscriptionRequiredToUse": "A subscription is required to use this feature.",
|
||||
"mustUpgradeToUse": "You must upgrade your subscription to use this feature.",
|
||||
"subscriptionRequiredTierToUse": "This feature requires <tierLink>{tier}</tierLink>.",
|
||||
"upgradeToTierToUse": "Upgrade to <tierLink>{tier}</tierLink> to use this feature.",
|
||||
"subscriptionTierTier1": "Home",
|
||||
"subscriptionTierTier2": "Team",
|
||||
"subscriptionTierTier3": "Business",
|
||||
"subscriptionTierEnterprise": "Enterprise",
|
||||
"idpDisabled": "Identity providers are disabled.",
|
||||
"orgAuthPageDisabled": "Organization auth page is disabled.",
|
||||
"domainRestartedDescription": "Domain verification restarted successfully",
|
||||
@@ -2231,10 +1971,10 @@
|
||||
"manageMachineClients": "Manage Machine Clients",
|
||||
"manageMachineClientsDescription": "Create and manage clients that servers and systems use to privately connect to resources",
|
||||
"machineClientsBannerTitle": "Servers & Automated Systems",
|
||||
"machineClientsBannerDescription": "Machine clients are for servers and automated systems that are not associated with a specific user. They authenticate with an ID and secret, and can be deployed as a CLI or a container.",
|
||||
"machineClientsBannerDescription": "Machine clients are for servers and automated systems that are not associated with a specific user. They authenticate with an ID and secret, and can run with Pangolin CLI, Olm CLI, or Olm as a container.",
|
||||
"machineClientsBannerPangolinCLI": "Pangolin CLI",
|
||||
"machineClientsBannerOlmCLI": "Olm CLI",
|
||||
"machineClientsBannerOlmContainer": "Container",
|
||||
"machineClientsBannerOlmContainer": "Olm Container",
|
||||
"clientsTableUserClients": "User",
|
||||
"clientsTableMachineClients": "Machine",
|
||||
"licenseTableValidUntil": "Valid Until",
|
||||
@@ -2333,32 +2073,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"newPricingLicenseForm": {
|
||||
"title": "Get a license",
|
||||
"description": "Choose a plan and tell us how you plan to use Pangolin.",
|
||||
"chooseTier": "Choose your plan",
|
||||
"viewPricingLink": "See pricing, features, and limits",
|
||||
"tiers": {
|
||||
"starter": {
|
||||
"title": "Starter",
|
||||
"description": "Enterprise features, 25 users, 25 sites, and community support."
|
||||
},
|
||||
"scale": {
|
||||
"title": "Scale",
|
||||
"description": "Enterprise features, 50 users, 50 sites, and priority support."
|
||||
}
|
||||
},
|
||||
"personalUseOnly": "Personal use only (free license - no checkout)",
|
||||
"buttons": {
|
||||
"continueToCheckout": "Continue to Checkout"
|
||||
},
|
||||
"toasts": {
|
||||
"checkoutError": {
|
||||
"title": "Checkout error",
|
||||
"description": "Could not start checkout. Please try again."
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": "Priority",
|
||||
"priorityDescription": "Higher priority routes are evaluated first. Priority = 100 means automatic ordering (system decides). Use another number to enforce manual priority.",
|
||||
"instanceName": "Instance Name",
|
||||
@@ -2447,8 +2161,6 @@
|
||||
"logRetentionAccessDescription": "How long to retain access logs",
|
||||
"logRetentionActionLabel": "Action Log Retention",
|
||||
"logRetentionActionDescription": "How long to retain action logs",
|
||||
"logRetentionConnectionLabel": "Connection Log Retention",
|
||||
"logRetentionConnectionDescription": "How long to retain connection logs",
|
||||
"logRetentionDisabled": "Disabled",
|
||||
"logRetention3Days": "3 days",
|
||||
"logRetention7Days": "7 days",
|
||||
@@ -2459,15 +2171,7 @@
|
||||
"logRetentionEndOfFollowingYear": "End of following year",
|
||||
"actionLogsDescription": "View a history of actions performed in this organization",
|
||||
"accessLogsDescription": "View access auth requests for resources in this organization",
|
||||
"connectionLogs": "Connection Logs",
|
||||
"connectionLogsDescription": "View connection logs for tunnels in this organization",
|
||||
"sidebarLogsConnection": "Connection Logs",
|
||||
"sidebarLogsStreaming": "Streaming",
|
||||
"sourceAddress": "Source Address",
|
||||
"destinationAddress": "Destination Address",
|
||||
"duration": "Duration",
|
||||
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
|
||||
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
|
||||
"licenseRequiredToUse": "An Enterprise license is required to use this feature.",
|
||||
"certResolver": "Certificate Resolver",
|
||||
"certResolverDescription": "Select the certificate resolver to use for this resource.",
|
||||
"selectCertResolver": "Select Certificate Resolver",
|
||||
@@ -2528,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",
|
||||
@@ -2542,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",
|
||||
@@ -2604,14 +2306,10 @@
|
||||
"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",
|
||||
"run": "Run",
|
||||
"envFile": "Environment File",
|
||||
"serviceFile": "Service File",
|
||||
"enableAndStart": "Enable and Start",
|
||||
"clientNameDescription": "The display name of the client that can be changed later.",
|
||||
"clientAddress": "Client Address (Advanced)",
|
||||
"setupFailedToFetchSubnet": "Failed to fetch default subnet",
|
||||
@@ -2635,9 +2333,9 @@
|
||||
"remoteExitNodeRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this remote exit node?",
|
||||
"remoteExitNodeRegenerateCredentialsWarning": "This will regenerate the credentials. The remote exit node will stay connected until you manually restart it and use the new credentials.",
|
||||
"agent": "Agent",
|
||||
"personalUseOnly": "Personal Use Only",
|
||||
"loginPageLicenseWatermark": "This instance is licensed for personal use only.",
|
||||
"instanceIsUnlicensed": "This instance is unlicensed.",
|
||||
"personalUseOnly": "Personal Use Only",
|
||||
"loginPageLicenseWatermark": "This instance is licensed for personal use only.",
|
||||
"instanceIsUnlicensed": "This instance is unlicensed.",
|
||||
"portRestrictions": "Port Restrictions",
|
||||
"allPorts": "All",
|
||||
"custom": "Custom",
|
||||
@@ -2667,17 +2365,6 @@
|
||||
"editInternalResourceDialogAccessControl": "Access Control",
|
||||
"editInternalResourceDialogAccessControlDescription": "Control which roles, users, and machine clients have access to this resource when connected. Admins always have access.",
|
||||
"editInternalResourceDialogPortRangeValidationError": "Port range must be \"*\" for all ports, or a comma-separated list of ports and ranges (e.g., \"80,443,8000-9000\"). Ports must be between 1 and 65535.",
|
||||
"internalResourceAuthDaemonStrategy": "SSH Auth Daemon Location",
|
||||
"internalResourceAuthDaemonStrategyDescription": "Choose where the SSH authentication daemon runs: on the site (Newt) or on a remote host.",
|
||||
"internalResourceAuthDaemonDescription": "The SSH authentication daemon handles SSH key signing and PAM authentication for this resource. Choose whether it runs on the site (Newt) or on a separate remote host. See <docsLink>the documentation</docsLink> for more.",
|
||||
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||
"internalResourceAuthDaemonStrategyPlaceholder": "Select Strategy",
|
||||
"internalResourceAuthDaemonStrategyLabel": "Location",
|
||||
"internalResourceAuthDaemonSite": "On Site",
|
||||
"internalResourceAuthDaemonSiteDescription": "Auth daemon runs on the site (Newt).",
|
||||
"internalResourceAuthDaemonRemote": "Remote Host",
|
||||
"internalResourceAuthDaemonRemoteDescription": "Auth daemon runs on this resource's destination - not the site.",
|
||||
"internalResourceAuthDaemonPort": "Daemon Port (optional)",
|
||||
"orgAuthWhatsThis": "Where can I find my organization ID?",
|
||||
"learnMore": "Learn more",
|
||||
"backToHome": "Go back to home",
|
||||
@@ -2691,7 +2378,7 @@
|
||||
"automaticModeDescription": " Show maintenance page only when all backend targets are down or unhealthy. Your resource continues working normally as long as at least one target is healthy.",
|
||||
"forced": "Forced",
|
||||
"forcedModeDescription": "Always show the maintenance page regardless of backend health. Use this for planned maintenance when you want to prevent all access.",
|
||||
"warning:": "Warning:",
|
||||
"warning:" : "Warning:",
|
||||
"forcedeModeWarning": "All traffic will be directed to the maintenance page. Your backend resources will not receive any requests.",
|
||||
"pageTitle": "Page Title",
|
||||
"pageTitleDescription": "The main heading displayed on the maintenance page",
|
||||
@@ -2707,191 +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",
|
||||
"deviceInformation": "Device Information",
|
||||
"deviceInformationDescription": "Information about the device and agent",
|
||||
"deviceSecurity": "Device Security",
|
||||
"deviceSecurityDescription": "Device security posture information",
|
||||
"platform": "Platform",
|
||||
"macosVersion": "macOS Version",
|
||||
"windowsVersion": "Windows Version",
|
||||
"iosVersion": "iOS Version",
|
||||
"androidVersion": "Android Version",
|
||||
"osVersion": "OS Version",
|
||||
"kernelVersion": "Kernel Version",
|
||||
"deviceModel": "Device Model",
|
||||
"serialNumber": "Serial Number",
|
||||
"hostname": "Hostname",
|
||||
"firstSeen": "First Seen",
|
||||
"lastSeen": "Last Seen",
|
||||
"biometricsEnabled": "Biometrics Enabled",
|
||||
"diskEncrypted": "Disk Encrypted",
|
||||
"firewallEnabled": "Firewall Enabled",
|
||||
"autoUpdatesEnabled": "Auto Updates Enabled",
|
||||
"tpmAvailable": "TPM Available",
|
||||
"windowsAntivirusEnabled": "Antivirus Enabled",
|
||||
"macosSipEnabled": "System Integrity Protection (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Firewall Stealth Mode",
|
||||
"linuxAppArmorEnabled": "AppArmor",
|
||||
"linuxSELinuxEnabled": "SELinux",
|
||||
"deviceSettingsDescription": "View device information and settings",
|
||||
"devicePendingApprovalDescription": "This device is waiting for approval",
|
||||
"deviceBlockedDescription": "This device is currently blocked. It won't be able to connect to any resources unless unblocked.",
|
||||
"unblockClient": "Unblock Client",
|
||||
"unblockClientDescription": "The device has been unblocked",
|
||||
"unarchiveClient": "Unarchive Client",
|
||||
"unarchiveClientDescription": "The device has been unarchived",
|
||||
"block": "Block",
|
||||
"unblock": "Unblock",
|
||||
"deviceActions": "Device Actions",
|
||||
"deviceActionsDescription": "Manage device status and access",
|
||||
"devicePendingApprovalBannerDescription": "This device is pending approval. It won't be able to connect to resources until approved.",
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected",
|
||||
"approvalsEmptyStateTitle": "Device Approvals Not Enabled",
|
||||
"approvalsEmptyStateDescription": "Enable device approvals for roles to require admin approval before users can connect new devices.",
|
||||
"approvalsEmptyStateStep1Title": "Go to Roles",
|
||||
"approvalsEmptyStateStep1Description": "Navigate to your organization's roles settings to configure device approvals.",
|
||||
"approvalsEmptyStateStep2Title": "Enable Device Approvals",
|
||||
"approvalsEmptyStateStep2Description": "Edit a role and enable the 'Require Device Approvals' option. Users with this role will need admin approval for new devices.",
|
||||
"approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review",
|
||||
"approvalsEmptyStateButtonText": "Manage Roles",
|
||||
"domainErrorTitle": "We are having trouble verifying your domain",
|
||||
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab.",
|
||||
"streamingTitle": "Event Streaming",
|
||||
"streamingDescription": "Stream events from your organization to external destinations in real time.",
|
||||
"streamingUnnamedDestination": "Unnamed destination",
|
||||
"streamingNoUrlConfigured": "No URL configured",
|
||||
"streamingAddDestination": "Add Destination",
|
||||
"streamingHttpWebhookTitle": "HTTP Webhook",
|
||||
"streamingHttpWebhookDescription": "Send events to any HTTP endpoint with flexible authentication and templating.",
|
||||
"streamingS3Title": "Amazon S3",
|
||||
"streamingS3Description": "Stream events to an S3-compatible object storage bucket. Coming soon.",
|
||||
"streamingDatadogTitle": "Datadog",
|
||||
"streamingDatadogDescription": "Forward events directly to your Datadog account. Coming soon.",
|
||||
"streamingTypePickerDescription": "Choose a destination type to get started.",
|
||||
"streamingFailedToLoad": "Failed to load destinations",
|
||||
"streamingUnexpectedError": "An unexpected error occurred.",
|
||||
"streamingFailedToUpdate": "Failed to update destination",
|
||||
"streamingDeletedSuccess": "Destination deleted successfully",
|
||||
"streamingFailedToDelete": "Failed to delete destination",
|
||||
"streamingDeleteTitle": "Delete Destination",
|
||||
"streamingDeleteButtonText": "Delete Destination",
|
||||
"streamingDeleteDialogAreYouSure": "Are you sure you want to delete",
|
||||
"streamingDeleteDialogThisDestination": "this destination",
|
||||
"streamingDeleteDialogPermanentlyRemoved": "? All configuration will be permanently removed.",
|
||||
"httpDestEditTitle": "Edit Destination",
|
||||
"httpDestAddTitle": "Add HTTP Destination",
|
||||
"httpDestEditDescription": "Update the configuration for this HTTP event streaming destination.",
|
||||
"httpDestAddDescription": "Configure a new HTTP endpoint to receive your organization's events.",
|
||||
"httpDestTabSettings": "Settings",
|
||||
"httpDestTabHeaders": "Headers",
|
||||
"httpDestTabBody": "Body",
|
||||
"httpDestTabLogs": "Logs",
|
||||
"httpDestNamePlaceholder": "My HTTP destination",
|
||||
"httpDestUrlLabel": "Destination URL",
|
||||
"httpDestUrlErrorHttpRequired": "URL must use http or https",
|
||||
"httpDestUrlErrorHttpsRequired": "HTTPS is required on cloud deployments",
|
||||
"httpDestUrlErrorInvalid": "Enter a valid URL (e.g. https://example.com/webhook)",
|
||||
"httpDestAuthTitle": "Authentication",
|
||||
"httpDestAuthDescription": "Choose how requests to your endpoint are authenticated.",
|
||||
"httpDestAuthNoneTitle": "No Authentication",
|
||||
"httpDestAuthNoneDescription": "Sends requests without an Authorization header.",
|
||||
"httpDestAuthBearerTitle": "Bearer Token",
|
||||
"httpDestAuthBearerDescription": "Adds an Authorization: Bearer '<token>' header to each request.",
|
||||
"httpDestAuthBearerPlaceholder": "Your API key or token",
|
||||
"httpDestAuthBasicTitle": "Basic Auth",
|
||||
"httpDestAuthBasicDescription": "Adds an Authorization: Basic '<credentials>' header. Provide credentials as username:password.",
|
||||
"httpDestAuthBasicPlaceholder": "username:password",
|
||||
"httpDestAuthCustomTitle": "Custom Header",
|
||||
"httpDestAuthCustomDescription": "Specify a custom HTTP header name and value for authentication (e.g. X-API-Key).",
|
||||
"httpDestAuthCustomHeaderNamePlaceholder": "Header name (e.g. X-API-Key)",
|
||||
"httpDestAuthCustomHeaderValuePlaceholder": "Header value",
|
||||
"httpDestCustomHeadersTitle": "Custom HTTP Headers",
|
||||
"httpDestCustomHeadersDescription": "Add custom headers to every outgoing request. Useful for static tokens or a custom Content-Type. By default, Content-Type: application/json is sent.",
|
||||
"httpDestNoHeadersConfigured": "No custom headers configured. Click \"Add Header\" to add one.",
|
||||
"httpDestHeaderNamePlaceholder": "Header name",
|
||||
"httpDestHeaderValuePlaceholder": "Value",
|
||||
"httpDestAddHeader": "Add Header",
|
||||
"httpDestBodyTemplateTitle": "Custom Body Template",
|
||||
"httpDestBodyTemplateDescription": "Control the JSON payload structure sent to your endpoint. If disabled, a default JSON object is sent for each event.",
|
||||
"httpDestEnableBodyTemplate": "Enable custom body template",
|
||||
"httpDestBodyTemplateLabel": "Body Template (JSON)",
|
||||
"httpDestBodyTemplateHint": "Use template variables to reference event fields in your payload.",
|
||||
"httpDestPayloadFormatTitle": "Payload Format",
|
||||
"httpDestPayloadFormatDescription": "How events are serialised into each request body.",
|
||||
"httpDestFormatJsonArrayTitle": "JSON Array",
|
||||
"httpDestFormatJsonArrayDescription": "One request per batch, body is a JSON array. Compatible with most generic webhooks and Datadog.",
|
||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||
"httpDestFormatNdjsonDescription": "One request per batch, body is newline-delimited JSON — one object per line, no outer array. Required by Splunk HEC, Elastic / OpenSearch, and Grafana Loki.",
|
||||
"httpDestFormatSingleTitle": "One Event Per Request",
|
||||
"httpDestFormatSingleDescription": "Sends a separate HTTP POST for each individual event. Use only for endpoints that cannot handle batches.",
|
||||
"httpDestLogTypesTitle": "Log Types",
|
||||
"httpDestLogTypesDescription": "Choose which log types are forwarded to this destination. Only enabled log types will be streamed.",
|
||||
"httpDestAccessLogsTitle": "Access Logs",
|
||||
"httpDestAccessLogsDescription": "Resource access attempts, including authenticated and denied requests.",
|
||||
"httpDestActionLogsTitle": "Action Logs",
|
||||
"httpDestActionLogsDescription": "Administrative actions performed by users within the organization.",
|
||||
"httpDestConnectionLogsTitle": "Connection Logs",
|
||||
"httpDestConnectionLogsDescription": "Site and tunnel connection events, including connects and disconnects.",
|
||||
"httpDestRequestLogsTitle": "Request Logs",
|
||||
"httpDestRequestLogsDescription": "HTTP request logs for proxied resources, including method, path, and response code.",
|
||||
"httpDestSaveChanges": "Save Changes",
|
||||
"httpDestCreateDestination": "Create Destination",
|
||||
"httpDestUpdatedSuccess": "Destination updated successfully",
|
||||
"httpDestCreatedSuccess": "Destination created successfully",
|
||||
"httpDestUpdateFailed": "Failed to update destination",
|
||||
"httpDestCreateFailed": "Failed to create destination"
|
||||
"createInternalResourceDialogDestinationRequired": "Destination is required"
|
||||
}
|
||||
|
||||
+150
-521
File diff suppressed because it is too large
Load Diff
+149
-520
File diff suppressed because it is too large
Load Diff
+148
-519
File diff suppressed because it is too large
Load Diff
+151
-522
File diff suppressed because it is too large
Load Diff
+150
-521
File diff suppressed because it is too large
Load Diff
+149
-520
File diff suppressed because it is too large
Load Diff
+150
-521
File diff suppressed because it is too large
Load Diff
+150
-521
File diff suppressed because it is too large
Load Diff
+150
-521
File diff suppressed because it is too large
Load Diff
+150
-521
File diff suppressed because it is too large
Load Diff
+150
-521
File diff suppressed because it is too large
Load Diff
+2095
-2393
File diff suppressed because it is too large
Load Diff
Generated
+6758
-2695
File diff suppressed because it is too large
Load Diff
+71
-70
@@ -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",
|
||||
@@ -12,29 +12,30 @@
|
||||
"license": "SEE LICENSE IN LICENSE AND README.md",
|
||||
"scripts": {
|
||||
"dev": "NODE_ENV=development ENVIRONMENT=dev tsx watch server/index.ts",
|
||||
"dev:check": "npx tsc --noEmit && npm run format:check",
|
||||
"dev:setup": "cp config/config.example.yml config/config.yml && npm run set:oss && npm run set:sqlite && npm run db:sqlite:generate && npm run db:sqlite:push",
|
||||
"db:generate": "drizzle-kit generate --config=./drizzle.config.ts",
|
||||
"db:push": "npx tsx server/db/migrate.ts",
|
||||
"db:studio": "drizzle-kit studio --config=./drizzle.config.ts",
|
||||
"db:pg:generate": "drizzle-kit generate --config=./drizzle.pg.config.ts",
|
||||
"db:sqlite:generate": "drizzle-kit generate --config=./drizzle.sqlite.config.ts",
|
||||
"db:pg:push": "npx tsx server/db/pg/migrate.ts",
|
||||
"db:sqlite:push": "npx tsx server/db/sqlite/migrate.ts",
|
||||
"db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts",
|
||||
"db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts",
|
||||
"db:clear-migrations": "rm -rf server/migrations",
|
||||
"set:oss": "echo 'export const build = \"oss\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.oss.json tsconfig.json",
|
||||
"set:saas": "echo 'export const build = \"saas\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.saas.json tsconfig.json",
|
||||
"set:enterprise": "echo 'export const build = \"enterprise\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json",
|
||||
"set:sqlite": "echo 'export * from \"./sqlite\";\nexport const driver: \"pg\" | \"sqlite\" = \"sqlite\";' > server/db/index.ts && cp drizzle.sqlite.config.ts drizzle.config.ts && cp server/setup/migrationsSqlite.ts server/setup/migrations.ts",
|
||||
"set:pg": "echo 'export * from \"./pg\";\nexport const driver: \"pg\" | \"sqlite\" = \"pg\";' > server/db/index.ts && cp drizzle.pg.config.ts drizzle.config.ts && cp server/setup/migrationsPg.ts server/setup/migrations.ts",
|
||||
"build:next": "next build",
|
||||
"build": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrations.ts -o dist/migrations.mjs",
|
||||
"set:sqlite": "echo 'export * from \"./sqlite\";\nexport const driver: \"pg\" | \"sqlite\" = \"sqlite\";' > server/db/index.ts",
|
||||
"set:pg": "echo 'export * from \"./pg\";\nexport const driver: \"pg\" | \"sqlite\" = \"pg\";' > server/db/index.ts",
|
||||
"next:build": "next build",
|
||||
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
|
||||
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
|
||||
"start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs",
|
||||
"email": "email dev --dir server/emails/templates --port 3005",
|
||||
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs",
|
||||
"format:check": "prettier --check .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@asteasolutions/zod-to-openapi": "8.4.1",
|
||||
"@aws-sdk/client-s3": "3.1011.0",
|
||||
"@faker-js/faker": "10.3.0",
|
||||
"@asteasolutions/zod-to-openapi": "8.2.0",
|
||||
"@aws-sdk/client-s3": "3.955.0",
|
||||
"@faker-js/faker": "10.1.0",
|
||||
"@headlessui/react": "2.2.9",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
@@ -59,83 +60,90 @@
|
||||
"@radix-ui/react-tabs": "1.1.13",
|
||||
"@radix-ui/react-toast": "1.2.15",
|
||||
"@radix-ui/react-tooltip": "1.2.8",
|
||||
"@react-email/components": "1.0.8",
|
||||
"@react-email/render": "2.0.4",
|
||||
"@react-email/tailwind": "2.0.5",
|
||||
"@simplewebauthn/browser": "13.3.0",
|
||||
"@simplewebauthn/server": "13.3.0",
|
||||
"@react-email/components": "1.0.2",
|
||||
"@react-email/render": "2.0.0",
|
||||
"@react-email/tailwind": "2.0.2",
|
||||
"@simplewebauthn/browser": "13.2.2",
|
||||
"@simplewebauthn/server": "13.2.2",
|
||||
"@tailwindcss/forms": "0.5.11",
|
||||
"@tanstack/react-query": "5.90.21",
|
||||
"@tanstack/react-query": "5.90.12",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"arctic": "3.7.0",
|
||||
"axios": "1.13.5",
|
||||
"axios": "1.13.2",
|
||||
"better-sqlite3": "11.9.1",
|
||||
"canvas-confetti": "1.9.4",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
"cookie": "1.1.1",
|
||||
"cookie-parser": "1.4.7",
|
||||
"cors": "2.8.6",
|
||||
"cookies": "0.9.1",
|
||||
"cors": "2.8.5",
|
||||
"crypto-js": "4.2.0",
|
||||
"d3": "7.9.0",
|
||||
"date-fns": "4.1.0",
|
||||
"drizzle-orm": "0.45.1",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-next": "16.1.0",
|
||||
"express": "5.2.1",
|
||||
"express-rate-limit": "8.3.0",
|
||||
"glob": "13.0.6",
|
||||
"express-rate-limit": "8.2.1",
|
||||
"glob": "13.0.0",
|
||||
"helmet": "8.1.0",
|
||||
"http-errors": "2.0.1",
|
||||
"i": "0.3.7",
|
||||
"input-otp": "1.4.2",
|
||||
"ioredis": "5.10.0",
|
||||
"ioredis": "5.8.2",
|
||||
"jmespath": "0.16.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"jsonwebtoken": "9.0.3",
|
||||
"lucide-react": "0.577.0",
|
||||
"maxmind": "5.0.5",
|
||||
"lucide-react": "0.562.0",
|
||||
"maxmind": "5.0.1",
|
||||
"moment": "2.30.1",
|
||||
"next": "15.5.14",
|
||||
"next-intl": "4.8.3",
|
||||
"next": "15.5.9",
|
||||
"next-intl": "4.6.1",
|
||||
"next-themes": "0.4.6",
|
||||
"nextjs-toploader": "3.9.17",
|
||||
"node-cache": "5.1.2",
|
||||
"nodemailer": "8.0.4",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "7.0.11",
|
||||
"npm": "11.7.0",
|
||||
"nprogress": "0.2.0",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "8.20.0",
|
||||
"posthog-node": "5.28.0",
|
||||
"pg": "8.16.3",
|
||||
"posthog-node": "5.17.4",
|
||||
"qrcode.react": "4.2.0",
|
||||
"react": "19.2.4",
|
||||
"react-day-picker": "9.14.0",
|
||||
"react-dom": "19.2.4",
|
||||
"react": "19.2.3",
|
||||
"react-day-picker": "9.13.0",
|
||||
"react-dom": "19.2.3",
|
||||
"react-easy-sort": "1.8.0",
|
||||
"react-hook-form": "7.71.2",
|
||||
"react-icons": "5.6.0",
|
||||
"react-hook-form": "7.68.0",
|
||||
"react-icons": "5.5.0",
|
||||
"rebuild": "0.1.2",
|
||||
"recharts": "2.15.4",
|
||||
"reodotdev": "1.1.0",
|
||||
"resend": "6.9.2",
|
||||
"semver": "7.7.4",
|
||||
"sshpk": "1.18.0",
|
||||
"stripe": "20.4.1",
|
||||
"reodotdev": "1.0.0",
|
||||
"resend": "6.6.0",
|
||||
"semver": "7.7.3",
|
||||
"stripe": "20.1.0",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"tailwind-merge": "3.5.0",
|
||||
"tailwind-merge": "3.4.0",
|
||||
"topojson-client": "3.1.0",
|
||||
"tw-animate-css": "1.4.0",
|
||||
"use-debounce": "10.1.0",
|
||||
"uuid": "13.0.0",
|
||||
"vaul": "1.1.2",
|
||||
"visionscarto-world-atlas": "1.0.0",
|
||||
"winston": "3.19.0",
|
||||
"winston-daily-rotate-file": "5.0.0",
|
||||
"ws": "8.19.0",
|
||||
"yaml": "2.8.3",
|
||||
"ws": "8.18.3",
|
||||
"yaml": "2.8.2",
|
||||
"yargs": "18.0.0",
|
||||
"zod": "4.3.6",
|
||||
"zod": "4.2.1",
|
||||
"zod-validation-error": "5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dotenvx/dotenvx": "1.54.1",
|
||||
"@dotenvx/dotenvx": "1.51.2",
|
||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||
"@react-email/preview-server": "5.2.10",
|
||||
"@tailwindcss/postcss": "4.2.2",
|
||||
"@tanstack/react-query-devtools": "5.91.3",
|
||||
"@tailwindcss/postcss": "4.1.18",
|
||||
"@tanstack/react-query-devtools": "5.91.1",
|
||||
"@types/better-sqlite3": "7.6.13",
|
||||
"@types/cookie-parser": "1.4.10",
|
||||
"@types/cors": "2.8.19",
|
||||
@@ -144,37 +152,30 @@
|
||||
"@types/express": "5.0.6",
|
||||
"@types/express-session": "1.18.2",
|
||||
"@types/jmespath": "0.15.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsonwebtoken": "9.0.10",
|
||||
"@types/node": "25.3.5",
|
||||
"@types/nodemailer": "7.0.11",
|
||||
"@types/node": "24.10.2",
|
||||
"@types/nodemailer": "7.0.4",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@types/pg": "8.18.0",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/pg": "8.16.0",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/sshpk": "1.17.4",
|
||||
"@types/swagger-ui-express": "4.1.8",
|
||||
"@types/topojson-client": "3.1.5",
|
||||
"@types/ws": "8.18.1",
|
||||
"@types/yargs": "17.0.35",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"drizzle-kit": "0.31.10",
|
||||
"esbuild": "0.27.4",
|
||||
"drizzle-kit": "0.31.8",
|
||||
"esbuild": "0.27.2",
|
||||
"esbuild-node-externals": "1.20.1",
|
||||
"eslint": "10.0.3",
|
||||
"eslint-config-next": "16.1.7",
|
||||
"postcss": "8.5.8",
|
||||
"prettier": "3.8.1",
|
||||
"react-email": "5.2.10",
|
||||
"tailwindcss": "4.2.2",
|
||||
"postcss": "8.5.6",
|
||||
"prettier": "3.7.4",
|
||||
"react-email": "5.0.7",
|
||||
"tailwindcss": "4.1.18",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsx": "4.21.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.56.1"
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "0.27.4",
|
||||
"dompurify": "3.3.2"
|
||||
"typescript-eslint": "8.49.0"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 73 KiB |
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
+23
-29
@@ -1,10 +1,9 @@
|
||||
import { Request } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { userActions, roleActions } from "@server/db";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import { userActions, roleActions, userOrgs } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||
|
||||
export enum ActionsEnum {
|
||||
createOrgUser = "createOrgUser",
|
||||
@@ -20,7 +19,6 @@ export enum ActionsEnum {
|
||||
getSite = "getSite",
|
||||
listSites = "listSites",
|
||||
updateSite = "updateSite",
|
||||
resetSiteBandwidth = "resetSiteBandwidth",
|
||||
reGenerateSecret = "reGenerateSecret",
|
||||
createResource = "createResource",
|
||||
deleteResource = "deleteResource",
|
||||
@@ -54,8 +52,6 @@ export enum ActionsEnum {
|
||||
listRoleResources = "listRoleResources",
|
||||
// listRoleActions = "listRoleActions",
|
||||
addUserRole = "addUserRole",
|
||||
removeUserRole = "removeUserRole",
|
||||
setUserOrgRoles = "setUserOrgRoles",
|
||||
// addUserSite = "addUserSite",
|
||||
// addUserAction = "addUserAction",
|
||||
// removeUserAction = "removeUserAction",
|
||||
@@ -82,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",
|
||||
@@ -112,10 +104,6 @@ export enum ActionsEnum {
|
||||
listApiKeyActions = "listApiKeyActions",
|
||||
listApiKeys = "listApiKeys",
|
||||
getApiKey = "getApiKey",
|
||||
createSiteProvisioningKey = "createSiteProvisioningKey",
|
||||
listSiteProvisioningKeys = "listSiteProvisioningKeys",
|
||||
updateSiteProvisioningKey = "updateSiteProvisioningKey",
|
||||
deleteSiteProvisioningKey = "deleteSiteProvisioningKey",
|
||||
getCertificate = "getCertificate",
|
||||
restartCertificate = "restartCertificate",
|
||||
billing = "billing",
|
||||
@@ -137,14 +125,7 @@ export enum ActionsEnum {
|
||||
getBlueprint = "getBlueprint",
|
||||
applyBlueprint = "applyBlueprint",
|
||||
viewLogs = "viewLogs",
|
||||
exportLogs = "exportLogs",
|
||||
listApprovals = "listApprovals",
|
||||
updateApprovals = "updateApprovals",
|
||||
signSshKey = "signSshKey",
|
||||
createEventStreamingDestination = "createEventStreamingDestination",
|
||||
updateEventStreamingDestination = "updateEventStreamingDestination",
|
||||
deleteEventStreamingDestination = "deleteEventStreamingDestination",
|
||||
listEventStreamingDestinations = "listEventStreamingDestinations"
|
||||
exportLogs = "exportLogs"
|
||||
}
|
||||
|
||||
export async function checkUserActionPermission(
|
||||
@@ -165,16 +146,29 @@ export async function checkUserActionPermission(
|
||||
}
|
||||
|
||||
try {
|
||||
let userOrgRoleIds = req.userOrgRoleIds;
|
||||
let userOrgRoleId = req.userOrgRoleId;
|
||||
|
||||
if (userOrgRoleIds === undefined) {
|
||||
userOrgRoleIds = await getUserOrgRoleIds(userId, req.userOrgId!);
|
||||
if (userOrgRoleIds.length === 0) {
|
||||
// If userOrgRoleId is not available on the request, fetch it
|
||||
if (userOrgRoleId === undefined) {
|
||||
const userOrgRole = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId),
|
||||
eq(userOrgs.orgId, req.userOrgId!)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (userOrgRole.length === 0) {
|
||||
throw createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have access to this organization"
|
||||
);
|
||||
}
|
||||
|
||||
userOrgRoleId = userOrgRole[0].roleId;
|
||||
}
|
||||
|
||||
// Check if the user has direct permission for the action in the current org
|
||||
@@ -185,7 +179,7 @@ export async function checkUserActionPermission(
|
||||
and(
|
||||
eq(userActions.userId, userId),
|
||||
eq(userActions.actionId, actionId),
|
||||
eq(userActions.orgId, req.userOrgId!)
|
||||
eq(userActions.orgId, req.userOrgId!) // TODO: we cant pass the org id if we are not checking the org
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
@@ -194,14 +188,14 @@ export async function checkUserActionPermission(
|
||||
return true;
|
||||
}
|
||||
|
||||
// If no direct permission, check role-based permission (any of user's roles)
|
||||
// If no direct permission, check role-based permission
|
||||
const roleActionPermission = await db
|
||||
.select()
|
||||
.from(roleActions)
|
||||
.where(
|
||||
and(
|
||||
eq(roleActions.actionId, actionId),
|
||||
inArray(roleActions.roleId, userOrgRoleIds),
|
||||
eq(roleActions.roleId, userOrgRoleId!),
|
||||
eq(roleActions.orgId, req.userOrgId!)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
import { db } from "@server/db";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { roleResources, userResources } from "@server/db";
|
||||
|
||||
export async function canUserAccessResource({
|
||||
userId,
|
||||
resourceId,
|
||||
roleIds
|
||||
roleId
|
||||
}: {
|
||||
userId: string;
|
||||
resourceId: number;
|
||||
roleIds: number[];
|
||||
roleId: number;
|
||||
}): Promise<boolean> {
|
||||
const roleResourceAccess =
|
||||
roleIds.length > 0
|
||||
? await db
|
||||
.select()
|
||||
.from(roleResources)
|
||||
.where(
|
||||
and(
|
||||
eq(roleResources.resourceId, resourceId),
|
||||
inArray(roleResources.roleId, roleIds)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
: [];
|
||||
const roleResourceAccess = await db
|
||||
.select()
|
||||
.from(roleResources)
|
||||
.where(
|
||||
and(
|
||||
eq(roleResources.resourceId, resourceId),
|
||||
eq(roleResources.roleId, roleId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (roleResourceAccess.length > 0) {
|
||||
return true;
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { db } from "@server/db";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import { roleSiteResources, userSiteResources } from "@server/db";
|
||||
|
||||
export async function canUserAccessSiteResource({
|
||||
userId,
|
||||
resourceId,
|
||||
roleIds
|
||||
}: {
|
||||
userId: string;
|
||||
resourceId: number;
|
||||
roleIds: number[];
|
||||
}): Promise<boolean> {
|
||||
const roleResourceAccess =
|
||||
roleIds.length > 0
|
||||
? await db
|
||||
.select()
|
||||
.from(roleSiteResources)
|
||||
.where(
|
||||
and(
|
||||
eq(roleSiteResources.siteResourceId, resourceId),
|
||||
inArray(roleSiteResources.roleId, roleIds)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
: [];
|
||||
|
||||
if (roleResourceAccess.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const userResourceAccess = await db
|
||||
.select()
|
||||
.from(userSiteResources)
|
||||
.where(
|
||||
and(
|
||||
eq(userSiteResources.userId, userId),
|
||||
eq(userSiteResources.siteResourceId, resourceId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (userResourceAccess.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -3,14 +3,7 @@ import {
|
||||
encodeHexLowerCase
|
||||
} from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import {
|
||||
resourceSessions,
|
||||
safeRead,
|
||||
Session,
|
||||
sessions,
|
||||
User,
|
||||
users
|
||||
} from "@server/db";
|
||||
import { resourceSessions, Session, sessions, User, users } from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
@@ -61,15 +54,11 @@ export async function validateSessionToken(
|
||||
const sessionId = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(token))
|
||||
);
|
||||
|
||||
const result = await safeRead((db) =>
|
||||
db
|
||||
.select({ user: users, session: sessions })
|
||||
.from(sessions)
|
||||
.innerJoin(users, eq(sessions.userId, users.userId))
|
||||
.where(eq(sessions.sessionId, sessionId))
|
||||
);
|
||||
|
||||
const result = await db
|
||||
.select({ user: users, session: sessions })
|
||||
.from(sessions)
|
||||
.innerJoin(users, eq(sessions.userId, users.userId))
|
||||
.where(eq(sessions.sessionId, sessionId));
|
||||
if (result.length < 1) {
|
||||
return { session: null, user: null };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { resourceSessions, ResourceSession } from "@server/db";
|
||||
import { db, safeRead } from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
@@ -66,17 +66,15 @@ export async function validateResourceSessionToken(
|
||||
const sessionId = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(token))
|
||||
);
|
||||
const result = await safeRead((db) =>
|
||||
db
|
||||
.select()
|
||||
.from(resourceSessions)
|
||||
.where(
|
||||
and(
|
||||
eq(resourceSessions.sessionId, sessionId),
|
||||
eq(resourceSessions.resourceId, resourceId)
|
||||
)
|
||||
const result = await db
|
||||
.select()
|
||||
.from(resourceSessions)
|
||||
.where(
|
||||
and(
|
||||
eq(resourceSessions.sessionId, sessionId),
|
||||
eq(resourceSessions.resourceId, resourceId)
|
||||
)
|
||||
);
|
||||
);
|
||||
|
||||
if (result.length < 1) {
|
||||
return { resourceSession: null };
|
||||
@@ -87,7 +85,7 @@ export async function validateResourceSessionToken(
|
||||
if (Date.now() >= resourceSession.expiresAt) {
|
||||
await db
|
||||
.delete(resourceSessions)
|
||||
.where(eq(resourceSessions.sessionId, sessionId));
|
||||
.where(eq(resourceSessions.sessionId, resourceSessions.sessionId));
|
||||
return { resourceSession: null };
|
||||
} else if (
|
||||
Date.now() >=
|
||||
@@ -181,7 +179,7 @@ export function serializeResourceSessionCookie(
|
||||
return `${cookieName}_s.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${domain}`;
|
||||
} else {
|
||||
if (expiresAt === undefined) {
|
||||
return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Domain=${domain}`;
|
||||
return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Domain=$domain}`;
|
||||
}
|
||||
return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Domain=${domain}`;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { flushBandwidthToDb } from "@server/routers/newt/handleReceiveBandwidthMessage";
|
||||
import { flushConnectionLogToDb } from "#dynamic/routers/newt";
|
||||
import { flushSiteBandwidthToDb } from "@server/routers/gerbil/receiveBandwidth";
|
||||
import { stopPingAccumulator } from "@server/routers/newt/pingAccumulator";
|
||||
import { cleanup as wsCleanup } from "#dynamic/routers/ws";
|
||||
|
||||
async function cleanup() {
|
||||
await stopPingAccumulator();
|
||||
await flushBandwidthToDb();
|
||||
await flushConnectionLogToDb();
|
||||
await flushSiteBandwidthToDb();
|
||||
await wsCleanup();
|
||||
|
||||
process.exit(0);
|
||||
|
||||
+4
-4
@@ -56,15 +56,15 @@ Ensure drizzle-kit is installed.
|
||||
You must have a connection string in your config file, as shown above.
|
||||
|
||||
```bash
|
||||
npm run db:generate
|
||||
npm run db:push
|
||||
npm run db:pg:generate
|
||||
npm run db:pg:push
|
||||
```
|
||||
|
||||
### SQLite
|
||||
|
||||
```bash
|
||||
npm run db:generate
|
||||
npm run db:push
|
||||
npm run db:sqlite:generate
|
||||
npm run db:sqlite:push
|
||||
```
|
||||
|
||||
## Build Time
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import { runMigrations } from "./";
|
||||
|
||||
await runMigrations();
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
+13
-20
@@ -1,7 +1,7 @@
|
||||
import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres";
|
||||
import { Pool } from "pg";
|
||||
import { readConfigFile } from "@server/lib/readConfigFile";
|
||||
import { withReplicas } from "drizzle-orm/pg-core";
|
||||
import { createPool } from "./poolConfig";
|
||||
|
||||
function createDb() {
|
||||
const config = readConfigFile();
|
||||
@@ -39,17 +39,12 @@ function createDb() {
|
||||
|
||||
// Create connection pools instead of individual connections
|
||||
const poolConfig = config.postgres.pool;
|
||||
const maxConnections = poolConfig?.max_connections || 20;
|
||||
const idleTimeoutMs = poolConfig?.idle_timeout_ms || 30000;
|
||||
const connectionTimeoutMs = poolConfig?.connection_timeout_ms || 5000;
|
||||
|
||||
const primaryPool = createPool(
|
||||
const primaryPool = new Pool({
|
||||
connectionString,
|
||||
maxConnections,
|
||||
idleTimeoutMs,
|
||||
connectionTimeoutMs,
|
||||
"primary"
|
||||
);
|
||||
max: poolConfig?.max_connections || 20,
|
||||
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
||||
connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000
|
||||
});
|
||||
|
||||
const replicas = [];
|
||||
|
||||
@@ -60,15 +55,14 @@ function createDb() {
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const maxReplicaConnections = poolConfig?.max_replica_connections || 20;
|
||||
for (const conn of replicaConnections) {
|
||||
const replicaPool = createPool(
|
||||
conn.connection_string,
|
||||
maxReplicaConnections,
|
||||
idleTimeoutMs,
|
||||
connectionTimeoutMs,
|
||||
"replica"
|
||||
);
|
||||
const replicaPool = new Pool({
|
||||
connectionString: conn.connection_string,
|
||||
max: poolConfig?.max_replica_connections || 20,
|
||||
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
||||
connectionTimeoutMillis:
|
||||
poolConfig?.connection_timeout_ms || 5000
|
||||
});
|
||||
replicas.push(
|
||||
DrizzlePostgres(replicaPool, {
|
||||
logger: process.env.QUERY_LOGGING == "true"
|
||||
@@ -91,4 +85,3 @@ export const primaryDb = db.$primary;
|
||||
export type Transaction = Parameters<
|
||||
Parameters<(typeof db)["transaction"]>[0]
|
||||
>[0];
|
||||
export const DB_TYPE: "pg" | "sqlite" = "pg";
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
export * from "./driver";
|
||||
export * from "./logsDriver";
|
||||
export * from "./safeRead";
|
||||
export * from "./schema/schema";
|
||||
export * from "./schema/privateSchema";
|
||||
export * from "./migrate";
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres";
|
||||
import { readConfigFile } from "@server/lib/readConfigFile";
|
||||
import { withReplicas } from "drizzle-orm/pg-core";
|
||||
import { build } from "@server/build";
|
||||
import { db as mainDb, primaryDb as mainPrimaryDb } from "./driver";
|
||||
import { createPool } from "./poolConfig";
|
||||
|
||||
function createLogsDb() {
|
||||
// Only use separate logs database in SaaS builds
|
||||
if (build !== "saas") {
|
||||
return mainDb;
|
||||
}
|
||||
|
||||
const config = readConfigFile();
|
||||
|
||||
// Merge configs, prioritizing private config
|
||||
const logsConfig = config.postgres_logs;
|
||||
|
||||
// Check environment variable first
|
||||
let connectionString = process.env.POSTGRES_LOGS_CONNECTION_STRING;
|
||||
let replicaConnections: Array<{ connection_string: string }> = [];
|
||||
|
||||
if (!connectionString && logsConfig) {
|
||||
connectionString = logsConfig.connection_string;
|
||||
replicaConnections = logsConfig.replicas || [];
|
||||
}
|
||||
|
||||
// If POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS is set, use it
|
||||
if (process.env.POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS) {
|
||||
replicaConnections =
|
||||
process.env.POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS.split(",").map(
|
||||
(conn) => ({
|
||||
connection_string: conn.trim()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// If no logs database is configured, fall back to main database
|
||||
if (!connectionString) {
|
||||
return mainDb;
|
||||
}
|
||||
|
||||
// Create separate connection pool for logs database
|
||||
const poolConfig = logsConfig?.pool || config.postgres?.pool;
|
||||
const maxConnections = poolConfig?.max_connections || 20;
|
||||
const idleTimeoutMs = poolConfig?.idle_timeout_ms || 30000;
|
||||
const connectionTimeoutMs = poolConfig?.connection_timeout_ms || 5000;
|
||||
|
||||
const primaryPool = createPool(
|
||||
connectionString,
|
||||
maxConnections,
|
||||
idleTimeoutMs,
|
||||
connectionTimeoutMs,
|
||||
"logs-primary"
|
||||
);
|
||||
|
||||
const replicas = [];
|
||||
|
||||
if (!replicaConnections.length) {
|
||||
replicas.push(
|
||||
DrizzlePostgres(primaryPool, {
|
||||
logger: process.env.QUERY_LOGGING == "true"
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const maxReplicaConnections =
|
||||
poolConfig?.max_replica_connections || 20;
|
||||
for (const conn of replicaConnections) {
|
||||
const replicaPool = createPool(
|
||||
conn.connection_string,
|
||||
maxReplicaConnections,
|
||||
idleTimeoutMs,
|
||||
connectionTimeoutMs,
|
||||
"logs-replica"
|
||||
);
|
||||
replicas.push(
|
||||
DrizzlePostgres(replicaPool, {
|
||||
logger: process.env.QUERY_LOGGING == "true"
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return withReplicas(
|
||||
DrizzlePostgres(primaryPool, {
|
||||
logger: process.env.QUERY_LOGGING == "true"
|
||||
}),
|
||||
replicas as any
|
||||
);
|
||||
}
|
||||
|
||||
export const logsDb = createLogsDb();
|
||||
export default logsDb;
|
||||
export const primaryLogsDb = logsDb.$primary;
|
||||
@@ -4,7 +4,7 @@ import path from "path";
|
||||
|
||||
const migrationsFolder = path.join("server/migrations");
|
||||
|
||||
export const runMigrations = async () => {
|
||||
const runMigrations = async () => {
|
||||
console.log("Running migrations...");
|
||||
try {
|
||||
await migrate(db as any, {
|
||||
@@ -17,3 +17,5 @@ export const runMigrations = async () => {
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
runMigrations();
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Pool, PoolConfig } from "pg";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export function createPoolConfig(
|
||||
connectionString: string,
|
||||
maxConnections: number,
|
||||
idleTimeoutMs: number,
|
||||
connectionTimeoutMs: number
|
||||
): PoolConfig {
|
||||
return {
|
||||
connectionString,
|
||||
max: maxConnections,
|
||||
idleTimeoutMillis: idleTimeoutMs,
|
||||
connectionTimeoutMillis: connectionTimeoutMs,
|
||||
// TCP keepalive to prevent silent connection drops by NAT gateways,
|
||||
// load balancers, and other intermediate network devices (e.g. AWS
|
||||
// NAT Gateway drops idle TCP connections after ~350s)
|
||||
keepAlive: true,
|
||||
keepAliveInitialDelayMillis: 10000, // send first keepalive after 10s of idle
|
||||
// Allow connections to be released and recreated more aggressively
|
||||
// to avoid stale connections building up
|
||||
allowExitOnIdle: false
|
||||
};
|
||||
}
|
||||
|
||||
export function attachPoolErrorHandlers(pool: Pool, label: string): void {
|
||||
pool.on("error", (err) => {
|
||||
// This catches errors on idle clients in the pool. Without this
|
||||
// handler an unexpected disconnect would crash the process.
|
||||
logger.error(
|
||||
`Unexpected error on idle ${label} database client: ${err.message}`
|
||||
);
|
||||
});
|
||||
|
||||
pool.on("connect", (client) => {
|
||||
// Set a statement timeout on every new connection so a single slow
|
||||
// query can't block the pool forever
|
||||
client.query("SET statement_timeout = '30s'").catch((err: Error) => {
|
||||
logger.warn(
|
||||
`Failed to set statement_timeout on ${label} client: ${err.message}`
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function createPool(
|
||||
connectionString: string,
|
||||
maxConnections: number,
|
||||
idleTimeoutMs: number,
|
||||
connectionTimeoutMs: number,
|
||||
label: string
|
||||
): Pool {
|
||||
const pool = new Pool(
|
||||
createPoolConfig(
|
||||
connectionString,
|
||||
maxConnections,
|
||||
idleTimeoutMs,
|
||||
connectionTimeoutMs
|
||||
)
|
||||
);
|
||||
attachPoolErrorHandlers(pool, label);
|
||||
return pool;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { db, primaryDb } from "./driver";
|
||||
|
||||
/**
|
||||
* Runs a read query with replica fallback for Postgres.
|
||||
* Executes the query against the replica first (when replicas exist).
|
||||
* If the query throws or returns no data (null, undefined, or empty array),
|
||||
* runs the same query against the primary.
|
||||
*/
|
||||
export async function safeRead<T>(
|
||||
query: (d: typeof db | typeof primaryDb) => Promise<T>
|
||||
): Promise<T> {
|
||||
try {
|
||||
const result = await query(db);
|
||||
if (result === undefined || result === null) {
|
||||
return query(primaryDb);
|
||||
}
|
||||
if (Array.isArray(result) && result.length === 0) {
|
||||
return query(primaryDb);
|
||||
}
|
||||
return result;
|
||||
} catch {
|
||||
return query(primaryDb);
|
||||
}
|
||||
}
|
||||
@@ -7,22 +7,10 @@ import {
|
||||
bigint,
|
||||
real,
|
||||
text,
|
||||
index,
|
||||
primaryKey,
|
||||
uniqueIndex
|
||||
index
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { InferSelectModel } from "drizzle-orm";
|
||||
import {
|
||||
domains,
|
||||
orgs,
|
||||
targets,
|
||||
users,
|
||||
exitNodes,
|
||||
sessions,
|
||||
clients,
|
||||
siteResources,
|
||||
sites
|
||||
} from "./schema";
|
||||
import { domains, orgs, targets, users, exitNodes, sessions } from "./schema";
|
||||
|
||||
export const certificates = pgTable("certificates", {
|
||||
certId: serial("certId").primaryKey(),
|
||||
@@ -86,16 +74,11 @@ export const subscriptions = pgTable("subscriptions", {
|
||||
canceledAt: bigint("canceledAt", { mode: "number" }),
|
||||
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||
updatedAt: bigint("updatedAt", { mode: "number" }),
|
||||
version: integer("version"),
|
||||
billingCycleAnchor: bigint("billingCycleAnchor", { mode: "number" }),
|
||||
type: varchar("type", { length: 50 }) // tier1, tier2, tier3, or license
|
||||
billingCycleAnchor: bigint("billingCycleAnchor", { mode: "number" })
|
||||
});
|
||||
|
||||
export const subscriptionItems = pgTable("subscriptionItems", {
|
||||
subscriptionItemId: serial("subscriptionItemId").primaryKey(),
|
||||
stripeSubscriptionItemId: varchar("stripeSubscriptionItemId", {
|
||||
length: 255
|
||||
}),
|
||||
subscriptionId: varchar("subscriptionId", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => subscriptions.subscriptionId, {
|
||||
@@ -103,7 +86,6 @@ export const subscriptionItems = pgTable("subscriptionItems", {
|
||||
}),
|
||||
planId: varchar("planId", { length: 255 }).notNull(),
|
||||
priceId: varchar("priceId", { length: 255 }),
|
||||
featureId: varchar("featureId", { length: 255 }),
|
||||
meterId: varchar("meterId", { length: 255 }),
|
||||
unitAmount: real("unitAmount"),
|
||||
tiers: text("tiers"),
|
||||
@@ -146,7 +128,6 @@ export const limits = pgTable("limits", {
|
||||
})
|
||||
.notNull(),
|
||||
value: real("value"),
|
||||
override: boolean("override").default(false),
|
||||
description: text("description")
|
||||
});
|
||||
|
||||
@@ -225,7 +206,7 @@ export const loginPageOrg = pgTable("loginPageOrg", {
|
||||
|
||||
export const loginPageBranding = pgTable("loginPageBranding", {
|
||||
loginPageBrandingId: serial("loginPageBrandingId").primaryKey(),
|
||||
logoUrl: text("logoUrl"),
|
||||
logoUrl: text("logoUrl").notNull(),
|
||||
logoWidth: integer("logoWidth").notNull(),
|
||||
logoHeight: integer("logoHeight").notNull(),
|
||||
primaryColor: text("primaryColor"),
|
||||
@@ -292,7 +273,6 @@ export const accessAuditLog = pgTable(
|
||||
actor: varchar("actor", { length: 255 }),
|
||||
actorId: varchar("actorId", { length: 255 }),
|
||||
resourceId: integer("resourceId"),
|
||||
siteResourceId: integer("siteResourceId"),
|
||||
ip: varchar("ip", { length: 45 }),
|
||||
type: varchar("type", { length: 100 }).notNull(),
|
||||
action: boolean("action").notNull(),
|
||||
@@ -309,156 +289,6 @@ export const accessAuditLog = pgTable(
|
||||
]
|
||||
);
|
||||
|
||||
export const connectionAuditLog = pgTable(
|
||||
"connectionAuditLog",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
sessionId: text("sessionId").notNull(),
|
||||
siteResourceId: integer("siteResourceId").references(
|
||||
() => siteResources.siteResourceId,
|
||||
{ onDelete: "cascade" }
|
||||
),
|
||||
orgId: text("orgId").references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
siteId: integer("siteId").references(() => sites.siteId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
clientId: integer("clientId").references(() => clients.clientId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
userId: text("userId").references(() => users.userId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
sourceAddr: text("sourceAddr").notNull(),
|
||||
destAddr: text("destAddr").notNull(),
|
||||
protocol: text("protocol").notNull(),
|
||||
startedAt: integer("startedAt").notNull(),
|
||||
endedAt: integer("endedAt"),
|
||||
bytesTx: integer("bytesTx"),
|
||||
bytesRx: integer("bytesRx")
|
||||
},
|
||||
(table) => [
|
||||
index("idx_accessAuditLog_startedAt").on(table.startedAt),
|
||||
index("idx_accessAuditLog_org_startedAt").on(
|
||||
table.orgId,
|
||||
table.startedAt
|
||||
),
|
||||
index("idx_accessAuditLog_siteResourceId").on(table.siteResourceId)
|
||||
]
|
||||
);
|
||||
|
||||
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 const bannedEmails = pgTable("bannedEmails", {
|
||||
email: varchar("email", { length: 255 }).primaryKey()
|
||||
});
|
||||
|
||||
export const bannedIps = pgTable("bannedIps", {
|
||||
ip: varchar("ip", { length: 255 }).primaryKey()
|
||||
});
|
||||
|
||||
export const siteProvisioningKeys = pgTable("siteProvisioningKeys", {
|
||||
siteProvisioningKeyId: varchar("siteProvisioningKeyId", {
|
||||
length: 255
|
||||
}).primaryKey(),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
siteProvisioningKeyHash: text("siteProvisioningKeyHash").notNull(),
|
||||
lastChars: varchar("lastChars", { length: 4 }).notNull(),
|
||||
createdAt: varchar("dateCreated", { length: 255 }).notNull(),
|
||||
lastUsed: varchar("lastUsed", { length: 255 }),
|
||||
maxBatchSize: integer("maxBatchSize"), // null = no limit
|
||||
numUsed: integer("numUsed").notNull().default(0),
|
||||
validUntil: varchar("validUntil", { length: 255 }),
|
||||
approveNewSites: boolean("approveNewSites").notNull().default(true)
|
||||
});
|
||||
|
||||
export const siteProvisioningKeyOrg = pgTable(
|
||||
"siteProvisioningKeyOrg",
|
||||
{
|
||||
siteProvisioningKeyId: varchar("siteProvisioningKeyId", {
|
||||
length: 255
|
||||
})
|
||||
.notNull()
|
||||
.references(() => siteProvisioningKeys.siteProvisioningKeyId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
orgId: varchar("orgId", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||
},
|
||||
(table) => [
|
||||
primaryKey({
|
||||
columns: [table.siteProvisioningKeyId, table.orgId]
|
||||
})
|
||||
]
|
||||
);
|
||||
|
||||
export const eventStreamingDestinations = pgTable(
|
||||
"eventStreamingDestinations",
|
||||
{
|
||||
destinationId: serial("destinationId").primaryKey(),
|
||||
orgId: varchar("orgId", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
sendConnectionLogs: boolean("sendConnectionLogs").notNull().default(false),
|
||||
sendRequestLogs: boolean("sendRequestLogs").notNull().default(false),
|
||||
sendActionLogs: boolean("sendActionLogs").notNull().default(false),
|
||||
sendAccessLogs: boolean("sendAccessLogs").notNull().default(false),
|
||||
type: varchar("type", { length: 50 }).notNull(), // e.g. "http", "kafka", etc.
|
||||
config: text("config").notNull(), // JSON string with the configuration for the destination
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
|
||||
}
|
||||
);
|
||||
|
||||
export const eventStreamingCursors = pgTable(
|
||||
"eventStreamingCursors",
|
||||
{
|
||||
cursorId: serial("cursorId").primaryKey(),
|
||||
destinationId: integer("destinationId")
|
||||
.notNull()
|
||||
.references(() => eventStreamingDestinations.destinationId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
logType: varchar("logType", { length: 50 }).notNull(), // "request" | "action" | "access" | "connection"
|
||||
lastSentId: bigint("lastSentId", { mode: "number" }).notNull().default(0),
|
||||
lastSentAt: bigint("lastSentAt", { mode: "number" }) // epoch milliseconds, null if never sent
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("idx_eventStreamingCursors_dest_type").on(
|
||||
table.destinationId,
|
||||
table.logType
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
export type Approval = InferSelectModel<typeof approvals>;
|
||||
export type Limit = InferSelectModel<typeof limits>;
|
||||
export type Account = InferSelectModel<typeof account>;
|
||||
export type Certificate = InferSelectModel<typeof certificates>;
|
||||
@@ -479,19 +309,3 @@ export type LoginPage = InferSelectModel<typeof loginPage>;
|
||||
export type LoginPageBranding = InferSelectModel<typeof loginPageBranding>;
|
||||
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
|
||||
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
|
||||
export type ConnectionAuditLog = InferSelectModel<typeof connectionAuditLog>;
|
||||
export type SessionTransferToken = InferSelectModel<
|
||||
typeof sessionTransferToken
|
||||
>;
|
||||
export type BannedEmail = InferSelectModel<typeof bannedEmails>;
|
||||
export type BannedIp = InferSelectModel<typeof bannedIps>;
|
||||
export type SiteProvisioningKey = InferSelectModel<typeof siteProvisioningKeys>;
|
||||
export type SiteProvisioningKeyOrg = InferSelectModel<
|
||||
typeof siteProvisioningKeyOrg
|
||||
>;
|
||||
export type EventStreamingDestination = InferSelectModel<
|
||||
typeof eventStreamingDestinations
|
||||
>;
|
||||
export type EventStreamingCursor = InferSelectModel<
|
||||
typeof eventStreamingCursors
|
||||
>;
|
||||
|
||||
+30
-221
@@ -1,18 +1,18 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { InferSelectModel } from "drizzle-orm";
|
||||
import {
|
||||
bigint,
|
||||
boolean,
|
||||
index,
|
||||
integer,
|
||||
pgTable,
|
||||
primaryKey,
|
||||
real,
|
||||
serial,
|
||||
varchar,
|
||||
boolean,
|
||||
integer,
|
||||
bigint,
|
||||
real,
|
||||
text,
|
||||
unique,
|
||||
varchar
|
||||
index,
|
||||
uniqueIndex
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { InferSelectModel } from "drizzle-orm";
|
||||
import { randomUUID } from "crypto";
|
||||
import { alias } from "yargs";
|
||||
|
||||
export const domains = pgTable("domains", {
|
||||
domainId: varchar("domainId").primaryKey(),
|
||||
@@ -24,8 +24,7 @@ export const domains = pgTable("domains", {
|
||||
tries: integer("tries").notNull().default(0),
|
||||
certResolver: varchar("certResolver"),
|
||||
customCertResolver: varchar("customCertResolver"),
|
||||
preferWildcardCert: boolean("preferWildcardCert"),
|
||||
errorMessage: text("errorMessage")
|
||||
preferWildcardCert: boolean("preferWildcardCert")
|
||||
});
|
||||
|
||||
export const dnsRecords = pgTable("dnsRecords", {
|
||||
@@ -56,14 +55,7 @@ export const orgs = pgTable("orgs", {
|
||||
.default(0),
|
||||
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||
.notNull()
|
||||
.default(0),
|
||||
settingsLogRetentionDaysConnection: integer("settingsLogRetentionDaysConnection") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||
.notNull()
|
||||
.default(0),
|
||||
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
||||
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
|
||||
isBillingOrg: boolean("isBillingOrg"),
|
||||
billingOrgId: varchar("billingOrgId")
|
||||
.default(0)
|
||||
});
|
||||
|
||||
export const orgDomains = pgTable("orgDomains", {
|
||||
@@ -94,14 +86,12 @@ export const sites = pgTable("sites", {
|
||||
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
||||
type: varchar("type").notNull(), // "newt" or "wireguard"
|
||||
online: boolean("online").notNull().default(false),
|
||||
lastPing: integer("lastPing"),
|
||||
address: varchar("address"),
|
||||
endpoint: varchar("endpoint"),
|
||||
publicKey: varchar("publicKey"),
|
||||
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
||||
listenPort: integer("listenPort"),
|
||||
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
|
||||
status: varchar("status").$type<"pending" | "approved">().default("approved")
|
||||
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true)
|
||||
});
|
||||
|
||||
export const resources = pgTable("resources", {
|
||||
@@ -152,8 +142,7 @@ export const resources = pgTable("resources", {
|
||||
}).default("forced"), // "forced" = always show, "automatic" = only when down
|
||||
maintenanceTitle: text("maintenanceTitle"),
|
||||
maintenanceMessage: text("maintenanceMessage"),
|
||||
maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
|
||||
postAuthPath: text("postAuthPath")
|
||||
maintenanceEstimatedTime: text("maintenanceEstimatedTime")
|
||||
});
|
||||
|
||||
export const targets = pgTable("targets", {
|
||||
@@ -198,9 +187,7 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
|
||||
hcFollowRedirects: boolean("hcFollowRedirects").default(true),
|
||||
hcMethod: varchar("hcMethod").default("GET"),
|
||||
hcStatus: integer("hcStatus"), // http code
|
||||
hcHealth: text("hcHealth")
|
||||
.$type<"unknown" | "healthy" | "unhealthy">()
|
||||
.default("unknown"), // "unknown", "healthy", "unhealthy"
|
||||
hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy"
|
||||
hcTlsServerName: text("hcTlsServerName")
|
||||
});
|
||||
|
||||
@@ -230,7 +217,7 @@ export const siteResources = pgTable("siteResources", {
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
niceId: varchar("niceId").notNull(),
|
||||
name: varchar("name").notNull(),
|
||||
mode: varchar("mode").$type<"host" | "cidr">().notNull(), // "host" | "cidr" | "port"
|
||||
mode: varchar("mode").notNull(), // "host" | "cidr" | "port"
|
||||
protocol: varchar("protocol"), // only for port mode
|
||||
proxyPort: integer("proxyPort"), // only for port mode
|
||||
destinationPort: integer("destinationPort"), // only for port mode
|
||||
@@ -240,11 +227,7 @@ export const siteResources = pgTable("siteResources", {
|
||||
aliasAddress: varchar("aliasAddress"),
|
||||
tcpPortRangeString: varchar("tcpPortRangeString").notNull().default("*"),
|
||||
udpPortRangeString: varchar("udpPortRangeString").notNull().default("*"),
|
||||
disableIcmp: boolean("disableIcmp").notNull().default(false),
|
||||
authDaemonPort: integer("authDaemonPort").default(22123),
|
||||
authDaemonMode: varchar("authDaemonMode", { length: 32 })
|
||||
.$type<"site" | "remote">()
|
||||
.default("site")
|
||||
disableIcmp: boolean("disableIcmp").notNull().default(false)
|
||||
});
|
||||
|
||||
export const clientSiteResources = pgTable("clientSiteResources", {
|
||||
@@ -291,10 +274,8 @@ export const users = pgTable("user", {
|
||||
dateCreated: varchar("dateCreated").notNull(),
|
||||
termsAcceptedTimestamp: varchar("termsAcceptedTimestamp"),
|
||||
termsVersion: varchar("termsVersion"),
|
||||
marketingEmailConsent: boolean("marketingEmailConsent").default(false),
|
||||
serverAdmin: boolean("serverAdmin").notNull().default(false),
|
||||
lastPasswordChange: bigint("lastPasswordChange", { mode: "number" }),
|
||||
locale: varchar("locale")
|
||||
lastPasswordChange: bigint("lastPasswordChange", { mode: "number" })
|
||||
});
|
||||
|
||||
export const newts = pgTable("newt", {
|
||||
@@ -342,9 +323,11 @@ export const userOrgs = pgTable("userOrgs", {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId),
|
||||
isOwner: boolean("isOwner").notNull().default(false),
|
||||
autoProvisioned: boolean("autoProvisioned").default(false),
|
||||
pamUsername: varchar("pamUsername") // cleaned username for ssh and such
|
||||
autoProvisioned: boolean("autoProvisioned").default(false)
|
||||
});
|
||||
|
||||
export const emailVerificationCodes = pgTable("emailVerificationCodes", {
|
||||
@@ -382,30 +365,9 @@ export const roles = pgTable("roles", {
|
||||
.notNull(),
|
||||
isAdmin: boolean("isAdmin"),
|
||||
name: varchar("name").notNull(),
|
||||
description: varchar("description"),
|
||||
requireDeviceApproval: boolean("requireDeviceApproval").default(false),
|
||||
sshSudoMode: varchar("sshSudoMode", { length: 32 }).default("none"), // "none" | "full" | "commands"
|
||||
sshSudoCommands: text("sshSudoCommands").default("[]"),
|
||||
sshCreateHomeDir: boolean("sshCreateHomeDir").default(true),
|
||||
sshUnixGroups: text("sshUnixGroups").default("[]")
|
||||
description: varchar("description")
|
||||
});
|
||||
|
||||
export const userOrgRoles = pgTable(
|
||||
"userOrgRoles",
|
||||
{
|
||||
userId: varchar("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, { onDelete: "cascade" })
|
||||
},
|
||||
(t) => [unique().on(t.userId, t.orgId, t.roleId)]
|
||||
);
|
||||
|
||||
export const roleActions = pgTable("roleActions", {
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
@@ -473,22 +435,12 @@ export const userInvites = pgTable("userInvites", {
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
email: varchar("email").notNull(),
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
|
||||
tokenHash: varchar("token").notNull()
|
||||
tokenHash: varchar("token").notNull(),
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const userInviteRoles = pgTable(
|
||||
"userInviteRoles",
|
||||
{
|
||||
inviteId: varchar("inviteId")
|
||||
.notNull()
|
||||
.references(() => userInvites.inviteId, { onDelete: "cascade" }),
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, { onDelete: "cascade" })
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.inviteId, t.roleId] })]
|
||||
);
|
||||
|
||||
export const resourcePincode = pgTable("resourcePincode", {
|
||||
pincodeId: serial("pincodeId").primaryKey(),
|
||||
resourceId: integer("resourceId")
|
||||
@@ -639,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", {
|
||||
@@ -737,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(
|
||||
@@ -752,7 +698,6 @@ export const clientSitesAssociationsCache = pgTable(
|
||||
.notNull(),
|
||||
siteId: integer("siteId").notNull(),
|
||||
isRelayed: boolean("isRelayed").notNull().default(false),
|
||||
isJitMode: boolean("isJitMode").notNull().default(false),
|
||||
endpoint: varchar("endpoint"),
|
||||
publicKey: varchar("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
||||
}
|
||||
@@ -767,16 +712,6 @@ export const clientSiteResourcesAssociationsCache = pgTable(
|
||||
}
|
||||
);
|
||||
|
||||
export const clientPostureSnapshots = pgTable("clientPostureSnapshots", {
|
||||
snapshotId: serial("snapshotId").primaryKey(),
|
||||
|
||||
clientId: integer("clientId").references(() => clients.clientId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
|
||||
collectedAt: integer("collectedAt").notNull()
|
||||
});
|
||||
|
||||
export const olms = pgTable("olms", {
|
||||
olmId: varchar("id").primaryKey(),
|
||||
secretHash: varchar("secretHash").notNull(),
|
||||
@@ -791,118 +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 currentFingerprint = pgTable("currentFingerprint", {
|
||||
fingerprintId: serial("id").primaryKey(),
|
||||
|
||||
olmId: text("olmId")
|
||||
.references(() => olms.olmId, { onDelete: "cascade" })
|
||||
.notNull(),
|
||||
|
||||
firstSeen: integer("firstSeen").notNull(),
|
||||
lastSeen: integer("lastSeen").notNull(),
|
||||
lastCollectedAt: integer("lastCollectedAt").notNull(),
|
||||
|
||||
username: text("username"),
|
||||
hostname: text("hostname"),
|
||||
platform: text("platform"),
|
||||
osVersion: text("osVersion"),
|
||||
kernelVersion: text("kernelVersion"),
|
||||
arch: text("arch"),
|
||||
deviceModel: text("deviceModel"),
|
||||
serialNumber: text("serialNumber"),
|
||||
platformFingerprint: varchar("platformFingerprint"),
|
||||
|
||||
// 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
|
||||
|
||||
windowsAntivirusEnabled: boolean("windowsAntivirusEnabled")
|
||||
.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)
|
||||
});
|
||||
|
||||
export const fingerprintSnapshots = pgTable("fingerprintSnapshots", {
|
||||
snapshotId: serial("id").primaryKey(),
|
||||
|
||||
fingerprintId: integer("fingerprintId").references(
|
||||
() => currentFingerprint.fingerprintId,
|
||||
{
|
||||
onDelete: "set null"
|
||||
}
|
||||
),
|
||||
|
||||
username: text("username"),
|
||||
hostname: text("hostname"),
|
||||
platform: text("platform"),
|
||||
osVersion: text("osVersion"),
|
||||
kernelVersion: text("kernelVersion"),
|
||||
arch: text("arch"),
|
||||
deviceModel: text("deviceModel"),
|
||||
serialNumber: text("serialNumber"),
|
||||
platformFingerprint: varchar("platformFingerprint"),
|
||||
|
||||
// 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
|
||||
|
||||
windowsAntivirusEnabled: boolean("windowsAntivirusEnabled")
|
||||
.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),
|
||||
|
||||
hash: text("hash").notNull(),
|
||||
collectedAt: integer("collectedAt").notNull()
|
||||
})
|
||||
});
|
||||
|
||||
export const olmSessions = pgTable("clientSession", {
|
||||
@@ -1031,16 +855,6 @@ export const deviceWebAuthCodes = pgTable("deviceWebAuthCodes", {
|
||||
})
|
||||
});
|
||||
|
||||
export const roundTripMessageTracker = pgTable("roundTripMessageTracker", {
|
||||
messageId: serial("messageId").primaryKey(),
|
||||
wsClientId: varchar("clientId"),
|
||||
messageType: varchar("messageType"),
|
||||
sentAt: bigint("sentAt", { mode: "number" }).notNull(),
|
||||
receivedAt: bigint("receivedAt", { mode: "number" }),
|
||||
error: text("error"),
|
||||
complete: boolean("complete").notNull().default(false)
|
||||
});
|
||||
|
||||
export type Org = InferSelectModel<typeof orgs>;
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
export type Site = InferSelectModel<typeof sites>;
|
||||
@@ -1064,9 +878,7 @@ export type UserSite = InferSelectModel<typeof userSites>;
|
||||
export type RoleResource = InferSelectModel<typeof roleResources>;
|
||||
export type UserResource = InferSelectModel<typeof userResources>;
|
||||
export type UserInvite = InferSelectModel<typeof userInvites>;
|
||||
export type UserInviteRole = InferSelectModel<typeof userInviteRoles>;
|
||||
export type UserOrg = InferSelectModel<typeof userOrgs>;
|
||||
export type UserOrgRole = InferSelectModel<typeof userOrgRoles>;
|
||||
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
||||
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||
@@ -1103,6 +915,3 @@ export type SecurityKey = InferSelectModel<typeof securityKeys>;
|
||||
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
|
||||
export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
|
||||
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
||||
export type RoundTripMessageTracker = InferSelectModel<
|
||||
typeof roundTripMessageTracker
|
||||
>;
|
||||
|
||||
@@ -1,12 +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,
|
||||
@@ -20,12 +12,13 @@ import {
|
||||
resources,
|
||||
roleResources,
|
||||
sessions,
|
||||
userOrgs,
|
||||
userResources,
|
||||
users,
|
||||
ResourceHeaderAuthExtendedCompatibility,
|
||||
resourceHeaderAuthExtendedCompatibility
|
||||
} from "@server/db";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
|
||||
export type ResourceWithAuth = {
|
||||
resource: Resource | null;
|
||||
@@ -111,15 +104,16 @@ export async function getUserSessionWithUser(
|
||||
}
|
||||
|
||||
/**
|
||||
* Get role name by role ID (for display).
|
||||
* Get user organization role
|
||||
*/
|
||||
export async function getRoleName(roleId: number): Promise<string | null> {
|
||||
const [row] = await db
|
||||
.select({ name: roles.name })
|
||||
.from(roles)
|
||||
.where(eq(roles.roleId, roleId))
|
||||
export async function getUserOrgRole(userId: string, orgId: string) {
|
||||
const userOrgRole = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
||||
.limit(1);
|
||||
return row?.name ?? null;
|
||||
|
||||
return userOrgRole.length > 0 ? userOrgRole[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,7 +121,7 @@ export async function getRoleName(roleId: number): Promise<string | null> {
|
||||
*/
|
||||
export async function getRoleResourceAccess(
|
||||
resourceId: number,
|
||||
roleIds: number[]
|
||||
roleId: number
|
||||
) {
|
||||
const roleResourceAccess = await db
|
||||
.select()
|
||||
@@ -135,11 +129,12 @@ export async function getRoleResourceAccess(
|
||||
.where(
|
||||
and(
|
||||
eq(roleResources.resourceId, resourceId),
|
||||
inArray(roleResources.roleId, roleIds)
|
||||
eq(roleResources.roleId, roleId)
|
||||
)
|
||||
);
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return roleResourceAccess.length > 0 ? roleResourceAccess : null;
|
||||
return roleResourceAccess.length > 0 ? roleResourceAccess[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
// Regions of the World
|
||||
// as of 2025-10-25
|
||||
//
|
||||
// Adapted according to the United Nations Geoscheme
|
||||
// see https://www.unicode.org/cldr/charts/48/supplemental/territory_containment_un_m_49.html
|
||||
// see https://unstats.un.org/unsd/methodology/m49
|
||||
|
||||
export const REGIONS = [
|
||||
{
|
||||
name: "regionAfrica",
|
||||
id: "002",
|
||||
includes: [
|
||||
{
|
||||
name: "regionNorthernAfrica",
|
||||
id: "015",
|
||||
countries: ["DZ", "EG", "LY", "MA", "SD", "TN", "EH"]
|
||||
},
|
||||
{
|
||||
name: "regionEasternAfrica",
|
||||
id: "014",
|
||||
countries: ["IO", "BI", "KM", "DJ", "ER", "ET", "TF", "KE", "MG", "MW", "MU", "YT", "MZ", "RE", "RW", "SC", "SO", "SS", "UG", "ZM", "ZW"]
|
||||
},
|
||||
{
|
||||
name: "regionMiddleAfrica",
|
||||
id: "017",
|
||||
countries: ["AO", "CM", "CF", "TD", "CG", "CD", "GQ", "GA", "ST"]
|
||||
},
|
||||
{
|
||||
name: "regionSouthernAfrica",
|
||||
id: "018",
|
||||
countries: ["BW", "SZ", "LS", "NA", "ZA"]
|
||||
},
|
||||
{
|
||||
name: "regionWesternAfrica",
|
||||
id: "011",
|
||||
countries: ["BJ", "BF", "CV", "CI", "GM", "GH", "GN", "GW", "LR", "ML", "MR", "NE", "NG", "SH", "SN", "SL", "TG"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "regionAmericas",
|
||||
id: "019",
|
||||
includes: [
|
||||
{
|
||||
name: "regionCaribbean",
|
||||
id: "029",
|
||||
countries: ["AI", "AG", "AW", "BS", "BB", "BQ", "VG", "KY", "CU", "CW", "DM", "DO", "GD", "GP", "HT", "JM", "MQ", "MS", "PR", "BL", "KN", "LC", "MF", "VC", "SX", "TT", "TC", "VI"]
|
||||
},
|
||||
{
|
||||
name: "regionCentralAmerica",
|
||||
id: "013",
|
||||
countries: ["BZ", "CR", "SV", "GT", "HN", "MX", "NI", "PA"]
|
||||
},
|
||||
{
|
||||
name: "regionSouthAmerica",
|
||||
id: "005",
|
||||
countries: ["AR", "BO", "BV", "BR", "CL", "CO", "EC", "FK", "GF", "GY", "PY", "PE", "GS", "SR", "UY", "VE"]
|
||||
},
|
||||
{
|
||||
name: "regionNorthernAmerica",
|
||||
id: "021",
|
||||
countries: ["BM", "CA", "GL", "PM", "US"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "regionAsia",
|
||||
id: "142",
|
||||
includes: [
|
||||
{
|
||||
name: "regionCentralAsia",
|
||||
id: "143",
|
||||
countries: ["KZ", "KG", "TJ", "TM", "UZ"]
|
||||
},
|
||||
{
|
||||
name: "regionEasternAsia",
|
||||
id: "030",
|
||||
countries: ["CN", "HK", "MO", "KP", "JP", "MN", "KR"]
|
||||
},
|
||||
{
|
||||
name: "regionSouthEasternAsia",
|
||||
id: "035",
|
||||
countries: ["BN", "KH", "ID", "LA", "MY", "MM", "PH", "SG", "TH", "TL", "VN"]
|
||||
},
|
||||
{
|
||||
name: "regionSouthernAsia",
|
||||
id: "034",
|
||||
countries: ["AF", "BD", "BT", "IN", "IR", "MV", "NP", "PK", "LK"]
|
||||
},
|
||||
{
|
||||
name: "regionWesternAsia",
|
||||
id: "145",
|
||||
countries: ["AM", "AZ", "BH", "CY", "GE", "IQ", "IL", "JO", "KW", "LB", "OM", "QA", "SA", "PS", "SY", "TR", "AE", "YE"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "regionEurope",
|
||||
id: "150",
|
||||
includes: [
|
||||
{
|
||||
name: "regionEasternEurope",
|
||||
id: "151",
|
||||
countries: ["BY", "BG", "CZ", "HU", "PL", "MD", "RO", "RU", "SK", "UA"]
|
||||
},
|
||||
{
|
||||
name: "regionNorthernEurope",
|
||||
id: "154",
|
||||
countries: ["AX", "DK", "EE", "FO", "FI", "GG", "IS", "IE", "IM", "JE", "LV", "LT", "NO", "SJ", "SE", "GB"]
|
||||
},
|
||||
{
|
||||
name: "regionSouthernEurope",
|
||||
id: "039",
|
||||
countries: ["AL", "AD", "BA", "HR", "GI", "GR", "VA", "IT", "MT", "ME", "MK", "PT", "SM", "RS", "SI", "ES"]
|
||||
},
|
||||
{
|
||||
name: "regionWesternEurope",
|
||||
id: "155",
|
||||
countries: ["AT", "BE", "FR", "DE", "LI", "LU", "MC", "NL", "CH"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "regionOceania",
|
||||
id: "009",
|
||||
includes: [
|
||||
{
|
||||
name: "regionAustraliaAndNewZealand",
|
||||
id: "053",
|
||||
countries: ["AU", "CX", "CC", "HM", "NZ", "NF"]
|
||||
},
|
||||
{
|
||||
name: "regionMelanesia",
|
||||
id: "054",
|
||||
countries: ["FJ", "NC", "PG", "SB", "VU"]
|
||||
},
|
||||
{
|
||||
name: "regionMicronesia",
|
||||
id: "057",
|
||||
countries: ["GU", "KI", "MH", "FM", "NR", "MP", "PW", "UM"]
|
||||
},
|
||||
{
|
||||
name: "regionPolynesia",
|
||||
id: "061",
|
||||
countries: ["AS", "CK", "PF", "NU", "PN", "WS", "TK", "TO", "TV", "WF"]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
type Subregion = {
|
||||
name: string;
|
||||
id: string;
|
||||
countries: string[];
|
||||
};
|
||||
|
||||
type Region = {
|
||||
name: string;
|
||||
id: string;
|
||||
includes: Subregion[];
|
||||
};
|
||||
|
||||
export function getRegionNameById(regionId: string): string | undefined {
|
||||
// Check top-level regions
|
||||
const region = REGIONS.find((r) => r.id === regionId);
|
||||
if (region) {
|
||||
return region.name;
|
||||
}
|
||||
|
||||
// Check subregions
|
||||
for (const region of REGIONS) {
|
||||
for (const subregion of region.includes) {
|
||||
if (subregion.id === regionId) {
|
||||
return subregion.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isValidRegionId(regionId: string): boolean {
|
||||
// Check top-level regions
|
||||
if (REGIONS.find((r) => r.id === regionId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check subregions
|
||||
for (const region of REGIONS) {
|
||||
if (region.includes.find((s) => s.id === regionId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -23,8 +23,7 @@ export default db;
|
||||
export const primaryDb = db;
|
||||
export type Transaction = Parameters<
|
||||
Parameters<(typeof db)["transaction"]>[0]
|
||||
>[0];
|
||||
export const DB_TYPE: "pg" | "sqlite" = "sqlite";
|
||||
>[0];
|
||||
|
||||
function checkFileExists(filePath: string): boolean {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
export * from "./driver";
|
||||
export * from "./logsDriver";
|
||||
export * from "./safeRead";
|
||||
export * from "./schema/schema";
|
||||
export * from "./schema/privateSchema";
|
||||
export * from "./migrate";
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { db as mainDb } from "./driver";
|
||||
|
||||
// SQLite doesn't support separate databases for logs in the same way as Postgres
|
||||
// Always use the main database connection for SQLite
|
||||
export const logsDb = mainDb;
|
||||
export default logsDb;
|
||||
export const primaryLogsDb = logsDb;
|
||||
@@ -4,7 +4,7 @@ import path from "path";
|
||||
|
||||
const migrationsFolder = path.join("server/migrations");
|
||||
|
||||
export const runMigrations = async () => {
|
||||
const runMigrations = async () => {
|
||||
console.log("Running migrations...");
|
||||
try {
|
||||
migrate(db as any, {
|
||||
@@ -16,3 +16,5 @@ export const runMigrations = async () => {
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
runMigrations();
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { db } from "./driver";
|
||||
|
||||
/**
|
||||
* Runs a read query. For SQLite there is no replica/primary distinction,
|
||||
* so the query is executed once against the database.
|
||||
*/
|
||||
export async function safeRead<T>(
|
||||
query: (d: typeof db) => Promise<T>
|
||||
): Promise<T> {
|
||||
return query(db);
|
||||
}
|
||||
@@ -2,22 +2,11 @@ import { InferSelectModel } from "drizzle-orm";
|
||||
import {
|
||||
index,
|
||||
integer,
|
||||
primaryKey,
|
||||
real,
|
||||
sqliteTable,
|
||||
text,
|
||||
uniqueIndex
|
||||
text
|
||||
} from "drizzle-orm/sqlite-core";
|
||||
import {
|
||||
clients,
|
||||
domains,
|
||||
exitNodes,
|
||||
orgs,
|
||||
sessions,
|
||||
siteResources,
|
||||
sites,
|
||||
users
|
||||
} from "./schema";
|
||||
import { domains, exitNodes, orgs, sessions, users } from "./schema";
|
||||
|
||||
export const certificates = sqliteTable("certificates", {
|
||||
certId: integer("certId").primaryKey({ autoIncrement: true }),
|
||||
@@ -81,16 +70,13 @@ export const subscriptions = sqliteTable("subscriptions", {
|
||||
canceledAt: integer("canceledAt"),
|
||||
createdAt: integer("createdAt").notNull(),
|
||||
updatedAt: integer("updatedAt"),
|
||||
version: integer("version"),
|
||||
billingCycleAnchor: integer("billingCycleAnchor"),
|
||||
type: text("type") // tier1, tier2, tier3, or license
|
||||
billingCycleAnchor: integer("billingCycleAnchor")
|
||||
});
|
||||
|
||||
export const subscriptionItems = sqliteTable("subscriptionItems", {
|
||||
subscriptionItemId: integer("subscriptionItemId").primaryKey({
|
||||
autoIncrement: true
|
||||
}),
|
||||
stripeSubscriptionItemId: text("stripeSubscriptionItemId"),
|
||||
subscriptionId: text("subscriptionId")
|
||||
.notNull()
|
||||
.references(() => subscriptions.subscriptionId, {
|
||||
@@ -98,7 +84,6 @@ export const subscriptionItems = sqliteTable("subscriptionItems", {
|
||||
}),
|
||||
planId: text("planId").notNull(),
|
||||
priceId: text("priceId"),
|
||||
featureId: text("featureId"),
|
||||
meterId: text("meterId"),
|
||||
unitAmount: real("unitAmount"),
|
||||
tiers: text("tiers"),
|
||||
@@ -141,7 +126,6 @@ export const limits = sqliteTable("limits", {
|
||||
})
|
||||
.notNull(),
|
||||
value: real("value"),
|
||||
override: integer("override", { mode: "boolean" }).default(false),
|
||||
description: text("description")
|
||||
});
|
||||
|
||||
@@ -222,7 +206,7 @@ export const loginPageBranding = sqliteTable("loginPageBranding", {
|
||||
loginPageBrandingId: integer("loginPageBrandingId").primaryKey({
|
||||
autoIncrement: true
|
||||
}),
|
||||
logoUrl: text("logoUrl"),
|
||||
logoUrl: text("logoUrl").notNull(),
|
||||
logoWidth: integer("logoWidth").notNull(),
|
||||
logoHeight: integer("logoHeight").notNull(),
|
||||
primaryColor: text("primaryColor"),
|
||||
@@ -289,7 +273,6 @@ export const accessAuditLog = sqliteTable(
|
||||
actor: text("actor"),
|
||||
actorId: text("actorId"),
|
||||
resourceId: integer("resourceId"),
|
||||
siteResourceId: integer("siteResourceId"),
|
||||
ip: text("ip"),
|
||||
location: text("location"),
|
||||
type: text("type").notNull(),
|
||||
@@ -306,156 +289,6 @@ export const accessAuditLog = sqliteTable(
|
||||
]
|
||||
);
|
||||
|
||||
export const connectionAuditLog = sqliteTable(
|
||||
"connectionAuditLog",
|
||||
{
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
sessionId: text("sessionId").notNull(),
|
||||
siteResourceId: integer("siteResourceId").references(
|
||||
() => siteResources.siteResourceId,
|
||||
{ onDelete: "cascade" }
|
||||
),
|
||||
orgId: text("orgId").references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
siteId: integer("siteId").references(() => sites.siteId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
clientId: integer("clientId").references(() => clients.clientId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
userId: text("userId").references(() => users.userId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
sourceAddr: text("sourceAddr").notNull(),
|
||||
destAddr: text("destAddr").notNull(),
|
||||
protocol: text("protocol").notNull(),
|
||||
startedAt: integer("startedAt").notNull(),
|
||||
endedAt: integer("endedAt"),
|
||||
bytesTx: integer("bytesTx"),
|
||||
bytesRx: integer("bytesRx")
|
||||
},
|
||||
(table) => [
|
||||
index("idx_accessAuditLog_startedAt").on(table.startedAt),
|
||||
index("idx_accessAuditLog_org_startedAt").on(
|
||||
table.orgId,
|
||||
table.startedAt
|
||||
),
|
||||
index("idx_accessAuditLog_siteResourceId").on(table.siteResourceId)
|
||||
]
|
||||
);
|
||||
|
||||
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 const bannedEmails = sqliteTable("bannedEmails", {
|
||||
email: text("email").primaryKey()
|
||||
});
|
||||
|
||||
export const bannedIps = sqliteTable("bannedIps", {
|
||||
ip: text("ip").primaryKey()
|
||||
});
|
||||
|
||||
export const siteProvisioningKeys = sqliteTable("siteProvisioningKeys", {
|
||||
siteProvisioningKeyId: text("siteProvisioningKeyId").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
siteProvisioningKeyHash: text("siteProvisioningKeyHash").notNull(),
|
||||
lastChars: text("lastChars").notNull(),
|
||||
createdAt: text("dateCreated").notNull(),
|
||||
lastUsed: text("lastUsed"),
|
||||
maxBatchSize: integer("maxBatchSize"), // null = no limit
|
||||
numUsed: integer("numUsed").notNull().default(0),
|
||||
validUntil: text("validUntil"),
|
||||
approveNewSites: integer("approveNewSites", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(true)
|
||||
});
|
||||
|
||||
export const siteProvisioningKeyOrg = sqliteTable(
|
||||
"siteProvisioningKeyOrg",
|
||||
{
|
||||
siteProvisioningKeyId: text("siteProvisioningKeyId")
|
||||
.notNull()
|
||||
.references(() => siteProvisioningKeys.siteProvisioningKeyId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||
},
|
||||
(table) => [
|
||||
primaryKey({
|
||||
columns: [table.siteProvisioningKeyId, table.orgId]
|
||||
})
|
||||
]
|
||||
);
|
||||
|
||||
export const eventStreamingDestinations = sqliteTable(
|
||||
"eventStreamingDestinations",
|
||||
{
|
||||
destinationId: integer("destinationId").primaryKey({
|
||||
autoIncrement: true
|
||||
}),
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
sendConnectionLogs: integer("sendConnectionLogs", { mode: "boolean" }).notNull().default(false),
|
||||
sendRequestLogs: integer("sendRequestLogs", { mode: "boolean" }).notNull().default(false),
|
||||
sendActionLogs: integer("sendActionLogs", { mode: "boolean" }).notNull().default(false),
|
||||
sendAccessLogs: integer("sendAccessLogs", { mode: "boolean" }).notNull().default(false),
|
||||
type: text("type").notNull(), // e.g. "http", "kafka", etc.
|
||||
config: text("config").notNull(), // JSON string with the configuration for the destination
|
||||
enabled: integer("enabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(true),
|
||||
createdAt: integer("createdAt").notNull(),
|
||||
updatedAt: integer("updatedAt").notNull()
|
||||
}
|
||||
);
|
||||
|
||||
export const eventStreamingCursors = sqliteTable(
|
||||
"eventStreamingCursors",
|
||||
{
|
||||
cursorId: integer("cursorId").primaryKey({ autoIncrement: true }),
|
||||
destinationId: integer("destinationId")
|
||||
.notNull()
|
||||
.references(() => eventStreamingDestinations.destinationId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
logType: text("logType").notNull(), // "request" | "action" | "access" | "connection"
|
||||
lastSentId: integer("lastSentId").notNull().default(0),
|
||||
lastSentAt: integer("lastSentAt") // epoch milliseconds, null if never sent
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("idx_eventStreamingCursors_dest_type").on(
|
||||
table.destinationId,
|
||||
table.logType
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
export type Approval = InferSelectModel<typeof approvals>;
|
||||
export type Limit = InferSelectModel<typeof limits>;
|
||||
export type Account = InferSelectModel<typeof account>;
|
||||
export type Certificate = InferSelectModel<typeof certificates>;
|
||||
@@ -476,13 +309,3 @@ export type LoginPage = InferSelectModel<typeof loginPage>;
|
||||
export type LoginPageBranding = InferSelectModel<typeof loginPageBranding>;
|
||||
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
|
||||
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
|
||||
export type ConnectionAuditLog = InferSelectModel<typeof connectionAuditLog>;
|
||||
export type BannedEmail = InferSelectModel<typeof bannedEmails>;
|
||||
export type BannedIp = InferSelectModel<typeof bannedIps>;
|
||||
export type SiteProvisioningKey = InferSelectModel<typeof siteProvisioningKeys>;
|
||||
export type EventStreamingDestination = InferSelectModel<
|
||||
typeof eventStreamingDestinations
|
||||
>;
|
||||
export type EventStreamingCursor = InferSelectModel<
|
||||
typeof eventStreamingCursors
|
||||
>;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { InferSelectModel } from "drizzle-orm";
|
||||
import {
|
||||
index,
|
||||
integer,
|
||||
primaryKey,
|
||||
sqliteTable,
|
||||
text,
|
||||
unique
|
||||
integer,
|
||||
index,
|
||||
uniqueIndex
|
||||
} from "drizzle-orm/sqlite-core";
|
||||
import { no } from "zod/v4/locales";
|
||||
|
||||
export const domains = sqliteTable("domains", {
|
||||
domainId: text("domainId").primaryKey(),
|
||||
@@ -20,8 +20,7 @@ export const domains = sqliteTable("domains", {
|
||||
failed: integer("failed", { mode: "boolean" }).notNull().default(false),
|
||||
tries: integer("tries").notNull().default(0),
|
||||
certResolver: text("certResolver"),
|
||||
preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" }),
|
||||
errorMessage: text("errorMessage")
|
||||
preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" })
|
||||
});
|
||||
|
||||
export const dnsRecords = sqliteTable("dnsRecords", {
|
||||
@@ -53,14 +52,7 @@ export const orgs = sqliteTable("orgs", {
|
||||
.default(0),
|
||||
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||
.notNull()
|
||||
.default(0),
|
||||
settingsLogRetentionDaysConnection: integer("settingsLogRetentionDaysConnection") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||
.notNull()
|
||||
.default(0),
|
||||
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
||||
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
|
||||
isBillingOrg: integer("isBillingOrg", { mode: "boolean" }),
|
||||
billingOrgId: text("billingOrgId")
|
||||
.default(0)
|
||||
});
|
||||
|
||||
export const userDomains = sqliteTable("userDomains", {
|
||||
@@ -100,7 +92,6 @@ export const sites = sqliteTable("sites", {
|
||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||
type: text("type").notNull(), // "newt" or "wireguard"
|
||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||
lastPing: integer("lastPing"),
|
||||
|
||||
// exit node stuff that is how to connect to the site when it has a wg server
|
||||
address: text("address"), // this is the address of the wireguard interface in newt
|
||||
@@ -110,8 +101,7 @@ export const sites = sqliteTable("sites", {
|
||||
listenPort: integer("listenPort"),
|
||||
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(true),
|
||||
status: text("status").$type<"pending" | "approved">().default("approved")
|
||||
.default(true)
|
||||
});
|
||||
|
||||
export const resources = sqliteTable("resources", {
|
||||
@@ -172,8 +162,7 @@ export const resources = sqliteTable("resources", {
|
||||
}).default("forced"), // "forced" = always show, "automatic" = only when down
|
||||
maintenanceTitle: text("maintenanceTitle"),
|
||||
maintenanceMessage: text("maintenanceMessage"),
|
||||
maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
|
||||
postAuthPath: text("postAuthPath")
|
||||
maintenanceEstimatedTime: text("maintenanceEstimatedTime")
|
||||
});
|
||||
|
||||
export const targets = sqliteTable("targets", {
|
||||
@@ -224,9 +213,7 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
||||
}).default(true),
|
||||
hcMethod: text("hcMethod").default("GET"),
|
||||
hcStatus: integer("hcStatus"), // http code
|
||||
hcHealth: text("hcHealth")
|
||||
.$type<"unknown" | "healthy" | "unhealthy">()
|
||||
.default("unknown"), // "unknown", "healthy", "unhealthy"
|
||||
hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy"
|
||||
hcTlsServerName: text("hcTlsServerName")
|
||||
});
|
||||
|
||||
@@ -258,7 +245,7 @@ export const siteResources = sqliteTable("siteResources", {
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
niceId: text("niceId").notNull(),
|
||||
name: text("name").notNull(),
|
||||
mode: text("mode").$type<"host" | "cidr">().notNull(), // "host" | "cidr" | "port"
|
||||
mode: text("mode").notNull(), // "host" | "cidr" | "port"
|
||||
protocol: text("protocol"), // only for port mode
|
||||
proxyPort: integer("proxyPort"), // only for port mode
|
||||
destinationPort: integer("destinationPort"), // only for port mode
|
||||
@@ -268,13 +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),
|
||||
authDaemonPort: integer("authDaemonPort").default(22123),
|
||||
authDaemonMode: text("authDaemonMode")
|
||||
.$type<"site" | "remote">()
|
||||
.default("site")
|
||||
disableIcmp: integer("disableIcmp", { mode: "boolean" }).notNull().default(false)
|
||||
});
|
||||
|
||||
export const clientSiteResources = sqliteTable("clientSiteResources", {
|
||||
@@ -327,14 +308,10 @@ export const users = sqliteTable("user", {
|
||||
dateCreated: text("dateCreated").notNull(),
|
||||
termsAcceptedTimestamp: text("termsAcceptedTimestamp"),
|
||||
termsVersion: text("termsVersion"),
|
||||
marketingEmailConsent: integer("marketingEmailConsent", {
|
||||
mode: "boolean"
|
||||
}).default(false),
|
||||
serverAdmin: integer("serverAdmin", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
lastPasswordChange: integer("lastPasswordChange"),
|
||||
locale: text("locale")
|
||||
lastPasswordChange: integer("lastPasswordChange")
|
||||
});
|
||||
|
||||
export const securityKeys = sqliteTable("webauthnCredentials", {
|
||||
@@ -406,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(
|
||||
@@ -423,9 +395,6 @@ export const clientSitesAssociationsCache = sqliteTable(
|
||||
isRelayed: integer("isRelayed", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
isJitMode: integer("isJitMode", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
endpoint: text("endpoint"),
|
||||
publicKey: text("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
||||
}
|
||||
@@ -454,160 +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 currentFingerprint = sqliteTable("currentFingerprint", {
|
||||
fingerprintId: integer("id").primaryKey({ autoIncrement: true }),
|
||||
|
||||
olmId: text("olmId")
|
||||
.references(() => olms.olmId, { onDelete: "cascade" })
|
||||
.notNull(),
|
||||
|
||||
firstSeen: integer("firstSeen").notNull(),
|
||||
lastSeen: integer("lastSeen").notNull(),
|
||||
lastCollectedAt: integer("lastCollectedAt").notNull(),
|
||||
|
||||
username: text("username"),
|
||||
hostname: text("hostname"),
|
||||
platform: text("platform"),
|
||||
osVersion: text("osVersion"),
|
||||
kernelVersion: text("kernelVersion"),
|
||||
arch: text("arch"),
|
||||
deviceModel: text("deviceModel"),
|
||||
serialNumber: text("serialNumber"),
|
||||
platformFingerprint: text("platformFingerprint"),
|
||||
|
||||
// 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
|
||||
|
||||
windowsAntivirusEnabled: integer("windowsAntivirusEnabled", {
|
||||
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)
|
||||
});
|
||||
|
||||
export const fingerprintSnapshots = sqliteTable("fingerprintSnapshots", {
|
||||
snapshotId: integer("id").primaryKey({ autoIncrement: true }),
|
||||
|
||||
fingerprintId: integer("fingerprintId").references(
|
||||
() => currentFingerprint.fingerprintId,
|
||||
{
|
||||
onDelete: "set null"
|
||||
}
|
||||
),
|
||||
|
||||
username: text("username"),
|
||||
hostname: text("hostname"),
|
||||
platform: text("platform"),
|
||||
osVersion: text("osVersion"),
|
||||
kernelVersion: text("kernelVersion"),
|
||||
arch: text("arch"),
|
||||
deviceModel: text("deviceModel"),
|
||||
serialNumber: text("serialNumber"),
|
||||
platformFingerprint: text("platformFingerprint"),
|
||||
|
||||
// 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
|
||||
|
||||
windowsAntivirusEnabled: integer("windowsAntivirusEnabled", {
|
||||
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),
|
||||
|
||||
hash: text("hash").notNull(),
|
||||
collectedAt: integer("collectedAt").notNull()
|
||||
});
|
||||
|
||||
export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", {
|
||||
@@ -655,11 +471,13 @@ export const userOrgs = sqliteTable("userOrgs", {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId),
|
||||
isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false),
|
||||
autoProvisioned: integer("autoProvisioned", {
|
||||
mode: "boolean"
|
||||
}).default(false),
|
||||
pamUsername: text("pamUsername") // cleaned username for ssh and such
|
||||
}).default(false)
|
||||
});
|
||||
|
||||
export const emailVerificationCodes = sqliteTable("emailVerificationCodes", {
|
||||
@@ -697,34 +515,9 @@ 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),
|
||||
sshSudoMode: text("sshSudoMode").default("none"), // "none" | "full" | "commands"
|
||||
sshSudoCommands: text("sshSudoCommands").default("[]"),
|
||||
sshCreateHomeDir: integer("sshCreateHomeDir", { mode: "boolean" }).default(
|
||||
true
|
||||
),
|
||||
sshUnixGroups: text("sshUnixGroups").default("[]")
|
||||
description: text("description")
|
||||
});
|
||||
|
||||
export const userOrgRoles = sqliteTable(
|
||||
"userOrgRoles",
|
||||
{
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, { onDelete: "cascade" })
|
||||
},
|
||||
(t) => [unique().on(t.userId, t.orgId, t.roleId)]
|
||||
);
|
||||
|
||||
export const roleActions = sqliteTable("roleActions", {
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
@@ -810,22 +603,12 @@ export const userInvites = sqliteTable("userInvites", {
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
email: text("email").notNull(),
|
||||
expiresAt: integer("expiresAt").notNull(),
|
||||
tokenHash: text("token").notNull()
|
||||
tokenHash: text("token").notNull(),
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const userInviteRoles = sqliteTable(
|
||||
"userInviteRoles",
|
||||
{
|
||||
inviteId: text("inviteId")
|
||||
.notNull()
|
||||
.references(() => userInvites.inviteId, { onDelete: "cascade" }),
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, { onDelete: "cascade" })
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.inviteId, t.roleId] })]
|
||||
);
|
||||
|
||||
export const resourcePincode = sqliteTable("resourcePincode", {
|
||||
pincodeId: integer("pincodeId").primaryKey({
|
||||
autoIncrement: true
|
||||
@@ -991,8 +774,7 @@ export const idp = sqliteTable("idp", {
|
||||
mode: "boolean"
|
||||
})
|
||||
.notNull()
|
||||
.default(false),
|
||||
tags: text("tags")
|
||||
.default(false)
|
||||
});
|
||||
|
||||
// Identity Provider OAuth Configuration
|
||||
@@ -1133,16 +915,6 @@ export const deviceWebAuthCodes = sqliteTable("deviceWebAuthCodes", {
|
||||
})
|
||||
});
|
||||
|
||||
export const roundTripMessageTracker = sqliteTable("roundTripMessageTracker", {
|
||||
messageId: integer("messageId").primaryKey({ autoIncrement: true }),
|
||||
wsClientId: text("clientId"),
|
||||
messageType: text("messageType"),
|
||||
sentAt: integer("sentAt").notNull(),
|
||||
receivedAt: integer("receivedAt"),
|
||||
error: text("error"),
|
||||
complete: integer("complete", { mode: "boolean" }).notNull().default(false)
|
||||
});
|
||||
|
||||
export type Org = InferSelectModel<typeof orgs>;
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
export type Site = InferSelectModel<typeof sites>;
|
||||
@@ -1168,9 +940,7 @@ export type UserSite = InferSelectModel<typeof userSites>;
|
||||
export type RoleResource = InferSelectModel<typeof roleResources>;
|
||||
export type UserResource = InferSelectModel<typeof userResources>;
|
||||
export type UserInvite = InferSelectModel<typeof userInvites>;
|
||||
export type UserInviteRole = InferSelectModel<typeof userInviteRoles>;
|
||||
export type UserOrg = InferSelectModel<typeof userOrgs>;
|
||||
export type UserOrgRole = InferSelectModel<typeof userOrgRoles>;
|
||||
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
||||
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||
@@ -1206,6 +976,3 @@ export type SecurityKey = InferSelectModel<typeof securityKeys>;
|
||||
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
|
||||
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
||||
export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
|
||||
export type RoundTripMessageTracker = InferSelectModel<
|
||||
typeof roundTripMessageTracker
|
||||
>;
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
import React from "react";
|
||||
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
||||
import { themeColors } from "./lib/theme";
|
||||
import {
|
||||
EmailContainer,
|
||||
EmailFooter,
|
||||
EmailGreeting,
|
||||
EmailHeading,
|
||||
EmailInfoSection,
|
||||
EmailLetterHead,
|
||||
EmailSection,
|
||||
EmailSignature,
|
||||
EmailText
|
||||
} from "./components/Email";
|
||||
import CopyCodeBox from "./components/CopyCodeBox";
|
||||
import ButtonLink from "./components/ButtonLink";
|
||||
|
||||
type EnterpriseEditionKeyGeneratedProps = {
|
||||
keyValue: string;
|
||||
personalUseOnly: boolean;
|
||||
users: number;
|
||||
sites: number;
|
||||
modifySubscriptionLink?: string;
|
||||
};
|
||||
|
||||
export const EnterpriseEditionKeyGenerated = ({
|
||||
keyValue,
|
||||
personalUseOnly,
|
||||
users,
|
||||
sites,
|
||||
modifySubscriptionLink
|
||||
}: EnterpriseEditionKeyGeneratedProps) => {
|
||||
const previewText = personalUseOnly
|
||||
? "Your Enterprise Edition key for personal use is ready"
|
||||
: "Thank you for your purchase — your Enterprise Edition key is ready";
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind config={themeColors}>
|
||||
<Body className="font-sans bg-gray-50">
|
||||
<EmailContainer>
|
||||
<EmailLetterHead />
|
||||
|
||||
<EmailGreeting>Hi there,</EmailGreeting>
|
||||
|
||||
{personalUseOnly ? (
|
||||
<EmailText>
|
||||
Your Enterprise Edition license key has been
|
||||
generated. Qualifying users can use the
|
||||
Enterprise Edition for free for{" "}
|
||||
<strong>personal use only</strong>.
|
||||
</EmailText>
|
||||
) : (
|
||||
<>
|
||||
<EmailText>
|
||||
Thank you for your purchase. Your Enterprise
|
||||
Edition license key is ready. Below are the
|
||||
terms of your license.
|
||||
</EmailText>
|
||||
<EmailInfoSection
|
||||
title="License details"
|
||||
items={[
|
||||
{
|
||||
label: "Licensed users",
|
||||
value: users
|
||||
},
|
||||
{
|
||||
label: "Licensed sites",
|
||||
value: sites
|
||||
}
|
||||
]}
|
||||
/>
|
||||
{modifySubscriptionLink && (
|
||||
<EmailSection>
|
||||
<ButtonLink
|
||||
href={modifySubscriptionLink}
|
||||
>
|
||||
Modify subscription
|
||||
</ButtonLink>
|
||||
</EmailSection>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<EmailSection>
|
||||
<EmailText>Your license key:</EmailText>
|
||||
<CopyCodeBox
|
||||
text={keyValue}
|
||||
hint="Copy this key and use it when activating Enterprise Edition on your Pangolin host."
|
||||
/>
|
||||
</EmailSection>
|
||||
|
||||
<EmailText>
|
||||
If you need to purchase additional license keys or
|
||||
modify your existing license, please reach out to
|
||||
our support team at{" "}
|
||||
<a
|
||||
href="mailto:support@pangolin.net"
|
||||
className="text-primary font-medium"
|
||||
>
|
||||
support@pangolin.net
|
||||
</a>
|
||||
.
|
||||
</EmailText>
|
||||
|
||||
<EmailFooter>
|
||||
<EmailSignature />
|
||||
</EmailFooter>
|
||||
</EmailContainer>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnterpriseEditionKeyGenerated;
|
||||
@@ -1,14 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
const DEFAULT_HINT = "Copy and paste this code when prompted";
|
||||
|
||||
export default function CopyCodeBox({
|
||||
text,
|
||||
hint
|
||||
}: {
|
||||
text: string;
|
||||
hint?: string;
|
||||
}) {
|
||||
export default function CopyCodeBox({ text }: { text: string }) {
|
||||
return (
|
||||
<div className="inline-block">
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg px-6 py-4 mx-auto">
|
||||
@@ -16,7 +8,9 @@ export default function CopyCodeBox({
|
||||
{text}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-2">{hint ?? DEFAULT_HINT}</p>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Copy and paste this code when prompted
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ declare global {
|
||||
session: Session;
|
||||
userOrg?: UserOrg;
|
||||
apiKeyOrg?: ApiKeyOrg;
|
||||
userOrgRoleIds?: number[];
|
||||
userOrgRoleId?: number;
|
||||
userOrgId?: string;
|
||||
userOrgIds?: string[];
|
||||
remoteExitNode?: RemoteExitNode;
|
||||
|
||||
@@ -17,7 +17,6 @@ import fs from "fs";
|
||||
import path from "path";
|
||||
import { APP_PATH } from "./lib/consts";
|
||||
import yaml from "js-yaml";
|
||||
import { z } from "zod";
|
||||
|
||||
const dev = process.env.ENVIRONMENT !== "prod";
|
||||
const externalPort = config.getRawConfig().server.integration_port;
|
||||
@@ -39,24 +38,12 @@ export function createIntegrationApiServer() {
|
||||
apiServer.use(cookieParser());
|
||||
apiServer.use(express.json());
|
||||
|
||||
const openApiDocumentation = getOpenApiDocumentation();
|
||||
|
||||
apiServer.use(
|
||||
"/v1/docs",
|
||||
swaggerUi.serve,
|
||||
swaggerUi.setup(openApiDocumentation)
|
||||
swaggerUi.setup(getOpenApiDocumentation())
|
||||
);
|
||||
|
||||
// Unauthenticated OpenAPI spec endpoints
|
||||
apiServer.get("/v1/openapi.json", (_req, res) => {
|
||||
res.json(openApiDocumentation);
|
||||
});
|
||||
|
||||
apiServer.get("/v1/openapi.yaml", (_req, res) => {
|
||||
const yamlOutput = yaml.dump(openApiDocumentation);
|
||||
res.type("application/yaml").send(yamlOutput);
|
||||
});
|
||||
|
||||
// API routes
|
||||
const prefix = `/v1`;
|
||||
apiServer.use(logIncomingMiddleware);
|
||||
@@ -88,6 +75,16 @@ function getOpenApiDocumentation() {
|
||||
}
|
||||
);
|
||||
|
||||
for (const def of registry.definitions) {
|
||||
if (def.type === "route") {
|
||||
def.route.security = [
|
||||
{
|
||||
[bearerAuth.name]: []
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/",
|
||||
@@ -97,74 +94,6 @@ function getOpenApiDocumentation() {
|
||||
responses: {}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/openapi.json",
|
||||
description: "Get OpenAPI specification as JSON",
|
||||
tags: [],
|
||||
request: {},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "OpenAPI specification as JSON",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/openapi.yaml",
|
||||
description: "Get OpenAPI specification as YAML",
|
||||
tags: [],
|
||||
request: {},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "OpenAPI specification as YAML",
|
||||
content: {
|
||||
"application/yaml": {
|
||||
schema: {
|
||||
type: "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const def of registry.definitions) {
|
||||
if (def.type === "route") {
|
||||
def.route.security = [
|
||||
{
|
||||
[bearerAuth.name]: []
|
||||
}
|
||||
];
|
||||
|
||||
// Ensure every route has a generic JSON response schema so Swagger UI can render responses
|
||||
const existingResponses = def.route.responses;
|
||||
const hasExistingResponses =
|
||||
existingResponses && Object.keys(existingResponses).length > 0;
|
||||
|
||||
if (!hasExistingResponses) {
|
||||
def.route.responses = {
|
||||
"*": {
|
||||
description: "",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({})
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const generator = new OpenApiGeneratorV3(registry.definitions);
|
||||
|
||||
const generated = generator.generateDocument({
|
||||
@@ -176,13 +105,11 @@ function getOpenApiDocumentation() {
|
||||
servers: [{ url: "/v1" }]
|
||||
});
|
||||
|
||||
if (!process.env.DISABLE_GEN_OPENAPI) {
|
||||
// convert to yaml and save to file
|
||||
const outputPath = path.join(APP_PATH, "openapi.yaml");
|
||||
const yamlOutput = yaml.dump(generated);
|
||||
fs.writeFileSync(outputPath, yamlOutput, "utf8");
|
||||
logger.info(`OpenAPI documentation saved to ${outputPath}`);
|
||||
}
|
||||
// convert to yaml and save to file
|
||||
const outputPath = path.join(APP_PATH, "openapi.yaml");
|
||||
const yamlOutput = yaml.dump(generated);
|
||||
fs.writeFileSync(outputPath, yamlOutput, "utf8");
|
||||
logger.info(`OpenAPI documentation saved to ${outputPath}`);
|
||||
|
||||
return generated;
|
||||
}
|
||||
|
||||
@@ -16,11 +16,6 @@ const internalPort = config.getRawConfig().server.internal_port;
|
||||
export function createInternalServer() {
|
||||
const internalServer = express();
|
||||
|
||||
const trustProxy = config.getRawConfig().server.trust_proxy;
|
||||
if (trustProxy) {
|
||||
internalServer.set("trust proxy", trustProxy);
|
||||
}
|
||||
|
||||
internalServer.use(helmet());
|
||||
internalServer.use(cors());
|
||||
internalServer.use(stripDuplicateSesions);
|
||||
|
||||
@@ -1,44 +1,30 @@
|
||||
import Stripe from "stripe";
|
||||
|
||||
export enum FeatureId {
|
||||
SITE_UPTIME = "siteUptime",
|
||||
USERS = "users",
|
||||
SITES = "sites",
|
||||
EGRESS_DATA_MB = "egressDataMb",
|
||||
DOMAINS = "domains",
|
||||
REMOTE_EXIT_NODES = "remoteExitNodes",
|
||||
ORGINIZATIONS = "organizations",
|
||||
TIER1 = "tier1"
|
||||
REMOTE_EXIT_NODES = "remoteExitNodes"
|
||||
}
|
||||
|
||||
export async function getFeatureDisplayName(featureId: FeatureId): Promise<string> {
|
||||
switch (featureId) {
|
||||
case FeatureId.USERS:
|
||||
return "Users";
|
||||
case FeatureId.SITES:
|
||||
return "Sites";
|
||||
case FeatureId.EGRESS_DATA_MB:
|
||||
return "Egress Data (MB)";
|
||||
case FeatureId.DOMAINS:
|
||||
return "Domains";
|
||||
case FeatureId.REMOTE_EXIT_NODES:
|
||||
return "Remote Exit Nodes";
|
||||
case FeatureId.ORGINIZATIONS:
|
||||
return "Organizations";
|
||||
case FeatureId.TIER1:
|
||||
return "Home Lab";
|
||||
default:
|
||||
return featureId;
|
||||
}
|
||||
}
|
||||
|
||||
// this is from the old system
|
||||
export const FeatureMeterIds: Partial<Record<FeatureId, string>> = { // right now we are not charging for any data
|
||||
// [FeatureId.EGRESS_DATA_MB]: "mtr_61Srreh9eWrExDSCe41D3Ee2Ir7Wm5YW"
|
||||
export const FeatureMeterIds: Record<FeatureId, string> = {
|
||||
[FeatureId.SITE_UPTIME]: "mtr_61Srrej5wUJuiTWgo41D3Ee2Ir7WmDLU",
|
||||
[FeatureId.USERS]: "mtr_61SrreISyIWpwUNGR41D3Ee2Ir7WmQro",
|
||||
[FeatureId.EGRESS_DATA_MB]: "mtr_61Srreh9eWrExDSCe41D3Ee2Ir7Wm5YW",
|
||||
[FeatureId.DOMAINS]: "mtr_61Ss9nIKDNMw0LDRU41D3Ee2Ir7WmRPU",
|
||||
[FeatureId.REMOTE_EXIT_NODES]: "mtr_61T86UXnfxTVXy9sD41D3Ee2Ir7WmFTE"
|
||||
};
|
||||
|
||||
export const FeatureMeterIdsSandbox: Partial<Record<FeatureId, string>> = {
|
||||
// [FeatureId.EGRESS_DATA_MB]: "mtr_test_61Snh2a2m6qome5Kv41DCpkOb237B3dQ"
|
||||
export const FeatureMeterIdsSandbox: Record<FeatureId, string> = {
|
||||
[FeatureId.SITE_UPTIME]: "mtr_test_61Snh3cees4w60gv841DCpkOb237BDEu",
|
||||
[FeatureId.USERS]: "mtr_test_61Sn5fLtq1gSfRkyA41DCpkOb237B6au",
|
||||
[FeatureId.EGRESS_DATA_MB]: "mtr_test_61Snh2a2m6qome5Kv41DCpkOb237B3dQ",
|
||||
[FeatureId.DOMAINS]: "mtr_test_61SsA8qrdAlgPpFRQ41DCpkOb237BGts",
|
||||
[FeatureId.REMOTE_EXIT_NODES]: "mtr_test_61T86Vqmwa3D9ra3341DCpkOb237B94K"
|
||||
};
|
||||
|
||||
export function getFeatureMeterId(featureId: FeatureId): string | undefined {
|
||||
export function getFeatureMeterId(featureId: FeatureId): string {
|
||||
if (
|
||||
process.env.ENVIRONMENT == "prod" &&
|
||||
process.env.SANDBOX_MODE !== "true"
|
||||
@@ -57,81 +43,45 @@ export function getFeatureIdByMetricId(
|
||||
)?.[0];
|
||||
}
|
||||
|
||||
export type FeaturePriceSet = Partial<Record<FeatureId, string>>;
|
||||
|
||||
export const tier1FeaturePriceSet: FeaturePriceSet = {
|
||||
[FeatureId.TIER1]: "price_1SzVE3D3Ee2Ir7Wm6wT5Dl3G"
|
||||
export type FeaturePriceSet = {
|
||||
[key in Exclude<FeatureId, FeatureId.DOMAINS>]: string;
|
||||
} & {
|
||||
[FeatureId.DOMAINS]?: string; // Optional since domains are not billed
|
||||
};
|
||||
|
||||
export const tier1FeaturePriceSetSandbox: FeaturePriceSet = {
|
||||
[FeatureId.TIER1]: "price_1SxgpPDCpkOb237Bfo4rIsoT"
|
||||
export const standardFeaturePriceSet: FeaturePriceSet = {
|
||||
// Free tier matches the freeLimitSet
|
||||
[FeatureId.SITE_UPTIME]: "price_1RrQc4D3Ee2Ir7WmaJGZ3MtF",
|
||||
[FeatureId.USERS]: "price_1RrQeJD3Ee2Ir7WmgveP3xea",
|
||||
[FeatureId.EGRESS_DATA_MB]: "price_1RrQXFD3Ee2Ir7WmvGDlgxQk",
|
||||
// [FeatureId.DOMAINS]: "price_1Rz3tMD3Ee2Ir7Wm5qLeASzC",
|
||||
[FeatureId.REMOTE_EXIT_NODES]: "price_1S46weD3Ee2Ir7Wm94KEHI4h"
|
||||
};
|
||||
|
||||
export function getTier1FeaturePriceSet(): FeaturePriceSet {
|
||||
export const standardFeaturePriceSetSandbox: FeaturePriceSet = {
|
||||
// Free tier matches the freeLimitSet
|
||||
[FeatureId.SITE_UPTIME]: "price_1RefFBDCpkOb237BPrKZ8IEU",
|
||||
[FeatureId.USERS]: "price_1ReNa4DCpkOb237Bc67G5muF",
|
||||
[FeatureId.EGRESS_DATA_MB]: "price_1Rfp9LDCpkOb237BwuN5Oiu0",
|
||||
// [FeatureId.DOMAINS]: "price_1Ryi88DCpkOb237B2D6DM80b",
|
||||
[FeatureId.REMOTE_EXIT_NODES]: "price_1RyiZvDCpkOb237BXpmoIYJL"
|
||||
};
|
||||
|
||||
export function getStandardFeaturePriceSet(): FeaturePriceSet {
|
||||
if (
|
||||
process.env.ENVIRONMENT == "prod" &&
|
||||
process.env.SANDBOX_MODE !== "true"
|
||||
) {
|
||||
return tier1FeaturePriceSet;
|
||||
return standardFeaturePriceSet;
|
||||
} else {
|
||||
return tier1FeaturePriceSetSandbox;
|
||||
return standardFeaturePriceSetSandbox;
|
||||
}
|
||||
}
|
||||
|
||||
export const tier2FeaturePriceSet: FeaturePriceSet = {
|
||||
[FeatureId.USERS]: "price_1SzVCcD3Ee2Ir7Wmn6U3KvPN"
|
||||
};
|
||||
|
||||
export const tier2FeaturePriceSetSandbox: FeaturePriceSet = {
|
||||
[FeatureId.USERS]: "price_1SxaEHDCpkOb237BD9lBkPiR"
|
||||
};
|
||||
|
||||
export function getTier2FeaturePriceSet(): FeaturePriceSet {
|
||||
if (
|
||||
process.env.ENVIRONMENT == "prod" &&
|
||||
process.env.SANDBOX_MODE !== "true"
|
||||
) {
|
||||
return tier2FeaturePriceSet;
|
||||
} else {
|
||||
return tier2FeaturePriceSetSandbox;
|
||||
}
|
||||
}
|
||||
|
||||
export const tier3FeaturePriceSet: FeaturePriceSet = {
|
||||
[FeatureId.USERS]: "price_1SzVDKD3Ee2Ir7WmPtOKNusv"
|
||||
};
|
||||
|
||||
export const tier3FeaturePriceSetSandbox: FeaturePriceSet = {
|
||||
[FeatureId.USERS]: "price_1SxaEODCpkOb237BiXdCBSfs"
|
||||
};
|
||||
|
||||
export function getTier3FeaturePriceSet(): FeaturePriceSet {
|
||||
if (
|
||||
process.env.ENVIRONMENT == "prod" &&
|
||||
process.env.SANDBOX_MODE !== "true"
|
||||
) {
|
||||
return tier3FeaturePriceSet;
|
||||
} else {
|
||||
return tier3FeaturePriceSetSandbox;
|
||||
}
|
||||
}
|
||||
|
||||
export function getFeatureIdByPriceId(priceId: string): FeatureId | undefined {
|
||||
// Check all feature price sets
|
||||
const allPriceSets = [
|
||||
getTier1FeaturePriceSet(),
|
||||
getTier2FeaturePriceSet(),
|
||||
getTier3FeaturePriceSet()
|
||||
];
|
||||
|
||||
for (const priceSet of allPriceSets) {
|
||||
const entry = (Object.entries(priceSet) as [FeatureId, string][]).find(
|
||||
([_, price]) => price === priceId
|
||||
);
|
||||
if (entry) {
|
||||
return entry[0];
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
export function getLineItems(
|
||||
featurePriceSet: FeaturePriceSet
|
||||
): Stripe.Checkout.SessionCreateParams.LineItem[] {
|
||||
return Object.entries(featurePriceSet).map(([featureId, priceId]) => ({
|
||||
price: priceId
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import Stripe from "stripe";
|
||||
import { FeatureId, FeaturePriceSet } from "./features";
|
||||
import { usageService } from "./usageService";
|
||||
|
||||
export async function getLineItems(
|
||||
featurePriceSet: FeaturePriceSet,
|
||||
orgId: string,
|
||||
): Promise<Stripe.Checkout.SessionCreateParams.LineItem[]> {
|
||||
const users = await usageService.getUsage(orgId, FeatureId.USERS);
|
||||
|
||||
return Object.entries(featurePriceSet).map(([featureId, priceId]) => {
|
||||
let quantity: number | undefined;
|
||||
|
||||
if (featureId === FeatureId.USERS) {
|
||||
quantity = users?.instantaneousValue || 1;
|
||||
} else if (featureId === FeatureId.TIER1) {
|
||||
quantity = 1;
|
||||
}
|
||||
|
||||
return {
|
||||
price: priceId,
|
||||
quantity: quantity
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
export enum LicenseId {
|
||||
SMALL_LICENSE = "small_license",
|
||||
BIG_LICENSE = "big_license"
|
||||
}
|
||||
|
||||
export type LicensePriceSet = {
|
||||
[key in LicenseId]: string;
|
||||
};
|
||||
|
||||
export const licensePriceSet: LicensePriceSet = {
|
||||
// Free license matches the freeLimitSet
|
||||
[LicenseId.SMALL_LICENSE]: "price_1SxKHiD3Ee2Ir7WmvtEh17A8",
|
||||
[LicenseId.BIG_LICENSE]: "price_1SxKHiD3Ee2Ir7WmMUiP0H6Y"
|
||||
};
|
||||
|
||||
export const licensePriceSetSandbox: LicensePriceSet = {
|
||||
// Free license matches the freeLimitSet
|
||||
// when matching license the keys closer to 0 index are matched first so list the licenses in descending order of value
|
||||
[LicenseId.SMALL_LICENSE]: "price_1SxDwuDCpkOb237Bz0yTiOgN",
|
||||
[LicenseId.BIG_LICENSE]: "price_1SxDy0DCpkOb237BWJxrxYkl"
|
||||
};
|
||||
|
||||
export function getLicensePriceSet(
|
||||
environment?: string,
|
||||
sandbox_mode?: boolean
|
||||
): LicensePriceSet {
|
||||
if (
|
||||
(process.env.ENVIRONMENT == "prod" &&
|
||||
process.env.SANDBOX_MODE !== "true") ||
|
||||
(environment === "prod" && sandbox_mode !== true)
|
||||
) {
|
||||
// THIS GETS LOADED CLIENT SIDE AND SERVER SIDE
|
||||
return licensePriceSet;
|
||||
} else {
|
||||
return licensePriceSetSandbox;
|
||||
}
|
||||
}
|
||||
@@ -1,70 +1,50 @@
|
||||
import { FeatureId } from "./features";
|
||||
|
||||
export type LimitSet = Partial<{
|
||||
export type LimitSet = {
|
||||
[key in FeatureId]: {
|
||||
value: number | null; // null indicates no limit
|
||||
description?: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
export const sandboxLimitSet: LimitSet = {
|
||||
[FeatureId.SITE_UPTIME]: { value: 2880, description: "Sandbox limit" }, // 1 site up for 2 days
|
||||
[FeatureId.USERS]: { value: 1, description: "Sandbox limit" },
|
||||
[FeatureId.EGRESS_DATA_MB]: { value: 1000, description: "Sandbox limit" }, // 1 GB
|
||||
[FeatureId.DOMAINS]: { value: 0, description: "Sandbox limit" },
|
||||
[FeatureId.REMOTE_EXIT_NODES]: { value: 0, description: "Sandbox limit" }
|
||||
};
|
||||
|
||||
export const freeLimitSet: LimitSet = {
|
||||
[FeatureId.SITES]: { value: 5, description: "Basic limit" },
|
||||
[FeatureId.USERS]: { value: 5, description: "Basic limit" },
|
||||
[FeatureId.DOMAINS]: { value: 5, description: "Basic limit" },
|
||||
[FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Basic limit" },
|
||||
[FeatureId.ORGINIZATIONS]: { value: 1, description: "Basic limit" },
|
||||
[FeatureId.SITE_UPTIME]: { value: 46080, description: "Free tier limit" }, // 1 site up for 32 days
|
||||
[FeatureId.USERS]: { value: 3, description: "Free tier limit" },
|
||||
[FeatureId.EGRESS_DATA_MB]: {
|
||||
value: 25000,
|
||||
description: "Free tier limit"
|
||||
}, // 25 GB
|
||||
[FeatureId.DOMAINS]: { value: 3, description: "Free tier limit" },
|
||||
[FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Free tier limit" }
|
||||
};
|
||||
|
||||
export const tier1LimitSet: LimitSet = {
|
||||
[FeatureId.USERS]: { value: 7, description: "Home limit" },
|
||||
[FeatureId.SITES]: { value: 10, description: "Home limit" },
|
||||
[FeatureId.DOMAINS]: { value: 10, description: "Home limit" },
|
||||
[FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Home limit" },
|
||||
[FeatureId.ORGINIZATIONS]: { value: 1, description: "Home limit" },
|
||||
};
|
||||
|
||||
export const tier2LimitSet: LimitSet = {
|
||||
export const subscribedLimitSet: LimitSet = {
|
||||
[FeatureId.SITE_UPTIME]: {
|
||||
value: 2232000,
|
||||
description: "Contact us to increase soft limit."
|
||||
}, // 50 sites up for 31 days
|
||||
[FeatureId.USERS]: {
|
||||
value: 100,
|
||||
description: "Team limit"
|
||||
},
|
||||
[FeatureId.SITES]: {
|
||||
value: 50,
|
||||
description: "Team limit"
|
||||
value: 150,
|
||||
description: "Contact us to increase soft limit."
|
||||
},
|
||||
[FeatureId.EGRESS_DATA_MB]: {
|
||||
value: 12000000,
|
||||
description: "Contact us to increase soft limit."
|
||||
}, // 12000 GB
|
||||
[FeatureId.DOMAINS]: {
|
||||
value: 50,
|
||||
description: "Team limit"
|
||||
value: 25,
|
||||
description: "Contact us to increase soft limit."
|
||||
},
|
||||
[FeatureId.REMOTE_EXIT_NODES]: {
|
||||
value: 3,
|
||||
description: "Team limit"
|
||||
},
|
||||
[FeatureId.ORGINIZATIONS]: {
|
||||
value: 1,
|
||||
description: "Team limit"
|
||||
value: 5,
|
||||
description: "Contact us to increase soft limit."
|
||||
}
|
||||
};
|
||||
|
||||
export const tier3LimitSet: LimitSet = {
|
||||
[FeatureId.USERS]: {
|
||||
value: 500,
|
||||
description: "Business limit"
|
||||
},
|
||||
[FeatureId.SITES]: {
|
||||
value: 250,
|
||||
description: "Business limit"
|
||||
},
|
||||
[FeatureId.DOMAINS]: {
|
||||
value: 100,
|
||||
description: "Business limit"
|
||||
},
|
||||
[FeatureId.REMOTE_EXIT_NODES]: {
|
||||
value: 20,
|
||||
description: "Business limit"
|
||||
},
|
||||
[FeatureId.ORGINIZATIONS]: {
|
||||
value: 5,
|
||||
description: "Business limit"
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import { db, limits } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { LimitSet } from "./limitSet";
|
||||
import { FeatureId } from "./features";
|
||||
import logger from "@server/logger";
|
||||
|
||||
class LimitService {
|
||||
async applyLimitSetToOrg(orgId: string, limitSet: LimitSet): Promise<void> {
|
||||
@@ -14,21 +13,6 @@ class LimitService {
|
||||
for (const [featureId, entry] of limitEntries) {
|
||||
const limitId = `${orgId}-${featureId}`;
|
||||
const { value, description } = entry;
|
||||
// get the limit first
|
||||
const [limit] = await trx
|
||||
.select()
|
||||
.from(limits)
|
||||
.where(eq(limits.limitId, limitId))
|
||||
.limit(1);
|
||||
|
||||
// check if its overriden
|
||||
if (limit && limit.override) {
|
||||
logger.debug(
|
||||
`Skipping limit ${limitId} for org ${orgId} since it is overridden...`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
await trx
|
||||
.insert(limits)
|
||||
.values({ limitId, orgId, featureId, value, description });
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Tier } from "@server/types/Tiers";
|
||||
|
||||
export enum TierFeature {
|
||||
OrgOidc = "orgOidc",
|
||||
LoginPageDomain = "loginPageDomain", // handle downgrade by removing custom domain
|
||||
DeviceApprovals = "deviceApprovals", // handle downgrade by disabling device approvals
|
||||
LoginPageBranding = "loginPageBranding", // handle downgrade by setting to default branding
|
||||
LogExport = "logExport",
|
||||
AccessLogs = "accessLogs", // set the retention period to none on downgrade
|
||||
ActionLogs = "actionLogs", // set the retention period to none on downgrade
|
||||
ConnectionLogs = "connectionLogs",
|
||||
RotateCredentials = "rotateCredentials",
|
||||
MaintencePage = "maintencePage", // handle downgrade
|
||||
DevicePosture = "devicePosture",
|
||||
TwoFactorEnforcement = "twoFactorEnforcement", // handle downgrade by setting to optional
|
||||
SessionDurationPolicies = "sessionDurationPolicies", // handle downgrade by setting to default duration
|
||||
PasswordExpirationPolicies = "passwordExpirationPolicies", // handle downgrade by setting to default duration
|
||||
AutoProvisioning = "autoProvisioning", // handle downgrade by disabling auto provisioning
|
||||
SshPam = "sshPam",
|
||||
FullRbac = "fullRbac",
|
||||
SiteProvisioningKeys = "siteProvisioningKeys", // handle downgrade by revoking keys if needed
|
||||
SIEM = "siem" // handle downgrade by disabling SIEM integrations
|
||||
}
|
||||
|
||||
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||
[TierFeature.OrgOidc]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||
[TierFeature.LoginPageDomain]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||
[TierFeature.DeviceApprovals]: ["tier1", "tier3", "enterprise"],
|
||||
[TierFeature.LoginPageBranding]: ["tier1", "tier3", "enterprise"],
|
||||
[TierFeature.LogExport]: ["tier3", "enterprise"],
|
||||
[TierFeature.AccessLogs]: ["tier2", "tier3", "enterprise"],
|
||||
[TierFeature.ActionLogs]: ["tier2", "tier3", "enterprise"],
|
||||
[TierFeature.ConnectionLogs]: ["tier2", "tier3", "enterprise"],
|
||||
[TierFeature.RotateCredentials]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||
[TierFeature.MaintencePage]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||
[TierFeature.DevicePosture]: ["tier2", "tier3", "enterprise"],
|
||||
[TierFeature.TwoFactorEnforcement]: [
|
||||
"tier1",
|
||||
"tier2",
|
||||
"tier3",
|
||||
"enterprise"
|
||||
],
|
||||
[TierFeature.SessionDurationPolicies]: [
|
||||
"tier1",
|
||||
"tier2",
|
||||
"tier3",
|
||||
"enterprise"
|
||||
],
|
||||
[TierFeature.PasswordExpirationPolicies]: [
|
||||
"tier1",
|
||||
"tier2",
|
||||
"tier3",
|
||||
"enterprise"
|
||||
],
|
||||
[TierFeature.AutoProvisioning]: ["tier1", "tier3", "enterprise"],
|
||||
[TierFeature.SshPam]: ["tier1", "tier3", "enterprise"],
|
||||
[TierFeature.FullRbac]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||
[TierFeature.SiteProvisioningKeys]: ["tier3", "enterprise"],
|
||||
[TierFeature.SIEM]: ["enterprise"]
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user