Compare commits

..

28 Commits

Author SHA1 Message Date
Owen Schwartz 5f89a72f9b New translations en-us.json (Norwegian Bokmal) 2026-02-11 20:36:31 -08:00
Owen Schwartz 1e168260f5 New translations en-us.json (Chinese Simplified) 2026-02-11 20:36:30 -08:00
Owen Schwartz 3f53b128e8 New translations en-us.json (Turkish) 2026-02-11 20:36:28 -08:00
Owen Schwartz 924bdf098b New translations en-us.json (Russian) 2026-02-11 20:36:27 -08:00
Owen Schwartz 662f258c8a New translations en-us.json (Portuguese) 2026-02-11 20:36:25 -08:00
Owen Schwartz 36f859947c New translations en-us.json (Polish) 2026-02-11 20:36:24 -08:00
Owen Schwartz 47d6df2649 New translations en-us.json (Dutch) 2026-02-11 20:36:23 -08:00
Owen Schwartz 07f2f5b3c5 New translations en-us.json (Korean) 2026-02-11 20:36:21 -08:00
Owen Schwartz 89784ab2d1 New translations en-us.json (Italian) 2026-02-11 20:36:20 -08:00
Owen Schwartz a7b1b3802f New translations en-us.json (German) 2026-02-11 20:36:18 -08:00
Owen Schwartz c7c4f9501e New translations en-us.json (Czech) 2026-02-11 20:36:17 -08:00
Owen Schwartz e5d1390e95 New translations en-us.json (Bulgarian) 2026-02-11 20:36:15 -08:00
Owen Schwartz 5ce0fc5c29 New translations en-us.json (Spanish) 2026-02-11 20:36:14 -08:00
Owen Schwartz b1c359fcba New translations en-us.json (French) 2026-02-11 20:36:12 -08:00
Owen Schwartz c28c003d3f New translations en-us.json (Norwegian Bokmal) 2026-02-11 19:03:11 -08:00
Owen Schwartz 48963ab924 New translations en-us.json (Chinese Simplified) 2026-02-11 19:03:09 -08:00
Owen Schwartz cfd6c04f7e New translations en-us.json (Turkish) 2026-02-11 19:03:08 -08:00
Owen Schwartz 316e85c77c New translations en-us.json (Russian) 2026-02-11 19:03:06 -08:00
Owen Schwartz 4e9bf58ccf New translations en-us.json (Portuguese) 2026-02-11 19:03:04 -08:00
Owen Schwartz dd044a8ee9 New translations en-us.json (Polish) 2026-02-11 19:03:03 -08:00
Owen Schwartz 16a2bdc8a5 New translations en-us.json (Dutch) 2026-02-11 19:03:02 -08:00
Owen Schwartz 8925253de3 New translations en-us.json (Korean) 2026-02-11 19:03:00 -08:00
Owen Schwartz 904dca3423 New translations en-us.json (Italian) 2026-02-11 19:02:58 -08:00
Owen Schwartz 8c1ee827e6 New translations en-us.json (German) 2026-02-11 19:02:57 -08:00
Owen Schwartz 37110154e8 New translations en-us.json (Czech) 2026-02-11 19:02:56 -08:00
Owen Schwartz bbf36947ea New translations en-us.json (Bulgarian) 2026-02-11 19:02:54 -08:00
Owen Schwartz 5afbe934a9 New translations en-us.json (Spanish) 2026-02-11 19:02:53 -08:00
Owen Schwartz 8b1e5fa574 New translations en-us.json (French) 2026-02-11 19:02:51 -08:00
914 changed files with 24005 additions and 77998 deletions
-5
View File
@@ -1,5 +0,0 @@
---
alwaysApply: true
---
Always localize strings and use the `t` function to convert keys to strings. Add the keys to the en-us.json file. Never edit the other language files, as en-us.json is the single source of truth.
-7
View File
@@ -1,7 +0,0 @@
---
description:
alwaysApply: true
---
Proxy resources = public resources
Private resources = client resources = site resources
+1 -3
View File
@@ -28,9 +28,7 @@ LICENSE
CONTRIBUTING.md CONTRIBUTING.md
dist dist
.git .git
server/migrations/ migrations/
config/ config/
build.ts build.ts
tsconfig.json tsconfig.json
Dockerfile*
drizzle.config.ts
-1
View File
@@ -1 +0,0 @@
* @oschwartz10612 @miloschwartz
+122 -58
View File
@@ -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. # 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. # 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 permissions: write-all
steps: steps:
- name: Configure AWS credentials - name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6 uses: aws-actions/configure-aws-credentials@v5
with: with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }} role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600 role-duration-seconds: 3600
@@ -62,7 +62,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Monitor storage space - name: Monitor storage space
run: | run: |
@@ -77,7 +77,7 @@ jobs:
fi fi
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -134,7 +134,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Monitor storage space - name: Monitor storage space
run: | run: |
@@ -149,7 +149,7 @@ jobs:
fi fi
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -201,10 +201,10 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -256,7 +256,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Extract tag name - name: Extract tag name
id: get-tag id: get-tag
@@ -264,9 +264,9 @@ jobs:
shell: bash shell: bash
- name: Install Go - name: Install Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with: with:
go-version: 1.25 go-version: 1.24
- name: Update version in package.json - name: Update version in package.json
run: | run: |
@@ -289,17 +289,25 @@ jobs:
echo "LATEST_BADGER_TAG=$LATEST_TAG" >> $GITHUB_ENV echo "LATEST_BADGER_TAG=$LATEST_TAG" >> $GITHUB_ENV
shell: bash 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 - name: Build installer
working-directory: install working-directory: install
run: | run: |
make go-build-release \ make go-build-release
PANGOLIN_VERSION=${{ env.TAG }} \
GERBIL_VERSION=${{ env.LATEST_GERBIL_TAG }} \
BADGER_VERSION=${{ env.LATEST_BADGER_TAG }}
shell: bash
- name: Upload artifacts from /install/bin - name: Upload artifacts from /install/bin
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with: with:
name: install-bin name: install-bin
path: install/bin/ path: install/bin/
@@ -407,25 +415,31 @@ jobs:
shell: bash shell: bash
- name: Login to GitHub Container Registry (for cosign) - name: Login to GitHub Container Registry (for cosign)
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Install cosign - name: Install cosign
# cosign is used to sign container images using keyless (OIDC) signing # 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: Sign (GHCR, keyless) - name: Dual-sign and verify (GHCR & Docker Hub)
# Sign each GHCR image by digest using keyless (OIDC) signing via Sigstore/Rekor. # Sign each image by digest using keyless (OIDC) and key-based signing,
# Signatures are stored in the registry alongside the image. # then verify both the public key signature and the keyless OIDC signature.
env: env:
TAG: ${{ env.TAG }} TAG: ${{ env.TAG }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }}
COSIGN_YES: "true" COSIGN_YES: "true"
run: | run: |
set -euo pipefail set -euo pipefail
issuer="https://token.actions.githubusercontent.com"
id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
# Determine if this is an RC release # Determine if this is an RC release
IS_RC="false" IS_RC="false"
if [[ "$TAG" == *"-rc."* ]]; then if [[ "$TAG" == *"-rc."* ]]; then
@@ -453,48 +467,98 @@ jobs:
) )
fi fi
FAILED_TAGS=() # Sign each image variant for both registries
SUCCESSFUL_TAGS=() for BASE_IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
for IMAGE_TAG in "${IMAGE_TAGS[@]}"; do
echo "Processing ${BASE_IMAGE}:${IMAGE_TAG}"
for IMAGE_TAG in "${IMAGE_TAGS[@]}"; do DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${IMAGE_TAG} | jq -r '.Digest')"
echo "Processing ${GHCR_IMAGE}:${IMAGE_TAG}" REF="${BASE_IMAGE}@${DIGEST}"
TAG_FAILED=false
(
set -e
DIGEST="$(skopeo inspect --retry-times 3 docker://${GHCR_IMAGE}:${IMAGE_TAG} | jq -r '.Digest')"
REF="${GHCR_IMAGE}@${DIGEST}"
echo "Resolved digest: ${REF}" echo "Resolved digest: ${REF}"
echo "==> cosign sign (keyless) --recursive ${REF}" echo "==> cosign sign (keyless) --recursive ${REF}"
cosign sign --recursive "${REF}" cosign sign --recursive "${REF}"
) || TAG_FAILED=true
if [ "$TAG_FAILED" = "true" ]; then echo "==> cosign sign (key) --recursive ${REF}"
echo "⚠️ WARNING: Failed to sign ${GHCR_IMAGE}:${IMAGE_TAG}" cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
FAILED_TAGS+=("${GHCR_IMAGE}:${IMAGE_TAG}")
else # Retry wrapper for verification to handle registry propagation delays
echo "✓ Successfully signed ${GHCR_IMAGE}:${IMAGE_TAG}" retry_verify() {
SUCCESSFUL_TAGS+=("${GHCR_IMAGE}:${IMAGE_TAG}") local cmd="$1"
fi 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
# If index verification fails, attempt to verify child platform manifests
if [ "${VERIFIED_INDEX}" != "true" ] || [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then
echo "Index verification not available; attempting child manifest verification for ${BASE_IMAGE}:${IMAGE_TAG}"
CHILD_VERIFIED=false
for ARCH in arm64 amd64; do
CHILD_TAG="${IMAGE_TAG}-${ARCH}"
echo "Resolving child digest for ${BASE_IMAGE}:${CHILD_TAG}"
CHILD_DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${CHILD_TAG} | jq -r '.Digest' || true)"
if [ -n "${CHILD_DIGEST}" ] && [ "${CHILD_DIGEST}" != "null" ]; then
CHILD_REF="${BASE_IMAGE}@${CHILD_DIGEST}"
echo "==> cosign verify (public key) child ${CHILD_REF}"
if retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${CHILD_REF}' -o text"; then
CHILD_VERIFIED=true
echo "Public key verification succeeded for child ${CHILD_REF}"
else
echo "Public key verification failed for child ${CHILD_REF}"
fi
echo "==> cosign verify (keyless policy) child ${CHILD_REF}"
if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${CHILD_REF}' -o text"; then
CHILD_VERIFIED=true
echo "Keyless verification succeeded for child ${CHILD_REF}"
else
echo "Keyless verification failed for child ${CHILD_REF}"
fi
else
echo "No child digest found for ${BASE_IMAGE}:${CHILD_TAG}; skipping"
fi
done
if [ "${CHILD_VERIFIED}" != "true" ]; then
echo "Failed to verify index and no child manifests verified for ${BASE_IMAGE}:${IMAGE_TAG}"
exit 10
fi
fi
echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}"
done
done done
echo "" echo "All images signed and verified successfully!"
echo "=========================================="
echo "Sign Summary"
echo "=========================================="
echo "Successful: ${#SUCCESSFUL_TAGS[@]}"
echo "Failed: ${#FAILED_TAGS[@]}"
if [ ${#FAILED_TAGS[@]} -gt 0 ]; then
echo "Failed tags:"
for tag in "${FAILED_TAGS[@]}"; do
echo " - $tag"
done
echo "⚠️ WARNING: Some tags failed to sign, but continuing anyway"
else
echo "✓ All images signed successfully!"
fi
shell: bash shell: bash
post-run: post-run:
@@ -512,7 +576,7 @@ jobs:
permissions: write-all permissions: write-all
steps: steps:
- name: Configure AWS credentials - name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6 uses: aws-actions/configure-aws-credentials@v5
with: with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }} role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600 role-duration-seconds: 3600
+426
View File
@@ -0,0 +1,426 @@
name: CI/CD Pipeline
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
permissions:
contents: read
packages: write # for GHCR push
id-token: write # for Cosign Keyless (OIDC) Signing
# Required secrets:
# - DOCKER_HUB_USERNAME / DOCKER_HUB_ACCESS_TOKEN: push to Docker Hub
# - GITHUB_TOKEN: used for GHCR login and OIDC keyless signing
# - COSIGN_PRIVATE_KEY / COSIGN_PASSWORD / COSIGN_PUBLIC_KEY: for key-based signing
on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+"
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
pre-run:
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
aws-region: ${{ secrets.AWS_REGION }}
- name: Verify AWS identity
run: aws sts get-caller-identity
- name: Start EC2 instances
run: |
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
echo "EC2 instances started"
release-arm:
name: Build and Release (ARM64)
runs-on: [self-hosted, linux, arm64, us-east-1]
needs: [pre-run]
if: >-
${{
needs.pre-run.result == 'success'
}}
# Job-level timeout to avoid runaway or stuck runs
timeout-minutes: 120
env:
# Target images
DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Monitor storage space
run: |
THRESHOLD=75
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
echo "Used space: $USED_SPACE%"
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
echo "Used space is below the threshold of 75% free. Running Docker system prune."
echo y | docker system prune -a
else
echo "Storage space is above the threshold. No action needed."
fi
- name: Log in to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
shell: bash
- name: Update version in package.json
run: |
TAG=${{ env.TAG }}
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
cat server/lib/consts.ts
shell: bash
- name: Check if release candidate
id: check-rc
run: |
TAG=${{ env.TAG }}
if [[ "$TAG" == *"-rc."* ]]; then
echo "IS_RC=true" >> $GITHUB_ENV
else
echo "IS_RC=false" >> $GITHUB_ENV
fi
shell: bash
- name: Build and push Docker images (Docker Hub - ARM64)
run: |
TAG=${{ env.TAG }}
if [ "$IS_RC" = "true" ]; then
make build-rc-arm tag=$TAG
else
make build-release-arm tag=$TAG
fi
echo "Built & pushed ARM64 images to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
shell: bash
release-amd:
name: Build and Release (AMD64)
runs-on: [self-hosted, linux, x64, us-east-1]
needs: [pre-run]
if: >-
${{
needs.pre-run.result == 'success'
}}
# Job-level timeout to avoid runaway or stuck runs
timeout-minutes: 120
env:
# Target images
DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Monitor storage space
run: |
THRESHOLD=75
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
echo "Used space: $USED_SPACE%"
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
echo "Used space is below the threshold of 75% free. Running Docker system prune."
echo y | docker system prune -a
else
echo "Storage space is above the threshold. No action needed."
fi
- name: Log in to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
shell: bash
- name: Update version in package.json
run: |
TAG=${{ env.TAG }}
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
cat server/lib/consts.ts
shell: bash
- name: Check if release candidate
id: check-rc
run: |
TAG=${{ env.TAG }}
if [[ "$TAG" == *"-rc."* ]]; then
echo "IS_RC=true" >> $GITHUB_ENV
else
echo "IS_RC=false" >> $GITHUB_ENV
fi
shell: bash
- name: Build and push Docker images (Docker Hub - AMD64)
run: |
TAG=${{ env.TAG }}
if [ "$IS_RC" = "true" ]; then
make build-rc-amd tag=$TAG
else
make build-release-amd tag=$TAG
fi
echo "Built & pushed AMD64 images to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
shell: bash
create-manifest:
name: Create Multi-Arch Manifests
runs-on: [self-hosted, linux, x64, us-east-1]
needs: [release-arm, release-amd]
if: >-
${{
needs.release-arm.result == 'success' &&
needs.release-amd.result == 'success'
}}
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Log in to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
shell: bash
- name: Check if release candidate
id: check-rc
run: |
TAG=${{ env.TAG }}
if [[ "$TAG" == *"-rc."* ]]; then
echo "IS_RC=true" >> $GITHUB_ENV
else
echo "IS_RC=false" >> $GITHUB_ENV
fi
shell: bash
- name: Create multi-arch manifests
run: |
TAG=${{ env.TAG }}
if [ "$IS_RC" = "true" ]; then
make create-manifests-rc tag=$TAG
else
make create-manifests tag=$TAG
fi
echo "Created multi-arch manifests for tag: ${TAG}"
shell: bash
sign-and-package:
name: Sign and Package
runs-on: [self-hosted, linux, x64, us-east-1]
needs: [release-arm, release-amd, create-manifest]
if: >-
${{
needs.release-arm.result == 'success' &&
needs.release-amd.result == 'success' &&
needs.create-manifest.result == 'success'
}}
# Job-level timeout to avoid runaway or stuck runs
timeout-minutes: 120
env:
# Target images
DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
shell: bash
- name: Install Go
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
with:
go-version: 1.24
- name: Update version in package.json
run: |
TAG=${{ env.TAG }}
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
cat server/lib/consts.ts
shell: bash
- name: Pull latest Gerbil version
id: get-gerbil-tag
run: |
LATEST_TAG=$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name')
echo "LATEST_GERBIL_TAG=$LATEST_TAG" >> $GITHUB_ENV
shell: bash
- name: Pull latest Badger version
id: get-badger-tag
run: |
LATEST_TAG=$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name')
echo "LATEST_BADGER_TAG=$LATEST_TAG" >> $GITHUB_ENV
shell: bash
- name: Update install/main.go
run: |
PANGOLIN_VERSION=${{ env.TAG }}
GERBIL_VERSION=${{ env.LATEST_GERBIL_TAG }}
BADGER_VERSION=${{ env.LATEST_BADGER_TAG }}
sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"$PANGOLIN_VERSION\"/" install/main.go
sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"$GERBIL_VERSION\"/" install/main.go
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$BADGER_VERSION\"/" install/main.go
echo "Updated install/main.go with Pangolin version $PANGOLIN_VERSION, Gerbil version $GERBIL_VERSION, and Badger version $BADGER_VERSION"
cat install/main.go
shell: bash
- name: Build installer
working-directory: install
run: |
make go-build-release
- name: Upload artifacts from /install/bin
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: install-bin
path: install/bin/
- name: Install skopeo + jq
# skopeo: copy/inspect images between registries
# jq: JSON parsing tool used to extract digest values
run: |
sudo apt-get update -y
sudo apt-get install -y skopeo jq
skopeo --version
shell: bash
- name: Login to GHCR
env:
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
run: |
mkdir -p "$(dirname "$REGISTRY_AUTH_FILE")"
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
shell: bash
- name: Copy tag from Docker Hub to GHCR
# Mirror the already-built image (all architectures) to GHCR so we can sign it
# Wait a bit for both architectures to be available in Docker Hub manifest
env:
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
run: |
set -euo pipefail
TAG=${{ env.TAG }}
echo "Waiting for multi-arch manifest to be ready..."
sleep 30
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
skopeo copy --all --retry-times 3 \
docker://$DOCKERHUB_IMAGE:$TAG \
docker://$GHCR_IMAGE:$TAG
shell: bash
- name: Login to GitHub Container Registry (for cosign)
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install cosign
# cosign is used to sign and verify container images (key and keyless)
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Dual-sign and verify (GHCR & Docker Hub)
# Sign each image by digest using keyless (OIDC) and key-based signing,
# then verify both the public key signature and the keyless OIDC signature.
env:
TAG: ${{ env.TAG }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }}
COSIGN_YES: "true"
run: |
set -euo pipefail
issuer="https://token.actions.githubusercontent.com"
id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
for IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
echo "Processing ${IMAGE}:${TAG}"
DIGEST="$(skopeo inspect --retry-times 3 docker://${IMAGE}:${TAG} | jq -r '.Digest')"
REF="${IMAGE}@${DIGEST}"
echo "Resolved digest: ${REF}"
echo "==> cosign sign (keyless) --recursive ${REF}"
cosign sign --recursive "${REF}"
echo "==> cosign sign (key) --recursive ${REF}"
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
echo "==> cosign verify (public key) ${REF}"
cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text
echo "==> cosign verify (keyless policy) ${REF}"
cosign verify \
--certificate-oidc-issuer "${issuer}" \
--certificate-identity-regexp "${id_regex}" \
"${REF}" -o text
done
shell: bash
post-run:
needs: [pre-run, release-arm, release-amd, create-manifest, sign-and-package]
if: >-
${{
always() &&
needs.pre-run.result == 'success' &&
(needs.release-arm.result == 'success' || needs.release-arm.result == 'skipped' || needs.release-arm.result == 'failure') &&
(needs.release-amd.result == 'success' || needs.release-amd.result == 'skipped' || needs.release-amd.result == 'failure') &&
(needs.create-manifest.result == 'success' || needs.create-manifest.result == 'skipped' || needs.create-manifest.result == 'failure') &&
(needs.sign-and-package.result == 'success' || needs.sign-and-package.result == 'skipped' || needs.sign-and-package.result == 'failure')
}}
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
aws-region: ${{ secrets.AWS_REGION }}
- name: Verify AWS identity
run: aws sts get-caller-identity
- name: Stop EC2 instances
run: |
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
echo "EC2 instances stopped"
+2 -2
View File
@@ -21,10 +21,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with: with:
node-version: '24' node-version: '24'
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
skopeo --version skopeo --version
- name: Install cosign - name: Install cosign
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Input check - name: Input check
run: | run: |
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
permissions: write-all permissions: write-all
steps: steps:
- name: Configure AWS credentials - name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6 uses: aws-actions/configure-aws-credentials@v5
with: with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }} role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600 role-duration-seconds: 3600
+5 -40
View File
@@ -1,4 +1,4 @@
name: SAAS Pipeline name: CI/CD Pipeline
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries. # 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. # Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
@@ -23,7 +23,7 @@ jobs:
permissions: write-all permissions: write-all
steps: steps:
- name: Configure AWS credentials - name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6 uses: aws-actions/configure-aws-credentials@v5
with: with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }} role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600 role-duration-seconds: 3600
@@ -54,42 +54,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- 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 - name: Monitor storage space
run: | run: |
@@ -104,7 +69,7 @@ jobs:
fi fi
- name: Configure AWS credentials - name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6 uses: aws-actions/configure-aws-credentials@v5
with: with:
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }} role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600 role-duration-seconds: 3600
@@ -145,7 +110,7 @@ jobs:
permissions: write-all permissions: write-all
steps: steps:
- name: Configure AWS credentials - name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6 uses: aws-actions/configure-aws-credentials@v5
with: with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }} role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600 role-duration-seconds: 3600
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with: with:
days-before-stale: 14 days-before-stale: 14
days-before-close: 14 days-before-close: 14
+5 -5
View File
@@ -14,10 +14,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install Node - name: Install Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with: with:
node-version: '24' node-version: '24'
@@ -37,7 +37,7 @@ jobs:
run: npm run db:generate run: npm run db:generate
- name: Apply database migrations - name: Apply database migrations
run: npm run db:push run: npm run db:sqlite:push
- name: Test with tsc - name: Test with tsc
run: npx tsc --noEmit run: npx tsc --noEmit
@@ -62,7 +62,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Build Docker image sqlite - name: Build Docker image sqlite
run: make dev-build-sqlite run: make dev-build-sqlite
@@ -71,7 +71,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Build Docker image pg - name: Build Docker image pg
run: make dev-build-pg run: make dev-build-pg
-2
View File
@@ -52,5 +52,3 @@ scratch/
tsconfig.json tsconfig.json
hydrateSaas.ts hydrateSaas.ts
CLAUDE.md CLAUDE.md
drizzle.config.ts
server/setup/migrations.ts
+1 -1
View File
@@ -10,7 +10,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"[typescript]": { "[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "vscode.typescript-language-features"
}, },
"[typescriptreact]": { "[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
+38 -55
View File
@@ -1,71 +1,32 @@
# FROM node:24-slim AS base FROM node:24-alpine AS builder
FROM public.ecr.aws/docker/library/node:24-slim AS base
WORKDIR /app 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 BUILD=oss
ARG DATABASE=sqlite ARG DATABASE=sqlite
RUN apk add --no-cache python3 make g++
# COPY package.json package-lock.json ./
COPY package*.json ./
RUN npm ci
COPY . .
RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi && \ RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi && \
npm run set:$DATABASE && \ npm run set:$DATABASE && \
npm run set:$BUILD && \ npm run set:$BUILD && \
npm run db:generate && \ npm run db:$DATABASE:generate && \
npm run build && \ npm run build && \
npm run build:cli && \ npm run build:cli
test -f dist/server.mjs
# Create placeholder files for MaxMind databases to avoid COPY errors # test to make sure the build output is there and error if not
# Real files should be present for saas builds, placeholders for oss builds RUN test -f dist/server.mjs
RUN touch /app/GeoLite2-Country.mmdb /app/GeoLite2-ASN.mmdb
FROM base AS builder # Prune dev dependencies and clean up to prepare for copy to runner
RUN npm prune --omit=dev && npm cache clean --force
RUN npm ci --omit=dev FROM node:24-alpine AS runner
# FROM node:24-slim AS runner
FROM public.ecr.aws/docker/library/node:24-slim AS runner
WORKDIR /app
RUN apt-get update && apt-get install -y curl tzdata && rm -rf /var/lib/apt/lists/*
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 ./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 # OCI Image Labels - Build Args for dynamic values
ARG VERSION="dev" ARG VERSION="dev"
@@ -77,6 +38,28 @@ ARG LICENSE="AGPL-3.0"
ARG IMAGE_TITLE="Pangolin" ARG IMAGE_TITLE="Pangolin"
ARG IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" ARG IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere"
WORKDIR /app
# 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/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/server/migrations ./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
# OCI Image Labels # OCI Image Labels
# https://github.com/opencontainers/image-spec/blob/main/annotations.md # https://github.com/opencontainers/image-spec/blob/main/annotations.md
LABEL org.opencontainers.image.source="https://github.com/fosrl/pangolin" \ LABEL org.opencontainers.image.source="https://github.com/fosrl/pangolin" \
+1 -3
View File
@@ -1,9 +1,7 @@
FROM node:24-alpine FROM node:22-alpine
WORKDIR /app WORKDIR /app
RUN apk add --no-cache python3 make g++
COPY package*.json ./ COPY package*.json ./
# Install dependencies # Install dependencies
+31 -36
View File
@@ -35,53 +35,43 @@
</div> </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"> <p align="center">
<strong> <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> </strong>
</p> </p>
Pangolin is an open-source, identity-based remote access platform built on WireGuard® that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources with NAT traversal, all with granular access controls. Pangolin is an open-source, identity-based remote access platform built on WireGuard that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources, all with zero-trust security and granular access control.
## Installation ## Installation
- Get started for free with [Pangolin Cloud](https://app.pangolin.net/). - Check out the [quick install guide](https://docs.pangolin.net/self-host/quick-install) for how to install and set up Pangolin.
- Or, check out the [quick install guide](https://docs.pangolin.net/self-host/quick-install) for how to self-host Pangolin. - Install from the [DigitalOcean marketplace](https://marketplace.digitalocean.com/apps/pangolin-ce-1?refcode=edf0480eeb81) for a one-click pre-configured installer.
- Install from the [DigitalOcean marketplace](https://marketplace.digitalocean.com/apps/pangolin-ce-1?refcode=edf0480eeb81) for a one-click pre-configured installer.
<img src="public/screenshots/hero.png" alt="Pangolin" width="100%" /> <img src="public/screenshots/hero.png" />
## Deployment Options ## Deployment Options
- **Pangolin Cloud** - Fully managed service - no infrastructure required. | <img width=500 /> | Description |
- **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 making less than \$100K USD gross annual revenue. | **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 ## Key Features
### Connect remote networks with sites and NAT traversal | <img width=500 /> | <img width=500 /> |
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
Pangolin's site connectors provide gateways into networks so you can access any networked resources. Sites use outbound tunnels and intelligent NAT traversal to make networks behind restrictive firewalls available for authorized access without public IPs or open ports. Easily deploy a site as a binary or container on any platform. | **Connect remote networks with sites**<br /><br />Pangolin's lightweight site connectors create secure tunnels from remote networks without requiring public IP addresses or open ports. Sites make any network anywhere available for authorized access. | <img src="public/screenshots/sites.png" width=500 /><tr></tr> |
| **Browser-based reverse proxy access**<br /><br />Expose web applications through identity and context-aware tunneled reverse proxies. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet. Users access applications through any web browser with authentication and granular access control. | <img src="public/clip.gif" width=500 /><tr></tr> |
<img src="public/screenshots/sites.png" alt="Sites" width="100%" /> | **Client-based private resource access**<br /><br />Access private resources like SSH servers, databases, RDP, and entire network ranges through Pangolin clients. Intelligent NAT traversal enables connections even through restrictive firewalls, while DNS aliases provide friendly names and fast connections to resources across all your sites. | <img src="public/screenshots/private-resources.png" width=500 /><tr></tr> |
| **Zero-trust granular access**<br /><br />Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications and services you explicitly define, reducing security risk and attack surface. | <img src="public/screenshots/user-devices.png" width=500 /><tr></tr> |
### Browser-based reverse proxy access
Expose web applications through identity and context-aware tunneled reverse proxies. Users access applications through any web browser with authentication and granular access control without installing a client. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet.
<img src="public/clip.gif" alt="Reverse proxy access" width="100%" />
### Client-based private resource access
Access private resources like SSH servers, databases, RDP, and entire network ranges through Pangolin clients. Intelligent NAT traversal enables connections even through restrictive firewalls, while DNS aliases provide friendly names and fast connections to resources across all your sites. Add redundancy by routing traffic through multiple connectors in your network.
<img src="public/screenshots/private-resources.png" alt="Private resources" width="100%" />
### Give users and roles access to resources
Use Pangolin's built in users or bring your own identity provider and set up role based access control (RBAC). Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications, services, and routes you explicitly define.
<img src="public/screenshots/users.png" alt="Users from identity provider with roles" width="100%" />
## Download Clients ## Download Clients
@@ -95,16 +85,17 @@ Download the Pangolin client for your platform:
## Get Started ## Get Started
### Sign up now
Create a free account at [app.pangolin.net](https://app.pangolin.net) to get started with Pangolin Cloud.
### Check out the docs ### Check out the docs
We encourage everyone to read the full documentation first, which is 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 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. 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 ## 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). 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).
@@ -112,3 +103,7 @@ Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License
## Contributions ## Contributions
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices. Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
---
WireGuard® is a registered trademark of Jason A. Donenfeld.
+1 -1
View File
@@ -3,7 +3,7 @@
If you discover a security vulnerability, please follow the steps below to responsibly disclose it to us: 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. 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. - Description and location of the vulnerability.
- Potential impact of the vulnerability. - Potential impact of the vulnerability.
+17
View File
@@ -0,0 +1,17 @@
meta {
name: Create API Key
type: http
seq: 1
}
put {
url: http://localhost:3000/api/v1/api-key
body: json
auth: inherit
}
body:json {
{
"isRoot": true
}
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: Delete API Key
type: http
seq: 2
}
delete {
url: http://localhost:3000/api/v1/api-key/dm47aacqxxn3ubj
body: none
auth: inherit
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: List API Key Actions
type: http
seq: 6
}
get {
url: http://localhost:3000/api/v1/api-key/ex0izu2c37fjz9x/actions
body: none
auth: inherit
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: List Org API Keys
type: http
seq: 4
}
get {
url: http://localhost:3000/api/v1/org/home-lab/api-keys
body: none
auth: inherit
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: List Root API Keys
type: http
seq: 3
}
get {
url: http://localhost:3000/api/v1/root/api-keys
body: none
auth: inherit
}
+17
View File
@@ -0,0 +1,17 @@
meta {
name: Set API Key Actions
type: http
seq: 5
}
post {
url: http://localhost:3000/api/v1/api-key/ex0izu2c37fjz9x/actions
body: json
auth: inherit
}
body:json {
{
"actionIds": ["listSites"]
}
}
+17
View File
@@ -0,0 +1,17 @@
meta {
name: Set API Key Orgs
type: http
seq: 7
}
post {
url: http://localhost:3000/api/v1/api-key/ex0izu2c37fjz9x/orgs
body: json
auth: inherit
}
body:json {
{
"orgIds": ["home-lab"]
}
}
+3
View File
@@ -0,0 +1,3 @@
meta {
name: API Keys
}
+18
View File
@@ -0,0 +1,18 @@
meta {
name: 2fa-disable
type: http
seq: 6
}
post {
url: http://localhost:3000/api/v1/auth/2fa/disable
body: json
auth: none
}
body:json {
{
"password": "aaaaa-1A",
"code": "377289"
}
}
+17
View File
@@ -0,0 +1,17 @@
meta {
name: 2fa-enable
type: http
seq: 4
}
post {
url: http://localhost:3000/api/v1/auth/2fa/enable
body: json
auth: none
}
body:json {
{
"code": "374138"
}
}
+17
View File
@@ -0,0 +1,17 @@
meta {
name: 2fa-request
type: http
seq: 5
}
post {
url: http://localhost:3000/api/v1/auth/2fa/request
body: json
auth: none
}
body:json {
{
"password": "aaaaa-1A"
}
}
+18
View File
@@ -0,0 +1,18 @@
meta {
name: change-password
type: http
seq: 9
}
post {
url: http://localhost:3000/api/v1/auth/change-password
body: json
auth: none
}
body:json {
{
"oldPassword": "",
"newPassword": ""
}
}
+18
View File
@@ -0,0 +1,18 @@
meta {
name: login
type: http
seq: 1
}
post {
url: http://localhost:3000/api/v1/auth/login
body: json
auth: none
}
body:json {
{
"email": "admin@fosrl.io",
"password": "Password123!"
}
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: logout
type: http
seq: 3
}
post {
url: http://localhost:4000/api/v1/auth/logout
body: none
auth: none
}
+17
View File
@@ -0,0 +1,17 @@
meta {
name: reset-password-request
type: http
seq: 10
}
post {
url: http://localhost:3000/api/v1/auth/reset-password/request
body: json
auth: none
}
body:json {
{
"email": "milo@pangolin.net"
}
}
+19
View File
@@ -0,0 +1,19 @@
meta {
name: reset-password
type: http
seq: 11
}
post {
url: http://localhost:3000/api/v1/auth/reset-password
body: json
auth: none
}
body:json {
{
"token": "3uhsbom72dwdhboctwrtntyd6jrlg4jtf5oaxy4k",
"newPassword": "aaaaa-1A",
"code": "6irqCGR3"
}
}
+18
View File
@@ -0,0 +1,18 @@
meta {
name: signup
type: http
seq: 2
}
put {
url: http://localhost:3000/api/v1/auth/signup
body: json
auth: none
}
body:json {
{
"email": "numbat@pangolin.net",
"password": "Password123!"
}
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: verify-email-request
type: http
seq: 8
}
post {
url: http://localhost:3000/api/v1/auth/verify-email/request
body: none
auth: none
}
+17
View File
@@ -0,0 +1,17 @@
meta {
name: verify-email
type: http
seq: 7
}
post {
url: http://localhost:3000/api/v1/auth/verify-email
body: json
auth: none
}
body:json {
{
"code": "50317187"
}
}
+15
View File
@@ -0,0 +1,15 @@
meta {
name: verify-user
type: http
seq: 4
}
get {
url: http://localhost:3001/api/v1/badger/verify-user?sessionId=mb52273jkb6t3oys2bx6ur5x7rcrkl26c7warg3e
body: none
auth: none
}
params:query {
sessionId: mb52273jkb6t3oys2bx6ur5x7rcrkl26c7warg3e
}
+22
View File
@@ -0,0 +1,22 @@
meta {
name: createClient
type: http
seq: 1
}
put {
url: http://localhost:3000/api/v1/site/1/client
body: json
auth: none
}
body:json {
{
"siteId": 1,
"name": "test",
"type": "olm",
"subnet": "100.90.129.4/30",
"olmId": "029yzunhx6nh3y5",
"secret": "l0ymp075y3d4rccb25l6sqpgar52k09etunui970qq5gj7x6"
}
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: pickClientDefaults
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1/site/1/pick-client-defaults
body: none
auth: none
}
+22
View File
@@ -0,0 +1,22 @@
meta {
name: Create OIDC Provider
type: http
seq: 1
}
put {
url: http://localhost:3000/api/v1/org/home-lab/idp/oidc
body: json
auth: inherit
}
body:json {
{
"clientId": "JJoSvHCZcxnXT2sn6CObj6a21MuKNRXs3kN5wbys",
"clientSecret": "2SlGL2wOGgMEWLI9yUuMAeFxre7qSNJVnXMzyepdNzH1qlxYnC4lKhhQ6a157YQEkYH3vm40KK4RCqbYiF8QIweuPGagPX3oGxEj2exwutoXFfOhtq4hHybQKoFq01Z3",
"authUrl": "http://localhost:9000/application/o/authorize/",
"tokenUrl": "http://localhost:9000/application/o/token/",
"scopes": ["email", "openid", "profile"],
"userIdentifier": "email"
}
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: Generate OIDC URL
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1
body: none
auth: inherit
}
+3
View File
@@ -0,0 +1,3 @@
meta {
name: IDP
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: Traefik Config
type: http
seq: 1
}
get {
url: http://localhost:3001/api/v1/traefik-config
body: none
auth: inherit
}
+3
View File
@@ -0,0 +1,3 @@
meta {
name: Internal
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: Create Newt
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1/newt
body: none
auth: none
}
+18
View File
@@ -0,0 +1,18 @@
meta {
name: Get Token
type: http
seq: 1
}
get {
url: http://localhost:3000/api/v1/auth/newt/get-token
body: json
auth: none
}
body:json {
{
"newtId": "o0d4rdxq3stnz7b",
"secret": "sy7l09fnaesd03iwrfp9m3qf0ryn19g0zf3dqieaazb4k7vk"
}
}
+15
View File
@@ -0,0 +1,15 @@
meta {
name: createOlm
type: http
seq: 1
}
put {
url: http://localhost:3000/api/v1/olm
body: none
auth: inherit
}
settings {
encodeUrl: true
}
+8
View File
@@ -0,0 +1,8 @@
meta {
name: Olm
seq: 15
}
auth {
mode: inherit
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: Check Id
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1/org/checkId
body: none
auth: none
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: listOrgs
type: http
seq: 1
}
get {
url:
body: none
auth: none
}
@@ -0,0 +1,11 @@
meta {
name: createRemoteExitNode
type: http
seq: 1
}
put {
url: http://localhost:4000/api/v1/org/org_i21aifypnlyxur2/remote-exit-node
body: none
auth: none
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: listResourcesByOrg
type: http
seq: 1
}
get {
url:
body: none
auth: none
}
+16
View File
@@ -0,0 +1,16 @@
meta {
name: listResourcesBySite
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1/site/1/resources?limit=10&offset=0
body: none
auth: none
}
params:query {
limit: 10
offset: 0
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: Get Site
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1/org/test/sites/mexican-mole-lizard-windy
body: none
auth: none
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: listSites
type: http
seq: 1
}
get {
url:
body: none
auth: none
}
+16
View File
@@ -0,0 +1,16 @@
meta {
name: listTargets
type: http
seq: 1
}
get {
url: http://localhost:3000/api/v1/resource/web.main.localhost/targets?limit=10&offset=0
body: none
auth: none
}
params:query {
limit: 10
offset: 0
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: Test
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1
body: none
auth: inherit
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: traefik-config
type: http
seq: 1
}
get {
url: http://localhost:3001/api/v1/traefik-config
body: none
auth: none
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: adminListUsers
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1/users
body: none
auth: none
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: adminRemoveUser
type: http
seq: 3
}
delete {
url: http://localhost:3000/api/v1/user/ky5r7ivqs8wc7u4
body: none
auth: none
}
+11
View File
@@ -0,0 +1,11 @@
meta {
name: getUser
type: http
seq: 1
}
get {
url:
body: none
auth: none
}
+13
View File
@@ -0,0 +1,13 @@
{
"version": "1",
"name": "Pangolin",
"type": "collection",
"ignore": [
"node_modules",
".git"
],
"presets": {
"requestType": "http",
"requestUrl": "http://localhost:3000/api/v1"
}
}
-121
View File
@@ -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);
}
}
};
+1 -133
View File
@@ -1,5 +1,5 @@
import { CommandModule } from "yargs"; import { CommandModule } from "yargs";
import { db, idpOidcConfig, licenseKey, certificates, eventStreamingDestinations, alertWebhookActions } from "@server/db"; import { db, idpOidcConfig, licenseKey } from "@server/db";
import { encrypt, decrypt } from "@server/lib/crypto"; import { encrypt, decrypt } from "@server/lib/crypto";
import { configFilePath1, configFilePath2 } from "@server/lib/consts"; import { configFilePath1, configFilePath2 } from "@server/lib/consts";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@@ -129,15 +129,9 @@ export const rotateServerSecret: CommandModule<
console.log("\nReading encrypted data from database..."); console.log("\nReading encrypted data from database...");
const idpConfigs = await db.select().from(idpOidcConfig); const idpConfigs = await db.select().from(idpOidcConfig);
const licenseKeys = await db.select().from(licenseKey); const licenseKeys = await db.select().from(licenseKey);
const certs = await db.select().from(certificates);
const streamingDestinations = await db.select().from(eventStreamingDestinations);
const webhookActions = await db.select().from(alertWebhookActions);
console.log(`Found ${idpConfigs.length} OIDC IdP configuration(s)`); console.log(`Found ${idpConfigs.length} OIDC IdP configuration(s)`);
console.log(`Found ${licenseKeys.length} license key(s)`); console.log(`Found ${licenseKeys.length} license key(s)`);
console.log(`Found ${certs.length} certificate(s)`);
console.log(`Found ${streamingDestinations.length} event streaming destination(s)`);
console.log(`Found ${webhookActions.length} alert webhook action(s)`);
// Prepare all decrypted and re-encrypted values // Prepare all decrypted and re-encrypted values
console.log("\nDecrypting and re-encrypting values..."); console.log("\nDecrypting and re-encrypting values...");
@@ -155,27 +149,8 @@ export const rotateServerSecret: CommandModule<
encryptedInstanceId: string; encryptedInstanceId: string;
}; };
type CertUpdate = {
certId: number;
encryptedCertFile: string | null;
encryptedKeyFile: string | null;
};
type StreamingDestinationUpdate = {
destinationId: number;
encryptedConfig: string;
};
type WebhookActionUpdate = {
webhookActionId: number;
encryptedConfig: string;
};
const idpUpdates: IdpUpdate[] = []; const idpUpdates: IdpUpdate[] = [];
const licenseKeyUpdates: LicenseKeyUpdate[] = []; const licenseKeyUpdates: LicenseKeyUpdate[] = [];
const certUpdates: CertUpdate[] = [];
const streamingDestinationUpdates: StreamingDestinationUpdate[] = [];
const webhookActionUpdates: WebhookActionUpdate[] = [];
// Process idpOidcConfig entries // Process idpOidcConfig entries
for (const idpConfig of idpConfigs) { for (const idpConfig of idpConfigs) {
@@ -242,70 +217,6 @@ export const rotateServerSecret: CommandModule<
} }
} }
// Process certificate entries
for (const cert of certs) {
try {
const encryptedCertFile = cert.certFile
? encrypt(decrypt(cert.certFile, oldSecret), newSecret)
: null;
const encryptedKeyFile = cert.keyFile
? encrypt(decrypt(cert.keyFile, oldSecret), newSecret)
: null;
certUpdates.push({
certId: cert.certId,
encryptedCertFile,
encryptedKeyFile
});
} catch (error) {
console.error(
`Error processing certificate ${cert.certId} (${cert.domain}):`,
error
);
throw error;
}
}
// Process eventStreamingDestinations entries
for (const dest of streamingDestinations) {
try {
const decryptedConfig = decrypt(dest.config, oldSecret);
const encryptedConfig = encrypt(decryptedConfig, newSecret);
streamingDestinationUpdates.push({
destinationId: dest.destinationId,
encryptedConfig
});
} catch (error) {
console.error(
`Error processing event streaming destination ${dest.destinationId}:`,
error
);
throw error;
}
}
// Process alertWebhookActions entries
for (const webhook of webhookActions) {
try {
if (webhook.config == null) continue;
const decryptedConfig = decrypt(webhook.config, oldSecret);
const encryptedConfig = encrypt(decryptedConfig, newSecret);
webhookActionUpdates.push({
webhookActionId: webhook.webhookActionId,
encryptedConfig
});
} catch (error) {
console.error(
`Error processing alert webhook action ${webhook.webhookActionId}:`,
error
);
throw error;
}
}
// Perform all database updates in a single transaction // Perform all database updates in a single transaction
console.log("\nUpdating database in transaction..."); console.log("\nUpdating database in transaction...");
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
@@ -339,50 +250,10 @@ export const rotateServerSecret: CommandModule<
instanceId: update.encryptedInstanceId instanceId: update.encryptedInstanceId
}); });
} }
// Update certificate entries
for (const update of certUpdates) {
await trx
.update(certificates)
.set({
certFile: update.encryptedCertFile,
keyFile: update.encryptedKeyFile
})
.where(eq(certificates.certId, update.certId));
}
// Update event streaming destination entries
for (const update of streamingDestinationUpdates) {
await trx
.update(eventStreamingDestinations)
.set({ config: update.encryptedConfig })
.where(
eq(
eventStreamingDestinations.destinationId,
update.destinationId
)
);
}
// Update alert webhook action entries
for (const update of webhookActionUpdates) {
await trx
.update(alertWebhookActions)
.set({ config: update.encryptedConfig })
.where(
eq(
alertWebhookActions.webhookActionId,
update.webhookActionId
)
);
}
}); });
console.log(`Rotated ${idpUpdates.length} OIDC IdP configuration(s)`); console.log(`Rotated ${idpUpdates.length} OIDC IdP configuration(s)`);
console.log(`Rotated ${licenseKeyUpdates.length} license key(s)`); console.log(`Rotated ${licenseKeyUpdates.length} license key(s)`);
console.log(`Rotated ${certUpdates.length} certificate(s)`);
console.log(`Rotated ${streamingDestinationUpdates.length} event streaming destination(s)`);
console.log(`Rotated ${webhookActionUpdates.length} alert webhook action(s)`);
// Update config file with new secret // Update config file with new secret
console.log("\nUpdating config file..."); console.log("\nUpdating config file...");
@@ -399,9 +270,6 @@ export const rotateServerSecret: CommandModule<
console.log(`\nSummary:`); console.log(`\nSummary:`);
console.log(` - OIDC IdP configurations: ${idpUpdates.length}`); console.log(` - OIDC IdP configurations: ${idpUpdates.length}`);
console.log(` - License keys: ${licenseKeyUpdates.length}`); console.log(` - License keys: ${licenseKeyUpdates.length}`);
console.log(` - Certificates: ${certUpdates.length}`);
console.log(` - Event streaming destinations: ${streamingDestinationUpdates.length}`);
console.log(` - Alert webhook actions: ${webhookActionUpdates.length}`);
console.log( console.log(
`\n IMPORTANT: Restart the server for the new secret to take effect.` `\n IMPORTANT: Restart the server for the new secret to take effect.`
); );
-2
View File
@@ -8,7 +8,6 @@ import { clearExitNodes } from "./commands/clearExitNodes";
import { rotateServerSecret } from "./commands/rotateServerSecret"; import { rotateServerSecret } from "./commands/rotateServerSecret";
import { clearLicenseKeys } from "./commands/clearLicenseKeys"; import { clearLicenseKeys } from "./commands/clearLicenseKeys";
import { deleteClient } from "./commands/deleteClient"; import { deleteClient } from "./commands/deleteClient";
import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
yargs(hideBin(process.argv)) yargs(hideBin(process.argv))
.scriptName("pangctl") .scriptName("pangctl")
@@ -18,6 +17,5 @@ yargs(hideBin(process.argv))
.command(rotateServerSecret) .command(rotateServerSecret)
.command(clearLicenseKeys) .command(clearLicenseKeys)
.command(deleteClient) .command(deleteClient)
.command(generateOrgCaKeys)
.demandCommand() .demandCommand()
.help().argv; .help().argv;
-1
View File
@@ -1 +0,0 @@
*-journal
-6
View File
@@ -4,12 +4,6 @@ services:
image: fosrl/pangolin:latest image: fosrl/pangolin:latest
container_name: pangolin container_name: pangolin
restart: unless-stopped restart: unless-stopped
deploy:
resources:
limits:
memory: 1g
reservations:
memory: 256m
volumes: volumes:
- ./config:/app/config - ./config:/app/config
healthcheck: healthcheck:
+2 -2
View File
@@ -7,8 +7,8 @@ services:
POSTGRES_DB: postgres # Default database name POSTGRES_DB: postgres # Default database name
POSTGRES_USER: postgres # Default user POSTGRES_USER: postgres # Default user
POSTGRES_PASSWORD: password # Default password (change for production!) POSTGRES_PASSWORD: password # Default password (change for production!)
volumes: # volumes:
- ./config/postgres:/var/lib/postgresql/data # - ./config/postgres:/var/lib/postgresql/data
ports: ports:
- "5432:5432" # Map host port 5432 to container port 5432 - "5432:5432" # Map host port 5432 to container port 5432
restart: no restart: no
+14
View File
@@ -0,0 +1,14 @@
import { defineConfig } from "drizzle-kit";
import path from "path";
const schema = [path.join("server", "db", "pg", "schema")];
export default defineConfig({
dialect: "postgresql",
schema: schema,
out: path.join("server", "migrations"),
verbose: true,
dbCredentials: {
url: process.env.DATABASE_URL as string
}
});
+1 -1
View File
@@ -281,7 +281,7 @@ esbuild
}) })
], ],
sourcemap: "inline", sourcemap: "inline",
target: "node24" target: "node22"
}) })
.then((result) => { .then((result) => {
// Check if there were any errors in the build result // Check if there were any errors in the build result
+34 -17
View File
@@ -1,24 +1,41 @@
all: go-build-release all: update-versions go-build-release put-back
dev-all: dev-update-versions dev-build dev-clean
# 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)
go-build-release: 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 -o bin/installer_linux_amd64
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 -o bin/installer_linux_arm64
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/installer_linux_arm64
clean: clean:
rm -f bin/installer_linux_amd64 rm -f bin/installer_linux_amd64
rm -f bin/installer_linux_arm64 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
View File
@@ -99,6 +99,11 @@ func ReadAppConfig(configPath string) (*AppConfigValues, error) {
return values, nil 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 { func copyDockerService(sourceFile, destFile, serviceName string) error {
// Read source file // Read source file
sourceData, err := os.ReadFile(sourceFile) sourceData, err := os.ReadFile(sourceFile)
@@ -113,19 +118,19 @@ func copyDockerService(sourceFile, destFile, serviceName string) error {
} }
// Parse source Docker Compose YAML // Parse source Docker Compose YAML
var sourceCompose map[string]any var sourceCompose map[string]interface{}
if err := yaml.Unmarshal(sourceData, &sourceCompose); err != nil { if err := yaml.Unmarshal(sourceData, &sourceCompose); err != nil {
return fmt.Errorf("error parsing source Docker Compose file: %w", err) return fmt.Errorf("error parsing source Docker Compose file: %w", err)
} }
// Parse destination Docker Compose YAML // Parse destination Docker Compose YAML
var destCompose map[string]any var destCompose map[string]interface{}
if err := yaml.Unmarshal(destData, &destCompose); err != nil { if err := yaml.Unmarshal(destData, &destCompose); err != nil {
return fmt.Errorf("error parsing destination Docker Compose file: %w", err) return fmt.Errorf("error parsing destination Docker Compose file: %w", err)
} }
// Get services section from source // Get services section from source
sourceServices, ok := sourceCompose["services"].(map[string]any) sourceServices, ok := sourceCompose["services"].(map[string]interface{})
if !ok { if !ok {
return fmt.Errorf("services section not found in source file or has invalid format") 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 // Get or create services section in destination
destServices, ok := destCompose["services"].(map[string]any) destServices, ok := destCompose["services"].(map[string]interface{})
if !ok { if !ok {
// If services section doesn't exist, create it // If services section doesn't exist, create it
destServices = make(map[string]any) destServices = make(map[string]interface{})
destCompose["services"] = destServices destCompose["services"] = destServices
} }
@@ -182,21 +187,17 @@ func backupConfig() error {
return nil return nil
} }
func MarshalYAMLWithIndent(data any, indent int) (resp []byte, err error) { func MarshalYAMLWithIndent(data interface{}, indent int) ([]byte, error) {
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
encoder := yaml.NewEncoder(buffer) encoder := yaml.NewEncoder(buffer)
encoder.SetIndent(indent) encoder.SetIndent(indent)
if err := encoder.Encode(data); err != nil { err := encoder.Encode(data)
if err != nil {
return nil, err return nil, err
} }
defer func() { defer encoder.Close()
if cerr := encoder.Close(); cerr != nil && err == nil {
err = cerr
}
}()
return buffer.Bytes(), nil return buffer.Bytes(), nil
} }
@@ -208,7 +209,7 @@ func replaceInFile(filepath, oldStr, newStr string) error {
} }
// Replace the string // 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 // Write the modified content back to the file
err = os.WriteFile(filepath, []byte(newContent), 0644) err = os.WriteFile(filepath, []byte(newContent), 0644)
@@ -227,28 +228,28 @@ func CheckAndAddTraefikLogVolume(composePath string) error {
} }
// Parse YAML into a generic map // Parse YAML into a generic map
var compose map[string]any var compose map[string]interface{}
if err := yaml.Unmarshal(data, &compose); err != nil { if err := yaml.Unmarshal(data, &compose); err != nil {
return fmt.Errorf("error parsing compose file: %w", err) return fmt.Errorf("error parsing compose file: %w", err)
} }
// Get services section // Get services section
services, ok := compose["services"].(map[string]any) services, ok := compose["services"].(map[string]interface{})
if !ok { if !ok {
return fmt.Errorf("services section not found or invalid") return fmt.Errorf("services section not found or invalid")
} }
// Get traefik service // Get traefik service
traefik, ok := services["traefik"].(map[string]any) traefik, ok := services["traefik"].(map[string]interface{})
if !ok { if !ok {
return fmt.Errorf("traefik service not found or invalid") return fmt.Errorf("traefik service not found or invalid")
} }
// Check volumes // Check volumes
logVolume := "./config/traefik/logs:/var/log/traefik" 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 // Check if volume already exists
for _, v := range existingVolumes { for _, v := range existingVolumes {
if v.(string) == logVolume { if v.(string) == logVolume {
@@ -294,13 +295,13 @@ func MergeYAML(baseFile, overlayFile string) error {
} }
// Parse base YAML into a map // Parse base YAML into a map
var baseMap map[string]any var baseMap map[string]interface{}
if err := yaml.Unmarshal(baseContent, &baseMap); err != nil { if err := yaml.Unmarshal(baseContent, &baseMap); err != nil {
return fmt.Errorf("error parsing base YAML: %v", err) return fmt.Errorf("error parsing base YAML: %v", err)
} }
// Parse overlay YAML into a map // Parse overlay YAML into a map
var overlayMap map[string]any var overlayMap map[string]interface{}
if err := yaml.Unmarshal(overlayContent, &overlayMap); err != nil { if err := yaml.Unmarshal(overlayContent, &overlayMap); err != nil {
return fmt.Errorf("error parsing overlay YAML: %v", err) return fmt.Errorf("error parsing overlay YAML: %v", err)
} }
@@ -323,8 +324,8 @@ func MergeYAML(baseFile, overlayFile string) error {
} }
// mergeMap recursively merges two maps // mergeMap recursively merges two maps
func mergeMap(base, overlay map[string]any) map[string]any { func mergeMap(base, overlay map[string]interface{}) map[string]interface{} {
result := make(map[string]any) result := make(map[string]interface{})
// Copy all key-values from base map // Copy all key-values from base map
for k, v := range base { for k, v := range base {
@@ -335,8 +336,8 @@ func mergeMap(base, overlay map[string]any) map[string]any {
for k, v := range overlay { for k, v := range overlay {
// If both maps have the same key and both values are maps, merge recursively // If both maps have the same key and both values are maps, merge recursively
if baseVal, ok := base[k]; ok { if baseVal, ok := base[k]; ok {
if baseMap, isBaseMap := baseVal.(map[string]any); isBaseMap { if baseMap, isBaseMap := baseVal.(map[string]interface{}); isBaseMap {
if overlayMap, isOverlayMap := v.(map[string]any); isOverlayMap { if overlayMap, isOverlayMap := v.(map[string]interface{}); isOverlayMap {
result[k] = mergeMap(baseMap, overlayMap) result[k] = mergeMap(baseMap, overlayMap)
continue continue
} }
@@ -81,19 +81,11 @@ entryPoints:
transport: transport:
respondingTimeouts: respondingTimeouts:
readTimeout: "30m" readTimeout: "30m"
http3:
advertisedPort: 443
http: http:
tls: tls:
certResolver: "letsencrypt" certResolver: "letsencrypt"
middlewares: middlewares:
- crowdsec@file - crowdsec@file
encodedCharacters:
allowEncodedSlash: true
allowEncodedQuestionMark: true
serversTransport: serversTransport:
insecureSkipVerify: true insecureSkipVerify: true
ping:
entryPoint: "web"
+3 -8
View File
@@ -4,12 +4,6 @@ services:
image: docker.io/fosrl/pangolin:{{if .IsEnterprise}}ee-{{end}}{{.PangolinVersion}} image: docker.io/fosrl/pangolin:{{if .IsEnterprise}}ee-{{end}}{{.PangolinVersion}}
container_name: pangolin container_name: pangolin
restart: unless-stopped restart: unless-stopped
deploy:
resources:
limits:
memory: 1g
reservations:
memory: 256m
volumes: volumes:
- ./config:/app/config - ./config:/app/config
healthcheck: healthcheck:
@@ -38,14 +32,15 @@ services:
- 51820:51820/udp - 51820:51820/udp
- 21820:21820/udp - 21820:21820/udp
- 443:443 - 443:443
- 443:443/udp # For http3 QUIC if desired
- 80:80 - 80:80
{{end}} {{end}}
traefik: traefik:
image: docker.io/traefik:v3.6 image: docker.io/traefik:v3.6
container_name: traefik container_name: traefik
restart: unless-stopped 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: ports:
- 443:443 - 443:443
- 80:80 - 80:80
@@ -40,8 +40,6 @@ entryPoints:
transport: transport:
respondingTimeouts: respondingTimeouts:
readTimeout: "30m" readTimeout: "30m"
http3:
advertisedPort: 443
http: http:
tls: tls:
certResolver: "letsencrypt" certResolver: "letsencrypt"
+6 -7
View File
@@ -144,13 +144,12 @@ func installDocker() error {
} }
func startDockerService() error { func startDockerService() error {
switch runtime.GOOS { if runtime.GOOS == "linux" {
case "linux":
cmd := exec.Command("systemctl", "enable", "--now", "docker") cmd := exec.Command("systemctl", "enable", "--now", "docker")
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
return cmd.Run() return cmd.Run()
case "darwin": } else if runtime.GOOS == "darwin" {
// On macOS, Docker is usually started via the Docker Desktop application // On macOS, Docker is usually started via the Docker Desktop application
fmt.Println("Please start Docker Desktop manually on macOS.") fmt.Println("Please start Docker Desktop manually on macOS.")
return nil return nil
@@ -303,7 +302,7 @@ func pullContainers(containerType SupportedContainer) error {
return nil 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. // startContainers starts the containers using the appropriate command.
@@ -326,7 +325,7 @@ func startContainers(containerType SupportedContainer) error {
return nil 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. // stopContainers stops the containers using the appropriate command.
@@ -348,7 +347,7 @@ func stopContainers(containerType SupportedContainer) error {
return nil 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. // restartContainer restarts a specific container using the appropriate command.
@@ -370,5 +369,5 @@ func restartContainer(container string, containerType SupportedContainer) error
return nil return nil
} }
return fmt.Errorf("unsupported container type: %s", containerType) return fmt.Errorf("Unsupported container type: %s", containerType)
} }
+11 -89
View File
@@ -6,13 +6,12 @@ import (
"log" "log"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"strings" "strings"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
func installCrowdsec(config Config, installDir string) error { func installCrowdsec(config Config) error {
if err := stopContainers(config.InstallationContainerType); err != nil { if err := stopContainers(config.InstallationContainerType); err != nil {
return fmt.Errorf("failed to stop containers: %v", err) return fmt.Errorf("failed to stop containers: %v", err)
@@ -28,20 +27,9 @@ func installCrowdsec(config Config, installDir string) error {
os.Exit(1) os.Exit(1)
} }
if err := os.MkdirAll("config/crowdsec/db", 0755); err != nil { os.MkdirAll("config/crowdsec/db", 0755)
fmt.Printf("Error creating config files: %v\n", err) os.MkdirAll("config/crowdsec/acquis.d", 0755)
os.Exit(1) os.MkdirAll("config/traefik/logs", 0755)
}
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)
}
setupTraefikLogRotate(installDir)
if err := copyDockerService("config/crowdsec/docker-compose.yml", "docker-compose.yml", "crowdsec"); err != nil { if err := copyDockerService("config/crowdsec/docker-compose.yml", "docker-compose.yml", "crowdsec"); err != nil {
fmt.Printf("Error copying docker service: %v\n", err) fmt.Printf("Error copying docker service: %v\n", err)
@@ -165,34 +153,34 @@ func CheckAndAddCrowdsecDependency(composePath string) error {
} }
// Parse YAML into a generic map // Parse YAML into a generic map
var compose map[string]any var compose map[string]interface{}
if err := yaml.Unmarshal(data, &compose); err != nil { if err := yaml.Unmarshal(data, &compose); err != nil {
return fmt.Errorf("error parsing compose file: %w", err) return fmt.Errorf("error parsing compose file: %w", err)
} }
// Get services section // Get services section
services, ok := compose["services"].(map[string]any) services, ok := compose["services"].(map[string]interface{})
if !ok { if !ok {
return fmt.Errorf("services section not found or invalid") return fmt.Errorf("services section not found or invalid")
} }
// Get traefik service // Get traefik service
traefik, ok := services["traefik"].(map[string]any) traefik, ok := services["traefik"].(map[string]interface{})
if !ok { if !ok {
return fmt.Errorf("traefik service not found or invalid") return fmt.Errorf("traefik service not found or invalid")
} }
// Get dependencies // Get dependencies
dependsOn, ok := traefik["depends_on"].(map[string]any) dependsOn, ok := traefik["depends_on"].(map[string]interface{})
if ok { if ok {
// Append the new block for crowdsec // Append the new block for crowdsec
dependsOn["crowdsec"] = map[string]any{ dependsOn["crowdsec"] = map[string]interface{}{
"condition": "service_healthy", "condition": "service_healthy",
} }
} else { } else {
// No dependencies exist, create it // No dependencies exist, create it
traefik["depends_on"] = map[string]any{ traefik["depends_on"] = map[string]interface{}{
"crowdsec": map[string]any{ "crowdsec": map[string]interface{}{
"condition": "service_healthy", "condition": "service_healthy",
}, },
} }
@@ -211,69 +199,3 @@ func CheckAndAddCrowdsecDependency(composePath string) error {
fmt.Println("Added dependency of crowdsec to traefik") fmt.Println("Added dependency of crowdsec to traefik")
return nil return nil
} }
// setupTraefikLogRotate writes a logrotate config for the Traefik access log
// that CrowdSec depends on. This is only needed when CrowdSec is installed
// because the default Pangolin install does not enable Traefik access logs.
//
// copytruncate is used so Traefik does not need to be restarted or sent a
// signal after rotation — it keeps writing to the same file descriptor while
// the rotated copy is made and the original is truncated in place.
func setupTraefikLogRotate(installDir string) {
const logrotateDir = "/etc/logrotate.d"
const logrotateFile = "/etc/logrotate.d/pangolin-traefik"
logPath := filepath.Join(installDir, "config/traefik/logs/access.log")
if os.Geteuid() != 0 {
fmt.Println("\n[logrotate] Skipping automatic logrotate setup: not running as root.")
fmt.Println("[logrotate] To prevent unbounded growth of the Traefik access log used by CrowdSec,")
fmt.Println("[logrotate] create the file /etc/logrotate.d/pangolin-traefik manually with:")
printLogrotateConfig(logPath)
return
}
config := fmt.Sprintf(`# Logrotate config for Traefik access logs used by CrowdSec.
# Generated by the Pangolin installer. Safe to edit.
%s {
daily
rotate 7
compress
delaycompress
missingok
notifempty
copytruncate
}
`, logPath)
if err := os.MkdirAll(logrotateDir, 0755); err != nil {
fmt.Printf("[logrotate] Warning: could not create %s: %v\n", logrotateDir, err)
return
}
if err := os.WriteFile(logrotateFile, []byte(config), 0644); err != nil {
fmt.Printf("[logrotate] Warning: could not write %s: %v\n", logrotateFile, err)
fmt.Println("[logrotate] Set it up manually:")
printLogrotateConfig(logPath)
return
}
fmt.Printf("[logrotate] Wrote logrotate config to %s\n", logrotateFile)
fmt.Println("[logrotate] Traefik access logs will be rotated daily, keeping 7 compressed copies.")
}
// printLogrotateConfig prints a logrotate config block to stdout so users can
// set it up manually when the installer cannot write to /etc.
func printLogrotateConfig(logPath string) {
fmt.Printf(`
%s {
daily
rotate 7
compress
delaycompress
missingok
notifempty
copytruncate
}
`, logPath)
}
+3 -31
View File
@@ -1,38 +1,10 @@
module installer module installer
go 1.25.0 go 1.24.0
require ( require (
github.com/charmbracelet/huh v1.0.0 golang.org/x/term v0.39.0
github.com/charmbracelet/lipgloss v1.1.0
golang.org/x/term v0.42.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require golang.org/x/sys v0.40.0 // indirect
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.43.0 // indirect
golang.org/x/text v0.23.0 // indirect
)
+4 -77
View File
@@ -1,80 +1,7 @@
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
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.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+66 -182
View File
@@ -1,208 +1,92 @@
package main package main
import ( import (
"errors" "bufio"
"fmt" "fmt"
"os" "strings"
"strconv" "syscall"
"github.com/charmbracelet/huh"
"golang.org/x/term" "golang.org/x/term"
) )
// pangolinTheme is the custom theme using brand colors func readString(reader *bufio.Reader, prompt string, defaultValue string) string {
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
if defaultValue != "" { if defaultValue != "" {
title = fmt.Sprintf("%s (default: %s)", prompt, defaultValue) fmt.Printf("%s (default: %s): ", prompt, defaultValue)
} else {
fmt.Print(prompt + ": ")
} }
input, _ := reader.ReadString('\n')
input := huh.NewInput(). input = strings.TrimSpace(input)
Title(title). if input == "" {
Value(&value) return defaultValue
// 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
})
} }
return input
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
} }
func readPassword(prompt string) string { func readStringNoDefault(reader *bufio.Reader, prompt string) string {
var value 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 { for {
input := huh.NewInput(). input := readString(reader, prompt+" (yes/no)", defaultStr)
Title(prompt). lower := strings.ToLower(input)
Value(&value). if lower == "yes" {
EchoMode(huh.EchoModePassword). return true
Validate(func(s string) error { } else if lower == "no" {
if s == "" { return false
return fmt.Errorf("password is required") } else {
} fmt.Println("Please enter 'yes' or 'no'.")
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
} }
} }
} }
func readBool(prompt string, defaultValue bool) bool { func readBoolNoDefault(reader *bufio.Reader, prompt string) bool {
var value = defaultValue for {
input := readStringNoDefault(reader, prompt+" (yes/no)")
confirm := huh.NewConfirm(). lower := strings.ToLower(input)
Title(prompt). if lower == "yes" {
Value(&value). return true
Affirmative("Yes"). } else if lower == "no" {
Negative("No") return false
} else {
err := runField(confirm) fmt.Println("Please enter 'yes' or 'no'.")
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 readBoolNoDefault(prompt string) bool { func readInt(reader *bufio.Reader, prompt string, defaultValue int) int {
var value bool input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue))
if input == "" {
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)
}
return defaultValue return defaultValue
} }
value := defaultValue
result, err := strconv.Atoi(value) fmt.Sscanf(input, "%d", &value)
if err != nil { return value
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
} }
+93 -215
View File
@@ -1,35 +1,30 @@
package main package main
import ( import (
"crypto/rand" "bufio"
"embed" "embed"
"encoding/base64"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"crypto/rand"
"encoding/base64"
"net" "net"
"net/http"
"net/url" "net/url"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"strings" "strings"
"text/template" "text/template"
"time" "time"
) )
// Version variables injected at build time via -ldflags // DO NOT EDIT THIS FUNCTION; IT MATCHED BY REGEX IN CICD
var (
pangolinVersion string
gerbilVersion string
badgerVersion string
)
func loadVersions(config *Config) { func loadVersions(config *Config) {
config.PangolinVersion = pangolinVersion config.PangolinVersion = "replaceme"
config.GerbilVersion = gerbilVersion config.GerbilVersion = "replaceme"
config.BadgerVersion = badgerVersion config.BadgerVersion = "replaceme"
} }
//go:embed config/* //go:embed config/*
@@ -87,19 +82,14 @@ func main() {
} }
} }
reader := bufio.NewReader(os.Stdin)
var config Config var config Config
var alreadyInstalled = false 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 // check if there is already a config file
if _, err := os.Stat("config/config.yml"); err != nil { if _, err := os.Stat("config/config.yml"); err != nil {
config = collectUserInput() config = collectUserInput(reader)
loadVersions(&config) loadVersions(&config)
config.DoCrowdsecInstall = false config.DoCrowdsecInstall = false
@@ -112,10 +102,7 @@ func main() {
os.Exit(1) os.Exit(1)
} }
if err := moveFile("config/docker-compose.yml", "docker-compose.yml"); err != nil { moveFile("config/docker-compose.yml", "docker-compose.yml")
fmt.Printf("Error moving docker-compose.yml: %v\n", err)
os.Exit(1)
}
fmt.Println("\nConfiguration files created successfully!") fmt.Println("\nConfiguration files created successfully!")
@@ -130,17 +117,13 @@ func main() {
fmt.Println("\n=== Starting installation ===") 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 !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker {
if readBool("Docker is not installed. Would you like to install it?", true) { if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
if err := installDocker(); err != nil { installDocker()
fmt.Printf("Error installing Docker: %v\n", err)
return
}
// try to start docker service but ignore errors // try to start docker service but ignore errors
if err := startDockerService(); err != nil { if err := startDockerService(); err != nil {
fmt.Println("Error starting Docker service:", err) fmt.Println("Error starting Docker service:", err)
@@ -149,7 +132,7 @@ func main() {
} }
// wait 10 seconds for docker to start checking if docker is running every 2 seconds // wait 10 seconds for docker to start checking if docker is running every 2 seconds
fmt.Println("Waiting for Docker to start...") fmt.Println("Waiting for Docker to start...")
for range 5 { for i := 0; i < 5; i++ {
if isDockerRunning() { if isDockerRunning() {
fmt.Println("Docker is running!") fmt.Println("Docker is running!")
break break
@@ -184,7 +167,7 @@ func main() {
fmt.Println("\n=== MaxMind Database Update ===") fmt.Println("\n=== MaxMind Database Update ===")
if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil { if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil {
fmt.Println("MaxMind GeoLite2 Country database found.") 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 { if err := downloadMaxMindDatabase(); err != nil {
fmt.Printf("Error updating MaxMind database: %v\n", err) fmt.Printf("Error updating MaxMind database: %v\n", err)
fmt.Println("You can try updating it manually later if needed.") fmt.Println("You can try updating it manually later if needed.")
@@ -192,7 +175,7 @@ func main() {
} }
} else { } else {
fmt.Println("MaxMind GeoLite2 Country database not found.") 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 { if err := downloadMaxMindDatabase(); err != nil {
fmt.Printf("Error downloading MaxMind database: %v\n", err) fmt.Printf("Error downloading MaxMind database: %v\n", err)
fmt.Println("You can try downloading it manually later if needed.") fmt.Println("You can try downloading it manually later if needed.")
@@ -209,11 +192,11 @@ func main() {
if !checkIsCrowdsecInstalledInCompose() { if !checkIsCrowdsecInstalledInCompose() {
fmt.Println("\n=== CrowdSec Install ===") fmt.Println("\n=== CrowdSec Install ===")
// check if crowdsec is installed // 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.") 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. // 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 == "" { if config.DashboardDomain == "" {
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml") traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml")
if err != nil { if err != nil {
@@ -242,8 +225,8 @@ func main() {
fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail) fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail)
fmt.Printf("Badger Version: %s\n", config.BadgerVersion) fmt.Printf("Badger Version: %s\n", config.BadgerVersion)
if !readBool("Are these values correct?", true) { if !readBool(reader, "Are these values correct?", true) {
config = collectUserInput() config = collectUserInput(reader)
} }
} }
@@ -252,14 +235,14 @@ func main() {
if detectedType == Undefined { if detectedType == Undefined {
// If detection fails, prompt the user // If detection fails, prompt the user
fmt.Println("Unable to detect container type from existing installation.") fmt.Println("Unable to detect container type from existing installation.")
config.InstallationContainerType = podmanOrDocker() config.InstallationContainerType = podmanOrDocker(reader)
} else { } else {
config.InstallationContainerType = detectedType config.InstallationContainerType = detectedType
fmt.Printf("Detected container type: %s\n", config.InstallationContainerType) fmt.Printf("Detected container type: %s\n", config.InstallationContainerType)
} }
config.DoCrowdsecInstall = true config.DoCrowdsecInstall = true
err := installCrowdsec(config, installDir) err := installCrowdsec(config)
if err != nil { if err != nil {
fmt.Printf("Error installing CrowdSec: %v\n", err) fmt.Printf("Error installing CrowdSec: %v\n", err)
return return
@@ -294,119 +277,8 @@ func main() {
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
} }
func hasExistingInstall(dir string) bool { func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
configPath := filepath.Join(dir, "config", "config.yml") inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker")
_, 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")
chosenContainer := Docker chosenContainer := Docker
if strings.EqualFold(inputContainer, "docker") { if strings.EqualFold(inputContainer, "docker") {
@@ -418,8 +290,7 @@ func podmanOrDocker() SupportedContainer {
os.Exit(1) os.Exit(1)
} }
switch chosenContainer { if chosenContainer == Podman {
case Podman:
if !isPodmanInstalled() { if !isPodmanInstalled() {
fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.") 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) os.Exit(1)
@@ -428,7 +299,7 @@ func podmanOrDocker() SupportedContainer {
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.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 {
fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.") 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.") 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.d/99-podman.conf && sysctl --system\". Approve?", true)
if approved { if approved {
if os.Geteuid() != 0 { if os.Geteuid() != 0 {
fmt.Println("You need to run the installer as root for such a configuration.") fmt.Println("You need to run the installer as root for such a configuration.")
@@ -440,7 +311,7 @@ func podmanOrDocker() SupportedContainer {
// Linux only. // 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 { 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) fmt.Printf("Error configuring unprivileged ports: %v\n", err)
os.Exit(1) os.Exit(1)
} }
} else { } else {
@@ -450,7 +321,7 @@ func podmanOrDocker() SupportedContainer {
fmt.Println("Unprivileged ports have been configured.") fmt.Println("Unprivileged ports have been configured.")
} }
case Docker: } else if chosenContainer == Docker {
// check if docker is not installed and the user is root // check if docker is not installed and the user is root
if !isDockerInstalled() { if !isDockerInstalled() {
if os.Geteuid() != 0 { if os.Geteuid() != 0 {
@@ -465,7 +336,7 @@ func podmanOrDocker() SupportedContainer {
fmt.Println("The installer will not be able to run docker commands without running it as root.") fmt.Println("The installer will not be able to run docker commands without running it as root.")
os.Exit(1) os.Exit(1)
} }
default: } else {
// This shouldn't happen unless there's a third container runtime. // This shouldn't happen unless there's a third container runtime.
os.Exit(1) os.Exit(1)
} }
@@ -473,35 +344,35 @@ func podmanOrDocker() SupportedContainer {
return chosenContainer return chosenContainer
} }
func collectUserInput() Config { func collectUserInput(reader *bufio.Reader) Config {
config := Config{} config := Config{}
// Basic configuration // Basic configuration
fmt.Println("\n=== 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 personal 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 // Set default dashboard domain after base domain is collected
defaultDashboardDomain := "" defaultDashboardDomain := ""
if config.BaseDomain != "" { if config.BaseDomain != "" {
defaultDashboardDomain = "pangolin." + config.BaseDomain defaultDashboardDomain = "pangolin." + config.BaseDomain
} }
config.DashboardDomain = readString("Enter the domain for the Pangolin dashboard", defaultDashboardDomain) config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
config.LetsEncryptEmail = readString("Enter email for Let's Encrypt certificates", "") config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
config.InstallGerbil = readBool("Do you want to use Gerbil to allow tunneled connections", true) config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
// Email configuration // Email configuration
fmt.Println("\n=== 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 { if config.EnableEmail {
config.EmailSMTPHost = readString("Enter SMTP host", "") config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
config.EmailSMTPPort = readInt("Enter SMTP port (default 587)", 587) config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
config.EmailSMTPUser = readString("Enter SMTP username", "") config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
config.EmailSMTPPass = readPassword("Enter SMTP password") config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
config.EmailNoReply = readString("Enter no-reply email address (often the same as SMTP username)", "") config.EmailNoReply = readString(reader, "Enter no-reply email address (often the same as SMTP username)", "")
} }
// Validate required fields // Validate required fields
@@ -522,8 +393,8 @@ func collectUserInput() Config {
fmt.Println("\n=== Advanced Configuration ===") fmt.Println("\n=== Advanced Configuration ===")
config.EnableIPv6 = readBool("Is your server IPv6 capable?", true) config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
config.EnableGeoblocking = readBool("Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", true) config.EnableGeoblocking = readBool(reader, "Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", true)
if config.DashboardDomain == "" { if config.DashboardDomain == "" {
fmt.Println("Error: Dashboard Domain name is required") fmt.Println("Error: Dashboard Domain name is required")
@@ -534,23 +405,15 @@ func collectUserInput() Config {
} }
func createConfigFiles(config Config) error { func createConfigFiles(config Config) error {
if err := os.MkdirAll("config", 0755); err != nil { os.MkdirAll("config", 0755)
return fmt.Errorf("failed to create config directory: %v", err) os.MkdirAll("config/letsencrypt", 0755)
} os.MkdirAll("config/db", 0755)
if err := os.MkdirAll("config/letsencrypt", 0755); err != nil { os.MkdirAll("config/logs", 0755)
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)
}
// Walk through all embedded files // Walk through all embedded files
err := fs.WalkDir(configFiles, "config", func(path string, d fs.DirEntry, walkErr error) (err error) { err := fs.WalkDir(configFiles, "config", func(path string, d fs.DirEntry, err error) error {
if walkErr != nil { if err != nil {
return walkErr return err
} }
// Skip the root fs directory itself // Skip the root fs directory itself
@@ -601,11 +464,7 @@ func createConfigFiles(config Config) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to create %s: %v", path, err) return fmt.Errorf("failed to create %s: %v", path, err)
} }
defer func() { defer outFile.Close()
if cerr := outFile.Close(); cerr != nil && err == nil {
err = cerr
}
}()
// Execute template // Execute template
if err := tmpl.Execute(outFile, config); err != nil { if err := tmpl.Execute(outFile, config); err != nil {
@@ -621,26 +480,18 @@ func createConfigFiles(config Config) error {
return nil return nil
} }
func copyFile(src, dst string) (err error) { func copyFile(src, dst string) error {
source, err := os.Open(src) source, err := os.Open(src)
if err != nil { if err != nil {
return err return err
} }
defer func() { defer source.Close()
if cerr := source.Close(); cerr != nil && err == nil {
err = cerr
}
}()
destination, err := os.Create(dst) destination, err := os.Create(dst)
if err != nil { if err != nil {
return err return err
} }
defer func() { defer destination.Close()
if cerr := destination.Close(); cerr != nil && err == nil {
err = cerr
}
}()
_, err = io.Copy(destination, source) _, err = io.Copy(destination, source)
return err return err
@@ -711,24 +562,22 @@ func showSetupTokenInstructions(containerType SupportedContainer, dashboardDomai
fmt.Println("To get your setup token, you need to:") fmt.Println("To get your setup token, you need to:")
fmt.Println("") fmt.Println("")
fmt.Println("1. Start the containers") fmt.Println("1. Start the containers")
switch containerType { if containerType == Docker {
case Docker:
fmt.Println(" docker compose up -d") fmt.Println(" docker compose up -d")
case Podman: } else if containerType == Podman {
fmt.Println(" podman-compose up -d") fmt.Println(" podman-compose up -d")
} else {
} }
fmt.Println("") fmt.Println("")
fmt.Println("2. Wait for the Pangolin container to start and generate the token") fmt.Println("2. Wait for the Pangolin container to start and generate the token")
fmt.Println("") fmt.Println("")
fmt.Println("3. Check the container logs for the setup token") fmt.Println("3. Check the container logs for the setup token")
switch containerType { if containerType == Docker {
case Docker:
fmt.Println(" docker logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'") 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'") fmt.Println(" podman logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
} else {
} }
fmt.Println("") fmt.Println("")
fmt.Println("4. Look for output like") fmt.Println("4. Look for output like")
fmt.Println(" === SETUP TOKEN GENERATED ===") fmt.Println(" === SETUP TOKEN GENERATED ===")
@@ -752,6 +601,32 @@ func generateRandomSecretKey() string {
return base64.StdEncoding.EncodeToString(secret) return base64.StdEncoding.EncodeToString(secret)
} }
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. // Run external commands with stdio/stderr attached.
func run(name string, args ...string) error { func run(name string, args ...string) error {
cmd := exec.Command(name, args...) cmd := exec.Command(name, args...)
@@ -764,7 +639,10 @@ func checkPortsAvailable(port int) error {
addr := fmt.Sprintf(":%d", port) addr := fmt.Sprintf(":%d", port)
ln, err := net.Listen("tcp", addr) ln, err := net.Listen("tcp", addr)
if err != nil { 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 { if closeErr := ln.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, fmt.Fprintf(os.Stderr,
-51
View File
@@ -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
}
-137
View File
@@ -1,137 +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-2026 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.
*/
"""
HEADER_NORMALIZED = HEADER_TEXT.strip()
def extract_leading_block_comment(content):
"""
If the file content begins with a /* ... */ block comment, return the
full text of that comment (including the delimiters) and the index at
which the rest of the file starts (after any trailing newlines).
Returns (None, 0) when no such comment is found.
"""
stripped = content.lstrip()
if not stripped.startswith('/*'):
return None, 0
# Account for any leading whitespace before the comment
comment_start = content.index('/*')
end_marker = content.find('*/', comment_start + 2)
if end_marker == -1:
return None, 0
comment_end = end_marker + 2 # position just after '*/'
comment_text = content[comment_start:comment_end].strip()
# Advance past any whitespace / newlines that follow the closing */
rest_start = comment_end
while rest_start < len(content) and content[rest_start] in '\n\r':
rest_start += 1
return comment_text, rest_start
def should_add_header(file_path):
"""
Checks if a file should receive the commercial license header.
Returns True if 'server/private' is in the path.
"""
if 'server/private' in file_path.lower():
return True
return False
def process_directory(root_dir):
"""
Recursively scans a directory and adds/replaces/removes headers in
qualifying .ts or .tsx files, skipping any 'node_modules' directories.
"""
print(f"Scanning directory: {root_dir}")
files_processed = 0
files_modified = 0
for root, dirs, files in os.walk(root_dir):
# Exclude 'node_modules' directories from the scan.
if 'node_modules' in dirs:
dirs.remove('node_modules')
for file in files:
if not (file.endswith('.ts') or file.endswith('.tsx')):
continue
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()
existing_comment, body_start = extract_leading_block_comment(
original_content
)
has_any_header = existing_comment is not None
has_correct_header = existing_comment == HEADER_NORMALIZED
body = original_content[body_start:] if has_any_header else original_content
if should_add_header(file_path):
if has_correct_header:
print(f"Header up-to-date: {file_path}")
else:
# Either no header exists or the header is outdated - write
# the correct one.
action = "Replaced header in" if has_any_header else "Added header to"
new_content = HEADER_NORMALIZED + '\n\n' + body
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
print(f"{action}: {file_path}")
files_modified += 1
else:
if has_any_header:
# Remove the header - it shouldn't be here.
with open(file_path, 'w', encoding='utf-8') as f:
f.write(body)
print(f"Removed header from: {file_path}")
files_modified += 1
else:
print(f"No header needed: {file_path}")
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 (added/replaced/removed): {files_modified}")
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))
+20 -614
View File
File diff suppressed because it is too large Load Diff
+21 -615
View File
File diff suppressed because it is too large Load Diff
+22 -616
View File
File diff suppressed because it is too large Load Diff
+45 -642
View File
File diff suppressed because it is too large Load Diff
+19 -613
View File
File diff suppressed because it is too large Load Diff
+20 -614
View File
File diff suppressed because it is too large Load Diff
+60 -654
View File
File diff suppressed because it is too large Load Diff
+19 -613
View File
File diff suppressed because it is too large Load Diff
+18 -612
View File
@@ -1,8 +1,4 @@
{ {
"contactSalesEnable": "Kontakt salgsavdelingen for å aktivere denne funksjonen.",
"contactSalesBookDemo": "Bestill en demo",
"contactSalesOr": "eller",
"contactSalesContactUs": "kontakt oss",
"setupCreate": "Opprett organisasjonen, nettstedet og ressursene", "setupCreate": "Opprett organisasjonen, nettstedet og ressursene",
"headerAuthCompatibilityInfo": "Aktiver dette for å tvinge frem en 401 Uautorisert-respons når en autentiseringstoken mangler. Dette kreves for nettlesere eller spesifikke HTTP-biblioteker som ikke sender legitimasjon uten en serverutfordring.", "headerAuthCompatibilityInfo": "Aktiver dette for å tvinge frem en 401 Uautorisert-respons når en autentiseringstoken mangler. Dette kreves for nettlesere eller spesifikke HTTP-biblioteker som ikke sender legitimasjon uten en serverutfordring.",
"headerAuthCompatibility": "Utvidet kompatibilitet", "headerAuthCompatibility": "Utvidet kompatibilitet",
@@ -23,14 +19,6 @@
"componentsInvalidKey": "Ugyldig eller utgått lisensnøkkel oppdaget. Følg lisensvilkårene for å fortsette å kunne bruke alle funksjonene.", "componentsInvalidKey": "Ugyldig eller utgått lisensnøkkel oppdaget. Følg lisensvilkårene for å fortsette å kunne bruke alle funksjonene.",
"dismiss": "Avvis", "dismiss": "Avvis",
"subscriptionViolationMessage": "Du er utenfor grensen for gjeldende plan. Rett problemet ved å fjerne nettsteder, brukere eller andre ressurser for å bli innenfor planen din.", "subscriptionViolationMessage": "Du er utenfor grensen for gjeldende plan. Rett problemet ved å fjerne nettsteder, brukere eller andre ressurser for å bli innenfor planen din.",
"trialBannerMessage": "Din prøveperiode utløper om {countdown}. Oppgrader for å beholde tilgangen.",
"trialBannerExpired": "Prøveperioden din har utløpt. Oppgrader nå for å gjenopprette tilgangen.",
"trialActive": "Gratis prøveversjon aktiv",
"trialExpired": "Prøveperioden er utløpt",
"trialHasEnded": "Din prøveperiode har avsluttet.",
"trialDaysRemaining": "{count, plural, one {# dag igjen} other {# dager igjen}}",
"trialDaysLeftShort": "{days}d igjen av prøveperioden",
"trialGoToBilling": "Gå til faktureringssiden",
"subscriptionViolationViewBilling": "Vis fakturering", "subscriptionViolationViewBilling": "Vis fakturering",
"componentsLicenseViolation": "Lisens Brudd: Denne serveren bruker {usedSites} områder som overskrider den lisensierte grenser av {maxSites} områder. Følg lisensvilkårene for å fortsette å kunne bruke alle funksjonene.", "componentsLicenseViolation": "Lisens Brudd: Denne serveren bruker {usedSites} områder som overskrider den lisensierte grenser av {maxSites} områder. Følg lisensvilkårene for å fortsette å kunne bruke alle funksjonene.",
"componentsSupporterMessage": "Takk for at du støtter Pangolin som en {tier}!", "componentsSupporterMessage": "Takk for at du støtter Pangolin som en {tier}!",
@@ -93,8 +81,6 @@
"siteConfirmCopy": "Jeg har kopiert konfigurasjonen", "siteConfirmCopy": "Jeg har kopiert konfigurasjonen",
"searchSitesProgress": "Søker i områder...", "searchSitesProgress": "Søker i områder...",
"siteAdd": "Legg til område", "siteAdd": "Legg til område",
"sitesTableViewPublicResources": "Vis offentlige ressurser",
"sitesTableViewPrivateResources": "Vis private ressurser",
"siteInstallNewt": "Installer Newt", "siteInstallNewt": "Installer Newt",
"siteInstallNewtDescription": "Få Newt til å kjøre på systemet ditt", "siteInstallNewtDescription": "Få Newt til å kjøre på systemet ditt",
"WgConfiguration": "WireGuard Konfigurasjon", "WgConfiguration": "WireGuard Konfigurasjon",
@@ -112,21 +98,6 @@
"siteUpdatedDescription": "Området har blitt oppdatert.", "siteUpdatedDescription": "Området har blitt oppdatert.",
"siteGeneralDescription": "Konfigurer de generelle innstillingene for dette området", "siteGeneralDescription": "Konfigurer de generelle innstillingene for dette området",
"siteSettingDescription": "Konfigurere innstillingene på nettstedet", "siteSettingDescription": "Konfigurere innstillingene på nettstedet",
"siteResourcesTab": "Ressurser",
"siteResourcesNoneOnSite": "Dette nettstedet har ingen offentlige eller private ressurser enda.",
"siteResourcesSectionPublic": "Offentlige ressurser",
"siteResourcesSectionPrivate": "Private ressurser",
"siteResourcesSectionPublicDescription": "Ressurser eksponert eksternt gjennom domener eller porter.",
"siteResourcesSectionPrivateDescription": "Ressurser tilgjengelig på ditt private nettverk gjennom nettstedet.",
"siteResourcesViewAllPublic": "Vis alle ressurser",
"siteResourcesViewAllPrivate": "Vis alle ressurser",
"siteResourcesDialogDescription": "Oversikt over offentlige og private ressurser assosiert med dette nettstedet.",
"siteResourcesShowMore": "Vis mer",
"siteResourcesPermissionDenied": "Du har ikke tillatelse til å liste opp disse ressursene.",
"siteResourcesEmptyPublic": "Ingen offentlige ressurser retter seg mot dette nettstedet enda.",
"siteResourcesEmptyPrivate": "Ingen private ressurser er assosiert med dette nettstedet enda.",
"siteResourcesHowToAccess": "Hvordan få tilgang",
"siteResourcesTargetsOnSite": "Mål på dette nettstedet",
"siteSetting": "{siteName} Innstillinger", "siteSetting": "{siteName} Innstillinger",
"siteNewtTunnel": "Nyhetsnettsted (anbefalt)", "siteNewtTunnel": "Nyhetsnettsted (anbefalt)",
"siteNewtTunnelDescription": "Lekkeste måte å lage et inngangspunkt til ethvert nettverk. Ingen ekstra oppsett på.", "siteNewtTunnelDescription": "Lekkeste måte å lage et inngangspunkt til ethvert nettverk. Ingen ekstra oppsett på.",
@@ -177,11 +148,6 @@
"createLink": "Opprett lenke", "createLink": "Opprett lenke",
"resourcesNotFound": "Ingen ressurser funnet", "resourcesNotFound": "Ingen ressurser funnet",
"resourceSearch": "Søk i ressurser", "resourceSearch": "Søk i ressurser",
"machineSearch": "Søk etter maskiner",
"machinesSearch": "Søk etter maskinklienter...",
"machineNotFound": "Ingen maskiner funnet",
"userDeviceSearch": "Søk etter brukerenheter",
"userDevicesSearch": "Søk etter brukerenheter...",
"openMenu": "Åpne meny", "openMenu": "Åpne meny",
"resource": "Ressurs", "resource": "Ressurs",
"title": "Tittel", "title": "Tittel",
@@ -209,7 +175,6 @@
"resourceHTTPDescription": "Proxy forespørsler over HTTPS ved å bruke et fullstendig kvalifisert domenenavn.", "resourceHTTPDescription": "Proxy forespørsler over HTTPS ved å bruke et fullstendig kvalifisert domenenavn.",
"resourceRaw": "Rå TCP/UDP-ressurs", "resourceRaw": "Rå TCP/UDP-ressurs",
"resourceRawDescription": "Proxy forespørsler over rå TCP/UDP ved å bruke et portnummer.", "resourceRawDescription": "Proxy forespørsler over rå TCP/UDP ved å bruke et portnummer.",
"resourceRawDescriptionCloud": "Proxy forespørsler om rå TCP/UDP ved hjelp av et portnummer. Krever sider for å koble til en ekstern node.",
"resourceCreate": "Opprett ressurs", "resourceCreate": "Opprett ressurs",
"resourceCreateDescription": "Følg trinnene nedenfor for å opprette en ny ressurs", "resourceCreateDescription": "Følg trinnene nedenfor for å opprette en ny ressurs",
"resourceSeeAll": "Se alle ressurser", "resourceSeeAll": "Se alle ressurser",
@@ -236,7 +201,6 @@
"protocolSelect": "Velg en protokoll", "protocolSelect": "Velg en protokoll",
"resourcePortNumber": "Portnummer", "resourcePortNumber": "Portnummer",
"resourcePortNumberDescription": "Det eksterne portnummeret for proxy forespørsler.", "resourcePortNumberDescription": "Det eksterne portnummeret for proxy forespørsler.",
"back": "Tilbake",
"cancel": "Avbryt", "cancel": "Avbryt",
"resourceConfig": "Konfigurasjonsutdrag", "resourceConfig": "Konfigurasjonsutdrag",
"resourceConfigDescription": "Kopier og lim inn disse konfigurasjons-øyeblikkene for å sette opp TCP/UDP ressursen", "resourceConfigDescription": "Kopier og lim inn disse konfigurasjons-øyeblikkene for å sette opp TCP/UDP ressursen",
@@ -282,25 +246,11 @@
"orgErrorDeleteMessage": "Det oppsto en feil under sletting av organisasjonen.", "orgErrorDeleteMessage": "Det oppsto en feil under sletting av organisasjonen.",
"orgDeleted": "Organisasjon slettet", "orgDeleted": "Organisasjon slettet",
"orgDeletedMessage": "Organisasjonen og tilhørende data er slettet.", "orgDeletedMessage": "Organisasjonen og tilhørende data er slettet.",
"deleteAccount": "Slett konto",
"deleteAccountDescription": "Slett kontoen din permanent, alle organisasjoner du eier, og alle data i disse organisasjonene. Dette kan ikke angres.",
"deleteAccountButton": "Slett konto",
"deleteAccountConfirmTitle": "Slett konto",
"deleteAccountConfirmMessage": "Dette vil slette kontoen din, alle organisasjoner du eier og alle data i disse organisasjonene. Dette kan ikke gjøres om.",
"deleteAccountConfirmString": "Slett konto",
"deleteAccountSuccess": "Kontoen er slettet",
"deleteAccountSuccessMessage": "Kontoen din er slettet.",
"deleteAccountError": "Kunne ikke slette konto",
"deleteAccountPreviewAccount": "Din konto",
"deleteAccountPreviewOrgs": "Organisasjoner du eier (og alle deres data)",
"orgMissing": "Organisasjons-ID Mangler", "orgMissing": "Organisasjons-ID Mangler",
"orgMissingMessage": "Kan ikke regenerere invitasjon uten en organisasjons-ID.", "orgMissingMessage": "Kan ikke regenerere invitasjon uten en organisasjons-ID.",
"accessUsersManage": "Administrer brukere", "accessUsersManage": "Administrer brukere",
"accessUserManage": "Administrer brukere",
"accessUsersDescription": "Inviter og behandle brukere med tilgang til denne organisasjonen", "accessUsersDescription": "Inviter og behandle brukere med tilgang til denne organisasjonen",
"accessUsersSearch": "Søk etter brukere...", "accessUsersSearch": "Søk etter brukere...",
"accessUsersRoleFilterCount": "{count, plural, one {# rolle} other {# roller}}",
"accessUsersRoleFilterClear": "Fjern rollesøkefiltre",
"accessUserCreate": "Opprett bruker", "accessUserCreate": "Opprett bruker",
"accessUserRemove": "Fjern bruker", "accessUserRemove": "Fjern bruker",
"username": "Brukernavn", "username": "Brukernavn",
@@ -360,54 +310,6 @@
"apiKeysDelete": "Slett API-nøkkel", "apiKeysDelete": "Slett API-nøkkel",
"apiKeysManage": "Administrer API-nøkler", "apiKeysManage": "Administrer API-nøkler",
"apiKeysDescription": "API-nøkler brukes for å autentisere med integrasjons-API", "apiKeysDescription": "API-nøkler brukes for å autentisere med integrasjons-API",
"provisioningKeysTitle": "Foreløpig nøkkel",
"provisioningKeysManage": "Behandle bestemmende nøkler",
"provisioningKeysDescription": "Bestemmelsesnøkler brukes til å godkjenne automatisert nettstedsløsning for din organisasjon.",
"provisioningManage": "Levering",
"provisioningDescription": "Administrer foreløpig nøkler og gjennomgå ventende nettsteder som venter på godkjenning.",
"pendingSites": "Ventende nettsteder",
"siteApproveSuccess": "Vellykket godkjenning av nettsted",
"siteApproveError": "Feil ved godkjenning av side",
"provisioningKeys": "Foreløpig nøkler",
"searchProvisioningKeys": "Søk varer i lagrings nøkler...",
"provisioningKeysAdd": "Generer fremvisende nøkkel",
"provisioningKeysErrorDelete": "Feil under sletting av foreløpig nøkkel",
"provisioningKeysErrorDeleteMessage": "Feil under sletting av foreløpig nøkkel",
"provisioningKeysQuestionRemove": "Er du sikker på at du vil fjerne denne midlertidig nøkkelen fra organisasjonen?",
"provisioningKeysMessageRemove": "Når nøkkelen er fjernet, kan den ikke lenger brukes til anleggsavsetning.",
"provisioningKeysDeleteConfirm": "Bekreft sletting av bestemmelsesnøkkel",
"provisioningKeysDelete": "Slett bestemmelsesnøkkel",
"provisioningKeysCreate": "Generer fremvisende nøkkel",
"provisioningKeysCreateDescription": "Generer en ny foreløpig nøkkel til organisasjonen",
"provisioningKeysSeeAll": "Se alle foreløpig nøkler",
"provisioningKeysSave": "Lagre den midlertidig nøkkelen",
"provisioningKeysSaveDescription": "Du kan bare se denne én gang. Kopier det til et sikkert sted.",
"provisioningKeysErrorCreate": "Feil under oppretting av foreløpig nøkkel",
"provisioningKeysList": "Ny provisorisk nøkkel",
"provisioningKeysMaxBatchSize": "Maks størrelse på bunt",
"provisioningKeysUnlimitedBatchSize": "Ubegrenset mengde bunt (ingen begrensning)",
"provisioningKeysMaxBatchUnlimited": "Ubegrenset",
"provisioningKeysMaxBatchSizeInvalid": "Angi en gyldig sjakkstørrelse (11 000.000).",
"provisioningKeysValidUntil": "Gyldig til",
"provisioningKeysValidUntilHint": "La stå tomt for ingen utløp.",
"provisioningKeysValidUntilInvalid": "Angi en gyldig dato og klokkeslett.",
"provisioningKeysNumUsed": "Antall ganger brukt",
"provisioningKeysLastUsed": "Sist brukt",
"provisioningKeysNoExpiry": "Ingen utløpsdato",
"provisioningKeysNeverUsed": "Aldri",
"provisioningKeysEdit": "Rediger bestemmelsesnøkkel",
"provisioningKeysEditDescription": "Oppdater maksimal størrelse for bunt og utløpstid for denne nøkkelen.",
"provisioningKeysApproveNewSites": "Godkjenn nye nettsteder",
"provisioningKeysApproveNewSitesDescription": "Godkjenn automatisk nettsteder som registrerer deg med denne nøkkelen.",
"provisioningKeysUpdateError": "Feil under oppdatering av foreløpig nøkkel",
"provisioningKeysUpdated": "Foreslå nøkkel oppdatert",
"provisioningKeysUpdatedDescription": "Dine endringer er lagret.",
"provisioningKeysBannerTitle": "Sidens bestemmende nøkler",
"provisioningKeysBannerDescription": "Generer en provisjonsnøkkel og bruk den med Newt-kontakten for automatisk opprettelse av nettsteder ved første oppstart - ingen behov for å sette opp separate legitimasjoner for hvert nettsted.",
"provisioningKeysBannerButtonText": "Lær mer",
"pendingSitesBannerTitle": "Ventende nettsteder",
"pendingSitesBannerDescription": "Nettsteder som kobler seg til ved bruk av en provisjonsnøkkel vises her for vurdering.",
"pendingSitesBannerButtonText": "Lær mer",
"apiKeysSettings": "{apiKeyName} Innstillinger", "apiKeysSettings": "{apiKeyName} Innstillinger",
"userTitle": "Administrer alle brukere", "userTitle": "Administrer alle brukere",
"userDescription": "Vis og administrer alle brukere i systemet", "userDescription": "Vis og administrer alle brukere i systemet",
@@ -437,10 +339,6 @@
"licenseErrorKeyActivate": "Aktivering av lisensnøkkel feilet", "licenseErrorKeyActivate": "Aktivering av lisensnøkkel feilet",
"licenseErrorKeyActivateDescription": "Det oppstod en feil under aktivering av lisensnøkkelen.", "licenseErrorKeyActivateDescription": "Det oppstod en feil under aktivering av lisensnøkkelen.",
"licenseAbout": "Om Lisensiering", "licenseAbout": "Om Lisensiering",
"licenseBannerTitle": "Aktiver din bedriftslisens",
"licenseBannerDescription": "Lås opp bedriftsfunksjoner for din egenvertede Pangolin-instans. Kjøp en lisensnøkkel for å aktivere premium-funksjoner og legg den inn nedenfor.",
"licenseBannerGetLicense": "Få en lisens",
"licenseBannerViewDocs": "Vis dokumentasjon",
"communityEdition": "Fellesskapsutgave", "communityEdition": "Fellesskapsutgave",
"licenseAboutDescription": "Dette er for bedrifts- og foretaksbrukere som bruker Pangolin i et kommersielt miljø. Hvis du bruker Pangolin til personlig bruk, kan du ignorere denne seksjonen.", "licenseAboutDescription": "Dette er for bedrifts- og foretaksbrukere som bruker Pangolin i et kommersielt miljø. Hvis du bruker Pangolin til personlig bruk, kan du ignorere denne seksjonen.",
"licenseKeyActivated": "Lisensnøkkel aktivert", "licenseKeyActivated": "Lisensnøkkel aktivert",
@@ -563,8 +461,6 @@
"filterByApprovalState": "Filtrer etter godkjenningsstatus", "filterByApprovalState": "Filtrer etter godkjenningsstatus",
"approvalListEmpty": "Ingen godkjenninger", "approvalListEmpty": "Ingen godkjenninger",
"approvalState": "Godkjennings tilstand", "approvalState": "Godkjennings tilstand",
"approvalLoadMore": "Last mer",
"loadingApprovals": "Laster inn godkjenninger",
"approve": "Godkjenn", "approve": "Godkjenn",
"approved": "Godkjent", "approved": "Godkjent",
"denied": "Avvist", "denied": "Avvist",
@@ -598,12 +494,9 @@
"userSaved": "Bruker lagret", "userSaved": "Bruker lagret",
"userSavedDescription": "Brukeren har blitt oppdatert.", "userSavedDescription": "Brukeren har blitt oppdatert.",
"autoProvisioned": "Auto avlyst", "autoProvisioned": "Auto avlyst",
"autoProvisionSettings": "Auto leveringsinnstillinger",
"autoProvisionedDescription": "Tillat denne brukeren å bli automatisk administrert av en identitetsleverandør", "autoProvisionedDescription": "Tillat denne brukeren å bli automatisk administrert av en identitetsleverandør",
"accessControlsDescription": "Administrer hva denne brukeren kan få tilgang til og gjøre i organisasjonen", "accessControlsDescription": "Administrer hva denne brukeren kan få tilgang til og gjøre i organisasjonen",
"accessControlsSubmit": "Lagre tilgangskontroller", "accessControlsSubmit": "Lagre tilgangskontroller",
"singleRolePerUserPlanNotice": "Din plan støtter bare én rolle per bruker.",
"singleRolePerUserEditionNotice": "Denne utgaven støtter bare én rolle per bruker.",
"roles": "Roller", "roles": "Roller",
"accessUsersRoles": "Administrer brukere og roller", "accessUsersRoles": "Administrer brukere og roller",
"accessUsersRolesDescription": "Inviter brukere og legg dem til roller for å administrere tilgang til organisasjonen", "accessUsersRolesDescription": "Inviter brukere og legg dem til roller for å administrere tilgang til organisasjonen",
@@ -660,8 +553,6 @@
"targetErrorInvalidPortDescription": "Vennligst skriv inn et gyldig portnummer", "targetErrorInvalidPortDescription": "Vennligst skriv inn et gyldig portnummer",
"targetErrorNoSite": "Ingen nettsted valgt", "targetErrorNoSite": "Ingen nettsted valgt",
"targetErrorNoSiteDescription": "Velg et nettsted for målet", "targetErrorNoSiteDescription": "Velg et nettsted for målet",
"targetTargetsCleared": "Mål ryddet",
"targetTargetsClearedDescription": "Alle mål har blitt fjernet fra denne ressursen",
"targetCreated": "Mål opprettet", "targetCreated": "Mål opprettet",
"targetCreatedDescription": "Målet har blitt opprettet", "targetCreatedDescription": "Målet har blitt opprettet",
"targetErrorCreate": "Kunne ikke opprette målet", "targetErrorCreate": "Kunne ikke opprette målet",
@@ -745,7 +636,6 @@
"resourcesErrorUpdate": "Feilet å slå av/på ressurs", "resourcesErrorUpdate": "Feilet å slå av/på ressurs",
"resourcesErrorUpdateDescription": "En feil oppstod under oppdatering av ressursen", "resourcesErrorUpdateDescription": "En feil oppstod under oppdatering av ressursen",
"access": "Tilgang", "access": "Tilgang",
"accessControl": "Tilgangskontroll",
"shareLink": "{resource} Del Lenke", "shareLink": "{resource} Del Lenke",
"resourceSelect": "Velg ressurs", "resourceSelect": "Velg ressurs",
"shareLinks": "Del lenker", "shareLinks": "Del lenker",
@@ -763,7 +653,6 @@
"newtEndpoint": "Endpoint", "newtEndpoint": "Endpoint",
"newtId": "ID", "newtId": "ID",
"newtSecretKey": "Sikkerhetsnøkkel", "newtSecretKey": "Sikkerhetsnøkkel",
"newtVersion": "Versjon",
"architecture": "Arkitektur", "architecture": "Arkitektur",
"sites": "Områder", "sites": "Områder",
"siteWgAnyClients": "Bruk hvilken som helst WireGuard klient til å koble til. Du må adressere interne ressurser ved hjelp av peer IP.", "siteWgAnyClients": "Bruk hvilken som helst WireGuard klient til å koble til. Du må adressere interne ressurser ved hjelp av peer IP.",
@@ -887,7 +776,6 @@
"accessRoleRemoved": "Rolle fjernet", "accessRoleRemoved": "Rolle fjernet",
"accessRoleRemovedDescription": "Rollen er vellykket fjernet.", "accessRoleRemovedDescription": "Rollen er vellykket fjernet.",
"accessRoleRequiredRemove": "Før du sletter denne rollen, vennligst velg en ny rolle å overføre eksisterende medlemmer til.", "accessRoleRequiredRemove": "Før du sletter denne rollen, vennligst velg en ny rolle å overføre eksisterende medlemmer til.",
"network": "Nettverk",
"manage": "Administrer", "manage": "Administrer",
"sitesNotFound": "Ingen områder funnet.", "sitesNotFound": "Ingen områder funnet.",
"pangolinServerAdmin": "Server Admin - Pangolin", "pangolinServerAdmin": "Server Admin - Pangolin",
@@ -903,9 +791,9 @@
"sitestCountIncrease": "Øk antall områder", "sitestCountIncrease": "Øk antall områder",
"idpManage": "Administrer Identitetsleverandører", "idpManage": "Administrer Identitetsleverandører",
"idpManageDescription": "Vis og administrer identitetsleverandører i systemet", "idpManageDescription": "Vis og administrer identitetsleverandører i systemet",
"idpGlobalModeBanner": "Identitetsleverandører (IdPs) per organisasjon er deaktivert på denne serveren. Den bruker globale IdP (delt over alle organisasjoner). Administrer globale IdP'er i <adminPanelLink>admin-panelet</adminPanelLink>. For å aktivere IdP per organisasjon, rediger serverkonfigurasjonen og sett IdP-modus til org. <configDocsLink>Se dokumentasjonen</configDocsLink>. Hvis du vil fortsette å bruke globale IdPs og få denne til å forsvinne fra organisasjonens innstillinger, satt eksplisitt modusen til global i konfigurasjonen.", "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": "Identitetsleverandører (IdPs) per organisasjon er deaktivert på denne serveren. Den bruker globale IdPs (delt på tvers av alle organisasjoner). Administrer globale IdPs i <adminPanelLink>administrasjons-panelet</adminPanelLink>. For å bruke identitetsleverandører per organisasjon, må du oppgradere til Enterprise-utgaven.", "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": "Identitetsleverandører (IdPs) per organisasjon er deaktivert på denne serveren. Den bruker globale IdPs (delt på tvers av alle organisasjoner). Administrer globale IdPs i <adminPanelLink>administrasjons-panelet</adminPanelLink>. For å bruke identitetsleverandører per organisasjon, kreves en Enterprise-lisens.", "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": "Identitetsleverandør slettet vellykket", "idpDeletedDescription": "Identitetsleverandør slettet vellykket",
"idpOidc": "OAuth2/OIDC", "idpOidc": "OAuth2/OIDC",
"idpQuestionRemove": "Er du sikker på at du vil slette identitetsleverandøren permanent?", "idpQuestionRemove": "Er du sikker på at du vil slette identitetsleverandøren permanent?",
@@ -931,7 +819,6 @@
"idpDisplayName": "Et visningsnavn for denne identitetsleverandøren", "idpDisplayName": "Et visningsnavn for denne identitetsleverandøren",
"idpAutoProvisionUsers": "Automatisk brukerklargjøring", "idpAutoProvisionUsers": "Automatisk brukerklargjøring",
"idpAutoProvisionUsersDescription": "Når aktivert, opprettes brukere automatisk i systemet ved første innlogging, med mulighet til å tilordne brukere til roller og organisasjoner.", "idpAutoProvisionUsersDescription": "Når aktivert, opprettes brukere automatisk i systemet ved første innlogging, med mulighet til å tilordne brukere til roller og organisasjoner.",
"idpAutoProvisionConfigureAfterCreate": "Du kan konfigurere autoprovisjonsinnstillingene når identitetsleverandøren er opprettet.",
"licenseBadge": "EE", "licenseBadge": "EE",
"idpType": "Leverandørtype", "idpType": "Leverandørtype",
"idpTypeDescription": "Velg typen identitetsleverandør du ønsker å konfigurere", "idpTypeDescription": "Velg typen identitetsleverandør du ønsker å konfigurere",
@@ -983,7 +870,7 @@
"defaultMappingsRole": "Standard rolletilordning", "defaultMappingsRole": "Standard rolletilordning",
"defaultMappingsRoleDescription": "Resultatet av dette uttrykket må returnere rollenavnet slik det er definert i organisasjonen som en streng.", "defaultMappingsRoleDescription": "Resultatet av dette uttrykket må returnere rollenavnet slik det er definert i organisasjonen som en streng.",
"defaultMappingsOrg": "Standard organisasjonstilordning", "defaultMappingsOrg": "Standard organisasjonstilordning",
"defaultMappingsOrgDescription": "Når denne er satt, må uttrykket returnere organisasjons-IDen eller «true» for at brukeren skal få tilgang til den organisasjonen. Når den ikke er satt, er det nok å definere en rolletilordning: brukeren gis tilgang så lenge en gyldig rolletilknytting kan løses for dem i organisasjonen.", "defaultMappingsOrgDescription": "Dette uttrykket returnere organisasjons-ID-en eller «true» for å gi brukeren tilgang til organisasjonen.",
"defaultMappingsSubmit": "Lagre standard tilordninger", "defaultMappingsSubmit": "Lagre standard tilordninger",
"orgPoliciesEdit": "Rediger Organisasjonspolicy", "orgPoliciesEdit": "Rediger Organisasjonspolicy",
"org": "Organisasjon", "org": "Organisasjon",
@@ -1130,12 +1017,12 @@
"pangolinSetup": "Oppsett - Pangolin", "pangolinSetup": "Oppsett - Pangolin",
"orgNameRequired": "Organisasjonsnavn er påkrevd", "orgNameRequired": "Organisasjonsnavn er påkrevd",
"orgIdRequired": "Organisasjons-ID er påkrevd", "orgIdRequired": "Organisasjons-ID er påkrevd",
"orgIdMaxLength": "Organisasjons-ID må maksimalt være 32 tegn",
"orgErrorCreate": "En feil oppstod under oppretting av organisasjon", "orgErrorCreate": "En feil oppstod under oppretting av organisasjon",
"pageNotFound": "Siden ble ikke funnet", "pageNotFound": "Siden ble ikke funnet",
"pageNotFoundDescription": "Oops! Siden du leter etter finnes ikke.", "pageNotFoundDescription": "Oops! Siden du leter etter finnes ikke.",
"overview": "Oversikt", "overview": "Oversikt",
"home": "Hjem", "home": "Hjem",
"accessControl": "Tilgangskontroll",
"settings": "Innstillinger", "settings": "Innstillinger",
"usersAll": "Alle brukere", "usersAll": "Alle brukere",
"license": "Lisens", "license": "Lisens",
@@ -1198,12 +1085,6 @@
"actionGetUser": "Hent bruker", "actionGetUser": "Hent bruker",
"actionGetOrgUser": "Hent organisasjonsbruker", "actionGetOrgUser": "Hent organisasjonsbruker",
"actionListOrgDomains": "List opp organisasjonsdomener", "actionListOrgDomains": "List opp organisasjonsdomener",
"actionGetDomain": "Få Domene",
"actionCreateOrgDomain": "Opprett domene",
"actionUpdateOrgDomain": "Oppdater domene",
"actionDeleteOrgDomain": "Slett domene",
"actionGetDNSRecords": "Hent DNS-oppføringer",
"actionRestartOrgDomain": "Omstart Domene",
"actionCreateSite": "Opprett område", "actionCreateSite": "Opprett område",
"actionDeleteSite": "Slett område", "actionDeleteSite": "Slett område",
"actionGetSite": "Hent område", "actionGetSite": "Hent område",
@@ -1215,7 +1096,6 @@
"setupTokenDescription": "Skriv inn oppsetttoken fra serverkonsollen.", "setupTokenDescription": "Skriv inn oppsetttoken fra serverkonsollen.",
"setupTokenRequired": "Oppsetttoken er nødvendig", "setupTokenRequired": "Oppsetttoken er nødvendig",
"actionUpdateSite": "Oppdater område", "actionUpdateSite": "Oppdater område",
"actionResetSiteBandwidth": "Tilbakestill organisasjons-båndbredde",
"actionListSiteRoles": "List opp tillatte områderoller", "actionListSiteRoles": "List opp tillatte områderoller",
"actionCreateResource": "Opprett ressurs", "actionCreateResource": "Opprett ressurs",
"actionDeleteResource": "Slett ressurs", "actionDeleteResource": "Slett ressurs",
@@ -1245,7 +1125,6 @@
"actionRemoveUser": "Fjern bruker", "actionRemoveUser": "Fjern bruker",
"actionListUsers": "List opp brukere", "actionListUsers": "List opp brukere",
"actionAddUserRole": "Legg til brukerrolle", "actionAddUserRole": "Legg til brukerrolle",
"actionSetUserOrgRoles": "Angi brukerroller",
"actionGenerateAccessToken": "Generer tilgangstoken", "actionGenerateAccessToken": "Generer tilgangstoken",
"actionDeleteAccessToken": "Slett tilgangstoken", "actionDeleteAccessToken": "Slett tilgangstoken",
"actionListAccessTokens": "List opp tilgangstokener", "actionListAccessTokens": "List opp tilgangstokener",
@@ -1290,9 +1169,7 @@
"actionViewLogs": "Vis logger", "actionViewLogs": "Vis logger",
"noneSelected": "Ingen valgt", "noneSelected": "Ingen valgt",
"orgNotFound2": "Ingen organisasjoner funnet.", "orgNotFound2": "Ingen organisasjoner funnet.",
"search": "Søk", "searchProgress": "Søker...",
"searchPlaceholder": "Søk...",
"emptySearchOptions": "Ingen valg funnet",
"create": "Opprett", "create": "Opprett",
"orgs": "Organisasjoner", "orgs": "Organisasjoner",
"loginError": "En uventet feil oppstod. Vennligst prøv igjen.", "loginError": "En uventet feil oppstod. Vennligst prøv igjen.",
@@ -1356,14 +1233,12 @@
"sidebarClientResources": "Privat", "sidebarClientResources": "Privat",
"sidebarAccessControl": "Tilgangskontroll", "sidebarAccessControl": "Tilgangskontroll",
"sidebarLogsAndAnalytics": "Logger og analyser", "sidebarLogsAndAnalytics": "Logger og analyser",
"sidebarTeam": "Lag",
"sidebarUsers": "Brukere", "sidebarUsers": "Brukere",
"sidebarAdmin": "Administrator", "sidebarAdmin": "Administrator",
"sidebarInvitations": "Invitasjoner", "sidebarInvitations": "Invitasjoner",
"sidebarRoles": "Roller", "sidebarRoles": "Roller",
"sidebarShareableLinks": "Lenker", "sidebarShareableLinks": "Lenker",
"sidebarApiKeys": "API-nøkler", "sidebarApiKeys": "API-nøkler",
"sidebarProvisioning": "Levering",
"sidebarSettings": "Innstillinger", "sidebarSettings": "Innstillinger",
"sidebarAllUsers": "Alle brukere", "sidebarAllUsers": "Alle brukere",
"sidebarIdentityProviders": "Identitetsleverandører", "sidebarIdentityProviders": "Identitetsleverandører",
@@ -1375,167 +1250,8 @@
"sidebarGeneral": "Administrer", "sidebarGeneral": "Administrer",
"sidebarLogAndAnalytics": "Logg og analyser", "sidebarLogAndAnalytics": "Logg og analyser",
"sidebarBluePrints": "Tegninger", "sidebarBluePrints": "Tegninger",
"sidebarAlerting": "Varsling",
"sidebarHealthChecks": "Helsekontroller",
"sidebarOrganization": "Organisasjon", "sidebarOrganization": "Organisasjon",
"sidebarManagement": "Administrasjon",
"sidebarBillingAndLicenses": "Fakturering & lisenser",
"sidebarLogsAnalytics": "Analyser", "sidebarLogsAnalytics": "Analyser",
"alertingTitle": "Varsling",
"alertingDescription": "Definer kilder, triggere og handlinger for varsler",
"alertingRules": "Varslingsregler",
"alertingSearchRules": "Søk i regler…",
"alertingAddRule": "Opprett regel",
"alertingColumnSource": "Kilde",
"alertingColumnTrigger": "Utløser",
"alertingColumnActions": "Handlinger",
"alertingColumnEnabled": "Aktivert",
"alertingDeleteQuestion": "Vennligst bekreft at du vil slette denne varslingsregelen.",
"alertingDeleteRule": "Slett varslingsregel",
"alertingRuleDeleted": "Varslingsregel slettet",
"alertingRuleSaved": "Varslingsregel lagret",
"alertingRuleSavedCreatedDescription": "Din nye varslingsregel ble opprettet. Du kan fortsette å redigere den på denne siden.",
"alertingRuleSavedUpdatedDescription": "Endringene dine i denne varslingsregelen ble lagret.",
"alertingEditRule": "Rediger varslingsregel",
"alertingCreateRule": "Opprett varslingsregel",
"alertingRuleCredenzaDescription": "Velg hva som skal overvåkes, når det skal varsles, og hvordan du vil bli informert",
"alertingRuleNamePlaceholder": "Produksjonsside nede",
"alertingRuleEnabled": "Regel aktivert",
"alertingSectionSource": "Kilde",
"alertingSourceType": "Kildetype",
"alertingSourceSite": "Område",
"alertingSourceHealthCheck": "Helsekontroll",
"alertingPickSites": "Områder",
"alertingPickHealthChecks": "Helsekontroller",
"alertingPickResources": "Ressurser",
"alertingAllSites": "Alle områder",
"alertingAllSitesDescription": "Varsler for alle områder",
"alertingSpecificSites": "Spesifikke områder",
"alertingSpecificSitesDescription": "Velg spesifikke områder for overvåking",
"alertingAllHealthChecks": "Alle helsekontroller",
"alertingAllHealthChecksDescription": "Varsler for alle helsekontroller",
"alertingSpecificHealthChecks": "Spesifikke helsekontroller",
"alertingSpecificHealthChecksDescription": "Velg spesifikke helsekontroller for overvåking",
"alertingAllResources": "Alle ressurser",
"alertingAllResourcesDescription": "Varsler for alle ressurser",
"alertingSpecificResources": "Spesifikke ressurser",
"alertingSpecificResourcesDescription": "Velg spesifikke ressurser for overvåking",
"alertingSelectResources": "Velg ressurser…",
"alertingResourcesSelected": "{count} ressurser valgt",
"alertingResourcesEmpty": "Ingen ressurser med mål i de første 10 resultatene.",
"alertingSectionTrigger": "Utløser",
"alertingTrigger": "Når skal det varsles",
"alertingTriggerSiteOnline": "Nettsted er online",
"alertingTriggerSiteOffline": "Nettsted er offline",
"alertingTriggerSiteToggle": "Endringer i nettstedstatus",
"alertingTriggerHcHealthy": "Helsekontroll sunn",
"alertingTriggerHcUnhealthy": "Helsekontroll usunn",
"alertingTriggerHcToggle": "Endringer i helsekontrollstatus",
"alertingTriggerResourceHealthy": "Ressurs sunn",
"alertingTriggerResourceUnhealthy": "Ressurs usunn",
"alertingTriggerResourceDegraded": "Ressurs forringet",
"alertingSearchHealthChecks": "Søk i helsekontroller…",
"alertingHealthChecksEmpty": "Ingen tilgjengelige helsekontroller.",
"alertingTriggerResourceToggle": "Endringer i ressursstatus",
"alertingSourceResource": "Ressurs",
"alertingSectionActions": "Handlinger",
"alertingAddAction": "Legg til handling",
"alertingActionNotify": "E-post",
"alertingActionNotifyDescription": "Send e-postvarsler til brukere eller roller",
"alertingActionWebhook": "Webhook",
"alertingActionWebhookDescription": "Send en HTTP-forespørsel til et tilpasset endepunkt",
"alertingExternalIntegration": "Ekstern integrasjon",
"alertingExternalPagerDutyDescription": "Send varsler til PagerDuty for hendelseshåndtering",
"alertingExternalOpsgenieDescription": "Rute varsler til Opsgenie for vakt håndtering",
"alertingExternalServiceNowDescription": "Opprett ServiceNow hendelser fra varslingseventer",
"alertingExternalIncidentIoDescription": "Utløs Incident.io arbeidsflyter fra varsels begivenheter",
"alertingActionType": "Handlings type",
"alertingNotifyUsers": "Brukere",
"alertingNotifyRoles": "Roller",
"alertingNotifyEmails": "E-postadresser",
"alertingEmailPlaceholder": "Legg til e-post og trykk Enter",
"alertingWebhookMethod": "HTTP-metode",
"alertingWebhookSecret": "Signeringshemmelig (valgfritt)",
"alertingWebhookSecretPlaceholder": "HMAC-hemmelig",
"alertingWebhookHeaders": "Overskrifter",
"alertingAddHeader": "Legg til header",
"alertingSelectSites": "Velg områder…",
"alertingSitesSelected": "{count} områder valgt",
"alertingSelectHealthChecks": "Velg helsekontroller…",
"alertingHealthChecksSelected": "{count} helsekontroller valgt",
"alertingNoHealthChecks": "Ingen mål med helsekontroller aktivert",
"alertingHealthCheckStub": "Valg av helsekontrollkilde er ikke sluttført ennå - du kan fortsatt konfigurere triggere og handlinger.",
"alertingSelectUsers": "Velg brukere…",
"alertingUsersSelected": "{count} brukere valgt",
"alertingSelectRoles": "Velg roller…",
"alertingRolesSelected": "{count} roller valgt",
"alertingSummarySites": "Områder ({count})",
"alertingSummaryAllSites": "Alle områder",
"alertingSummaryHealthChecks": "Helsekontroller ({count})",
"alertingSummaryAllHealthChecks": "Alle helsekoner",
"alertingSummaryResources": "Ressurser ({count})",
"alertingSummaryAllResources": "Alle ressurser",
"alertingErrorNameRequired": "Skriv inn et navn",
"alertingErrorActionsMin": "Legg til minst én handling",
"alertingErrorPickSites": "Velg minst ett område",
"alertingErrorPickHealthChecks": "Velg minst én helsekontroll",
"alertingErrorPickResources": "Velg minst én ressurs",
"alertingErrorTriggerSite": "Velg en triggetjeneste for nettsted",
"alertingErrorTriggerHealth": "Velg en triggetjeneste for helsekontroll",
"alertingErrorTriggerResource": "Velg en triggetjeneste for ressurs",
"alertingErrorNotifyRecipients": "Velg brukere, roller, eller minst én e-post",
"alertingConfigureSource": "Konfigurer kilde",
"alertingConfigureTrigger": "Konfigurer trigger",
"alertingConfigureActions": "Konfigurer handlinger",
"alertingBackToRules": "Tilbake til regler",
"alertingRuleCooldown": "Nedkjøling (sekunder)",
"alertingRuleCooldownDescription": "Minimum tid mellom gjentatte varsler for samme regel. Sett til 0 for å skyte hver gang.",
"alertingDraftBadge": "Utkast - lagre for å lagre denne regelen",
"alertingSidebarHint": "Klikk på et steg på lerretet for å redigere det her.",
"alertingGraphCanvasTitle": "Regel Flyt",
"alertingGraphCanvasDescription": "Visuell oversikt over kilde, trigger og handlinger. Velg en node for å redigere den i panelet.",
"alertingNodeNotConfigured": "Ikke konfigurert ennå",
"alertingNodeActionsCount": "{count, plural, one {# handling} other {# handlinger}}",
"alertingNodeRoleSource": "Kilde",
"alertingNodeRoleTrigger": "Utløser",
"alertingNodeRoleAction": "Handling",
"alertingTabRules": "Varslingsregler",
"alertingTabHealthChecks": "Helsekontroller",
"alertingRulesBannerTitle": "Bli varslet",
"alertingRulesBannerDescription": "Hver regel binder sammen hva som skal overvåkes (et område, helsekontroll eller ressurs), når det skal varsles (for eksempel offline eller usunn), og hvordan varsle teamet ditt via e-post, webhooks eller integrasjoner. Bruk denne listen for å opprette, aktivere og administrere disse reglene.",
"alertingHealthChecksBannerTitle": "Overvåk helse & ressurser",
"alertingHealthChecksBannerDescription": "Helsekontroller er HTTP- eller TCP-monitorer du definerer én gang. Du kan deretter bruke dem som kilder i varslingsregler slik at du blir varslet når et mål blir sunt eller usunt. Helsekontroller på ressurser vises også her.",
"standaloneHcTableTitle": "Helsekontroller",
"standaloneHcSearchPlaceholder": "Søk i helsekontroller…",
"standaloneHcAddButton": "Opprett helsekontroll",
"standaloneHcCreateTitle": "Opprett helsekontroll",
"standaloneHcEditTitle": "Rediger helsekontroll",
"standaloneHcDescription": "Konfigurer en HTTP- eller TCP-helsekontroll for bruk i varslingsregler.",
"standaloneHcNameLabel": "Navn",
"standaloneHcNamePlaceholder": "Min HTTP-monitor",
"standaloneHcDeleteTitle": "Slett helsekontroll",
"standaloneHcDeleteQuestion": "Vennligst bekreft at du vil slette denne helsekontrollen.",
"standaloneHcDeleted": "Helsekontroll slettet",
"standaloneHcSaved": "Helsekontroll lagret",
"standaloneHcColumnHealth": "Helse",
"standaloneHcColumnMode": "Modus",
"standaloneHcColumnTarget": "Mål",
"standaloneHcHealthStateHealthy": "Sunn",
"standaloneHcHealthStateUnhealthy": "Usunn",
"standaloneHcHealthStateUnknown": "Ukjent",
"standaloneHcFilterAnySite": "Alle områder",
"standaloneHcFilterAnyResource": "Alle ressurser",
"standaloneHcFilterMode": "Modus",
"standaloneHcFilterModeHttp": "HTTP",
"standaloneHcFilterModeTcp": "TCP",
"standaloneHcFilterModeSnmp": "SNMP",
"standaloneHcFilterModePing": "Ping",
"standaloneHcFilterHealth": "Helse",
"standaloneHcFilterEnabled": "Aktivert",
"standaloneHcFilterEnabledOn": "Aktivert",
"standaloneHcFilterEnabledOff": "Deaktivert",
"standaloneHcFilterSiteIdFallback": "Område {id}",
"standaloneHcFilterResourceIdFallback": "Ressurs {id}",
"blueprints": "Tegninger", "blueprints": "Tegninger",
"blueprintsDescription": "Bruk deklarative konfigurasjoner og vis tidligere kjøringer", "blueprintsDescription": "Bruk deklarative konfigurasjoner og vis tidligere kjøringer",
"blueprintAdd": "Legg til blåkopi", "blueprintAdd": "Legg til blåkopi",
@@ -1556,6 +1272,7 @@
"parsedContents": "Parastinnhold (kun lese)", "parsedContents": "Parastinnhold (kun lese)",
"enableDockerSocket": "Aktiver Docker blåkopi", "enableDockerSocket": "Aktiver Docker blåkopi",
"enableDockerSocketDescription": "Aktiver skraping av Docker Socket for blueprint Etiketter. Socket bane må brukes for nye.", "enableDockerSocketDescription": "Aktiver skraping av Docker Socket for blueprint Etiketter. Socket bane må brukes for nye.",
"enableDockerSocketLink": "Lær mer",
"viewDockerContainers": "Vis Docker-containere", "viewDockerContainers": "Vis Docker-containere",
"containersIn": "Containere i {siteName}", "containersIn": "Containere i {siteName}",
"selectContainerDescription": "Velg en hvilken som helst container for å bruke som vertsnavn for dette målet. Klikk på en port for å bruke en port.", "selectContainerDescription": "Velg en hvilken som helst container for å bruke som vertsnavn for dette målet. Klikk på en port for å bruke en port.",
@@ -1597,8 +1314,7 @@
"initialSetupDescription": "Opprett den første serveradministratorkontoen. Det kan bare finnes én serveradministrator. Du kan alltid endre denne påloggingsinformasjonen senere.", "initialSetupDescription": "Opprett den første serveradministratorkontoen. Det kan bare finnes én serveradministrator. Du kan alltid endre denne påloggingsinformasjonen senere.",
"createAdminAccount": "Opprett administratorkonto", "createAdminAccount": "Opprett administratorkonto",
"setupErrorCreateAdmin": "En feil oppstod under opprettelsen av serveradministratorkontoen.", "setupErrorCreateAdmin": "En feil oppstod under opprettelsen av serveradministratorkontoen.",
"certificateStatus": "Sertifikat", "certificateStatus": "Sertifikatstatus",
"certificateStatusAutoRefreshHint": "Status oppdateres automatisk.",
"loading": "Laster inn", "loading": "Laster inn",
"loadingAnalytics": "Laster inn analyser", "loadingAnalytics": "Laster inn analyser",
"restart": "Start på nytt", "restart": "Start på nytt",
@@ -1667,7 +1383,6 @@
"pangolinUpdateAvailableReleaseNotes": "Se utgivelsesnotater", "pangolinUpdateAvailableReleaseNotes": "Se utgivelsesnotater",
"newtUpdateAvailable": "Oppdatering tilgjengelig", "newtUpdateAvailable": "Oppdatering tilgjengelig",
"newtUpdateAvailableInfo": "En ny versjon av Newt er tilgjengelig. Vennligst oppdater til den nyeste versjonen for den beste opplevelsen.", "newtUpdateAvailableInfo": "En ny versjon av Newt er tilgjengelig. Vennligst oppdater til den nyeste versjonen for den beste opplevelsen.",
"pangolinNodeUpdateAvailableInfo": "En ny versjon av Pangolin Node er tilgjengelig. Vennligst oppdater til den nyeste versjonen for den beste opplevelsen.",
"domainPickerEnterDomain": "Domene", "domainPickerEnterDomain": "Domene",
"domainPickerPlaceholder": "minapp.eksempel.no", "domainPickerPlaceholder": "minapp.eksempel.no",
"domainPickerDescription": "Skriv inn hele domenet til ressursen for å se tilgjengelige alternativer.", "domainPickerDescription": "Skriv inn hele domenet til ressursen for å se tilgjengelige alternativer.",
@@ -1685,7 +1400,6 @@
"domainPickerNamespace": "Navnerom: {namespace}", "domainPickerNamespace": "Navnerom: {namespace}",
"domainPickerShowMore": "Vis mer", "domainPickerShowMore": "Vis mer",
"regionSelectorTitle": "Velg Region", "regionSelectorTitle": "Velg Region",
"domainPickerRemoteExitNodeWarning": "Tilbudte domener støttes ikke når sider kobles til eksterne avkjøringsnoder. For ressurser som skal være tilgjengelige på eksterne noder, brukes et egendefinert domene i stedet.",
"regionSelectorInfo": "Å velge en region hjelper oss med å gi bedre ytelse for din lokasjon. Du trenger ikke være i samme region som serveren.", "regionSelectorInfo": "Å velge en region hjelper oss med å gi bedre ytelse for din lokasjon. Du trenger ikke være i samme region som serveren.",
"regionSelectorPlaceholder": "Velg en region", "regionSelectorPlaceholder": "Velg en region",
"regionSelectorComingSoon": "Kommer snart", "regionSelectorComingSoon": "Kommer snart",
@@ -1698,7 +1412,6 @@
"billingSites": "Områder", "billingSites": "Områder",
"billingUsers": "Brukere", "billingUsers": "Brukere",
"billingDomains": "Domener", "billingDomains": "Domener",
"billingOrganizations": "Orger",
"billingRemoteExitNodes": "Eksterne Noder", "billingRemoteExitNodes": "Eksterne Noder",
"billingNoLimitConfigured": "Ingen grense konfigurert", "billingNoLimitConfigured": "Ingen grense konfigurert",
"billingEstimatedPeriod": "Estimert faktureringsperiode", "billingEstimatedPeriod": "Estimert faktureringsperiode",
@@ -1741,7 +1454,6 @@
"failed": "Mislyktes", "failed": "Mislyktes",
"createNewOrgDescription": "Opprett en ny organisasjon", "createNewOrgDescription": "Opprett en ny organisasjon",
"organization": "Organisasjon", "organization": "Organisasjon",
"primary": "Primær",
"port": "Port", "port": "Port",
"securityKeyManage": "Administrer sikkerhetsnøkler", "securityKeyManage": "Administrer sikkerhetsnøkler",
"securityKeyDescription": "Legg til eller fjern sikkerhetsnøkler for passordløs autentisering", "securityKeyDescription": "Legg til eller fjern sikkerhetsnøkler for passordløs autentisering",
@@ -1839,16 +1551,6 @@
"billingFeatureLossWarning": "Fremhev tilgjengelig varsel", "billingFeatureLossWarning": "Fremhev tilgjengelig varsel",
"billingFeatureLossDescription": "Ved å nedgradere vil funksjoner som ikke er tilgjengelige i den nye planen automatisk bli deaktivert. Noen innstillinger og konfigurasjoner kan gå tapt. Vennligst gjennomgå prismatrisen for å forstå hvilke funksjoner som ikke lenger vil være tilgjengelige.", "billingFeatureLossDescription": "Ved å nedgradere vil funksjoner som ikke er tilgjengelige i den nye planen automatisk bli deaktivert. Noen innstillinger og konfigurasjoner kan gå tapt. Vennligst gjennomgå prismatrisen for å forstå hvilke funksjoner som ikke lenger vil være tilgjengelige.",
"billingUsageExceedsLimit": "Gjeldende bruk ({current}) overskrider grensen ({limit})", "billingUsageExceedsLimit": "Gjeldende bruk ({current}) overskrider grensen ({limit})",
"billingPastDueTitle": "Betalingen har forfalt",
"billingPastDueDescription": "Betalingen er forfalt. Vennligst oppdater betalingsmetoden din for å fortsette å bruke den gjeldende funksjonsplanen din. Hvis du ikke har løst deg, vil abonnementet ditt avbrytes, og du vil bli tilbakestilt til gratistiden.",
"billingUnpaidTitle": "Abonnement ubetalt",
"billingUnpaidDescription": "Ditt abonnement er ubetalt og du har blitt tilbakestilt til gratis kasse. Vennligst oppdater din betalingsmetode for å gjenopprette abonnementet.",
"billingIncompleteTitle": "Betaling ufullstendig",
"billingIncompleteDescription": "Betalingen er ufullstendig. Vennligst fullfør betalingsprosessen for å aktivere abonnementet.",
"billingIncompleteExpiredTitle": "Betaling utløpt",
"billingIncompleteExpiredDescription": "Din betaling ble aldri fullført, og har utløpt. Du har blitt tilbakestilt til gratis dekk. Vennligst abonner på nytt for å gjenopprette tilgangen til betalte funksjoner.",
"billingManageSubscription": "Administrere ditt abonnement",
"billingResolvePaymentIssue": "Vennligst løs ditt betalingsproblem før du oppgraderer eller nedgraderer betalingen",
"signUpTerms": { "signUpTerms": {
"IAgreeToThe": "Jeg godtar", "IAgreeToThe": "Jeg godtar",
"termsOfService": "brukervilkårene", "termsOfService": "brukervilkårene",
@@ -1907,7 +1609,6 @@
"configureHealthCheck": "Konfigurer Helsekontroll", "configureHealthCheck": "Konfigurer Helsekontroll",
"configureHealthCheckDescription": "Sett opp helsekontroll for {target}", "configureHealthCheckDescription": "Sett opp helsekontroll for {target}",
"enableHealthChecks": "Aktiver Helsekontroller", "enableHealthChecks": "Aktiver Helsekontroller",
"healthCheckDisabledStateDescription": "Når deaktivert, vil ikke nettstedet utføre helsekontroller, og tilstanden vil anses som ukjent.",
"enableHealthChecksDescription": "Overvåk helsen til dette målet. Du kan overvåke et annet endepunkt enn målet hvis nødvendig.", "enableHealthChecksDescription": "Overvåk helsen til dette målet. Du kan overvåke et annet endepunkt enn målet hvis nødvendig.",
"healthScheme": "Metode", "healthScheme": "Metode",
"healthSelectScheme": "Velg metode", "healthSelectScheme": "Velg metode",
@@ -1923,24 +1624,6 @@
"timeIsInSeconds": "Tid er i sekunder", "timeIsInSeconds": "Tid er i sekunder",
"requireDeviceApproval": "Krev enhetsgodkjenning", "requireDeviceApproval": "Krev enhetsgodkjenning",
"requireDeviceApprovalDescription": "Brukere med denne rollen trenger nye enheter godkjent av en admin før de kan koble seg og få tilgang til ressurser.", "requireDeviceApprovalDescription": "Brukere med denne rollen trenger nye enheter godkjent av en admin før de kan koble seg og få tilgang til ressurser.",
"sshAccess": "SSH tilgang",
"roleAllowSsh": "Tillat SSH",
"roleAllowSshAllow": "Tillat",
"roleAllowSshDisallow": "Forby",
"roleAllowSshDescription": "Tillat brukere med denne rollen å koble til ressurser via SSH. Når deaktivert får rollen ikke tilgang til SSH.",
"sshSudoMode": "Sudo tilgang",
"sshSudoModeNone": "Ingen",
"sshSudoModeNoneDescription": "Brukeren kan ikke kjøre kommandoer med sudo.",
"sshSudoModeFull": "Full Sudo",
"sshSudoModeFullDescription": "Brukeren kan kjøre hvilken som helst kommando med sudo.",
"sshSudoModeCommands": "Kommandoer",
"sshSudoModeCommandsDescription": "Brukeren kan bare kjøre de angitte kommandoene med sudo.",
"sshSudo": "Tillat sudo",
"sshSudoCommands": "Sudo kommandoer",
"sshSudoCommandsDescription": "Kommaseparert liste med kommandoer brukeren kan kjøre med sudo.",
"sshCreateHomeDir": "Opprett hjemmappe",
"sshUnixGroups": "Unix grupper",
"sshUnixGroupsDescription": "Kommaseparerte Unix grupper for å legge brukeren til på mål-verten.",
"retryAttempts": "Forsøk på nytt", "retryAttempts": "Forsøk på nytt",
"expectedResponseCodes": "Forventede svarkoder", "expectedResponseCodes": "Forventede svarkoder",
"expectedResponseCodesDescription": "HTTP-statuskode som indikerer sunn status. Hvis den blir stående tom, regnes 200-300 som sunn.", "expectedResponseCodesDescription": "HTTP-statuskode som indikerer sunn status. Hvis den blir stående tom, regnes 200-300 som sunn.",
@@ -1957,20 +1640,9 @@
"healthCheckIntervalMin": "Sjekkeintervallet må være minst 5 sekunder", "healthCheckIntervalMin": "Sjekkeintervallet må være minst 5 sekunder",
"healthCheckTimeoutMin": "Timeout må være minst 1 sekund", "healthCheckTimeoutMin": "Timeout må være minst 1 sekund",
"healthCheckRetryMin": "Forsøk på nytt må være minst 1", "healthCheckRetryMin": "Forsøk på nytt må være minst 1",
"healthCheckMode": "Sjekk modus",
"healthCheckStrategy": "Strategi",
"healthCheckModeDescription": "TCP-modus verifiserer kun tilkobling. HTTP-modus validerer HTTP-responsen.",
"healthyThreshold": "Sunnhets terskel",
"healthyThresholdDescription": "Suksesser på rad som kreves før man markerer som sunn.",
"unhealthyThreshold": "Usunn terskel",
"unhealthyThresholdDescription": "Feil på rad som kreves før man markerer som usunn.",
"healthCheckHealthyThresholdMin": "Sunnhet terskel må være minst 1",
"healthCheckUnhealthyThresholdMin": "Usunn terskel må være minst 1",
"httpMethod": "HTTP-metode", "httpMethod": "HTTP-metode",
"selectHttpMethod": "Velg HTTP-metode", "selectHttpMethod": "Velg HTTP-metode",
"domainPickerSubdomainLabel": "Underdomene", "domainPickerSubdomainLabel": "Underdomene",
"domainPickerWildcard": "Jokertegn",
"domainPickerWildcardPaidOnly": "Jokertegnsubdomener er en betalt funksjon. Vennligst oppgrader for å få tilgang til denne funksjonen.",
"domainPickerBaseDomainLabel": "Grunndomene", "domainPickerBaseDomainLabel": "Grunndomene",
"domainPickerSearchDomains": "Søk i domener...", "domainPickerSearchDomains": "Søk i domener...",
"domainPickerNoDomainsFound": "Ingen domener funnet", "domainPickerNoDomainsFound": "Ingen domener funnet",
@@ -1996,12 +1668,12 @@
"resourcesTableAliasAddressInfo": "Denne adressen er en del av organisasjonens undernettverk. Den brukes til å løse aliasposter ved hjelp av intern DNS-oppløsning.", "resourcesTableAliasAddressInfo": "Denne adressen er en del av organisasjonens undernettverk. Den brukes til å løse aliasposter ved hjelp av intern DNS-oppløsning.",
"resourcesTableClients": "Klienter", "resourcesTableClients": "Klienter",
"resourcesTableAndOnlyAccessibleInternally": "og er kun tilgjengelig internt når de er koblet til med en klient.", "resourcesTableAndOnlyAccessibleInternally": "og er kun tilgjengelig internt når de er koblet til med en klient.",
"resourcesTableNoTargets": "Ingen mål",
"resourcesTableHealthy": "Frisk", "resourcesTableHealthy": "Frisk",
"resourcesTableDegraded": "Nedgradert", "resourcesTableDegraded": "Nedgradert",
"resourcesTableUnhealthy": "Usunn", "resourcesTableOffline": "Frakoblet",
"resourcesTableUnknown": "Ukjent", "resourcesTableUnknown": "Ukjent",
"resourcesTableNotMonitored": "Ikke overvåket", "resourcesTableNotMonitored": "Ikke overvåket",
"resourcesTableNoTargets": "Ingen mål",
"editInternalResourceDialogEditClientResource": "Rediger Private Ressurser", "editInternalResourceDialogEditClientResource": "Rediger Private Ressurser",
"editInternalResourceDialogUpdateResourceProperties": "Oppdater ressurskonfigurasjonen og få tilgangskontroller for {resourceName}", "editInternalResourceDialogUpdateResourceProperties": "Oppdater ressurskonfigurasjonen og få tilgangskontroller for {resourceName}",
"editInternalResourceDialogResourceProperties": "Ressursegenskaper", "editInternalResourceDialogResourceProperties": "Ressursegenskaper",
@@ -2027,11 +1699,6 @@
"editInternalResourceDialogModePort": "Port", "editInternalResourceDialogModePort": "Port",
"editInternalResourceDialogModeHost": "Vert", "editInternalResourceDialogModeHost": "Vert",
"editInternalResourceDialogModeCidr": "CIDR", "editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogScheme": "Skjema",
"editInternalResourceDialogEnableSsl": "Aktiver SSL",
"editInternalResourceDialogEnableSslDescription": "Aktiver SSL/TLS-kryptering for sikre HTTPS-tilkoblinger til destinasjonen.",
"editInternalResourceDialogDestination": "Destinasjon", "editInternalResourceDialogDestination": "Destinasjon",
"editInternalResourceDialogDestinationHostDescription": "IP-adressen eller vertsnavnet til ressursen på nettstedets nettverk.", "editInternalResourceDialogDestinationHostDescription": "IP-adressen eller vertsnavnet til ressursen på nettstedets nettverk.",
"editInternalResourceDialogDestinationIPDescription": "IP eller vertsnavn til ressursen på nettstedets nettverk.", "editInternalResourceDialogDestinationIPDescription": "IP eller vertsnavn til ressursen på nettstedets nettverk.",
@@ -2047,7 +1714,6 @@
"createInternalResourceDialogName": "Navn", "createInternalResourceDialogName": "Navn",
"createInternalResourceDialogSite": "Område", "createInternalResourceDialogSite": "Område",
"selectSite": "Velg område...", "selectSite": "Velg område...",
"multiSitesSelectorSitesCount": "{count, plural, one {# sted} other {# steder}}",
"noSitesFound": "Ingen områder funnet.", "noSitesFound": "Ingen områder funnet.",
"createInternalResourceDialogProtocol": "Protokoll", "createInternalResourceDialogProtocol": "Protokoll",
"createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogTcp": "TCP",
@@ -2076,19 +1742,11 @@
"createInternalResourceDialogModePort": "Port", "createInternalResourceDialogModePort": "Port",
"createInternalResourceDialogModeHost": "Vert", "createInternalResourceDialogModeHost": "Vert",
"createInternalResourceDialogModeCidr": "CIDR", "createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"scheme": "Skjema",
"createInternalResourceDialogScheme": "Skjema",
"createInternalResourceDialogEnableSsl": "Aktiver SSL",
"createInternalResourceDialogEnableSslDescription": "Aktiver SSL/TLS-kryptering for sikre HTTPS-tilkoblinger til destinasjonen.",
"createInternalResourceDialogDestination": "Destinasjon", "createInternalResourceDialogDestination": "Destinasjon",
"createInternalResourceDialogDestinationHostDescription": "IP-adressen eller vertsnavnet til ressursen på nettstedets nettverk.", "createInternalResourceDialogDestinationHostDescription": "IP-adressen eller vertsnavnet til ressursen på nettstedets nettverk.",
"createInternalResourceDialogDestinationCidrDescription": "CIDR-rekkevidden til ressursen på nettstedets nettverk.", "createInternalResourceDialogDestinationCidrDescription": "CIDR-rekkevidden til ressursen på nettstedets nettverk.",
"createInternalResourceDialogAlias": "Alias", "createInternalResourceDialogAlias": "Alias",
"createInternalResourceDialogAliasDescription": "Et valgfritt internt DNS-alias for denne ressursen.", "createInternalResourceDialogAliasDescription": "Et valgfritt internt DNS-alias for denne ressursen.",
"internalResourceDownstreamSchemeRequired": "Skjema er påkrevd for HTTP-ressurser",
"internalResourceHttpPortRequired": "Destinasjonsport er nødvendig for HTTP-ressurser",
"siteConfiguration": "Konfigurasjon", "siteConfiguration": "Konfigurasjon",
"siteAcceptClientConnections": "Godta klientforbindelser", "siteAcceptClientConnections": "Godta klientforbindelser",
"siteAcceptClientConnectionsDescription": "Tillat brukere og klienter å få tilgang til ressurser på denne siden. Dette kan endres senere.", "siteAcceptClientConnectionsDescription": "Tillat brukere og klienter å få tilgang til ressurser på denne siden. Dette kan endres senere.",
@@ -2174,40 +1832,6 @@
"exitNode": "Utgangsnode", "exitNode": "Utgangsnode",
"country": "Land", "country": "Land",
"rulesMatchCountry": "For tiden basert på kilde IP", "rulesMatchCountry": "For tiden basert på kilde IP",
"region": "Fylke",
"selectRegion": "Velg region",
"searchRegions": "Søk etter områder...",
"noRegionFound": "Ingen region funnet.",
"rulesMatchRegion": "Velg en regional gruppering av land",
"rulesErrorInvalidRegion": "Ugyldig område",
"rulesErrorInvalidRegionDescription": "Vennligst velg et gyldig område.",
"regionAfrica": "Afrika",
"regionNorthernAfrica": "[country name] Nord-Afrika",
"regionEasternAfrica": "Øst-Afrika",
"regionMiddleAfrica": "Middle Africa",
"regionSouthernAfrica": "Sør-Afrika",
"regionWesternAfrica": "[country name] Vest-Afrika",
"regionAmericas": "Amerika",
"regionCaribbean": "Karibia",
"regionCentralAmerica": "Sentral-Amerika",
"regionSouthAmerica": "Sør-Amerika",
"regionNorthernAmerica": "Nord-Amerika",
"regionAsia": "Asia",
"regionCentralAsia": "Sentral-Asia",
"regionEasternAsia": "Øst-Asia",
"regionSouthEasternAsia": "Sørøst-Asia",
"regionSouthernAsia": "Sørlige Asia",
"regionWesternAsia": "Vest-Asia",
"regionEurope": "Europa",
"regionEasternEurope": "Øst-Europa",
"regionNorthernEurope": "Nord-Europa",
"regionSouthernEurope": "Sørlige Europa",
"regionWesternEurope": "Vest-Europa",
"regionOceania": "Oceania",
"regionAustraliaAndNewZealand": "Australia og New Zealand",
"regionMelanesia": "Melanesia",
"regionMicronesia": "Micronesia",
"regionPolynesia": "Polynesia",
"managedSelfHosted": { "managedSelfHosted": {
"title": "Administrert selv-hostet", "title": "Administrert selv-hostet",
"description": "Sikre og lavvedlikeholdsservere, selvbetjente Pangolin med ekstra klokker, og understell", "description": "Sikre og lavvedlikeholdsservere, selvbetjente Pangolin med ekstra klokker, og understell",
@@ -2246,7 +1870,7 @@
}, },
"internationaldomaindetected": "Internasjonalt domene oppdaget", "internationaldomaindetected": "Internasjonalt domene oppdaget",
"willbestoredas": "Vil bli lagret som:", "willbestoredas": "Vil bli lagret som:",
"roleMappingDescription": "Bestem hvordan roller tildeles brukere når de logger inn med denne identitetsleverandøren.", "roleMappingDescription": "Bestem hvordan roller tilordnes brukere når innloggingen er aktivert når autog-rapportering er aktivert.",
"selectRole": "Velg en rolle", "selectRole": "Velg en rolle",
"roleMappingExpression": "Uttrykk", "roleMappingExpression": "Uttrykk",
"selectRolePlaceholder": "Velg en rolle", "selectRolePlaceholder": "Velg en rolle",
@@ -2256,25 +1880,6 @@
"invalidValue": "Ugyldig verdi", "invalidValue": "Ugyldig verdi",
"idpTypeLabel": "Identitet leverandør type", "idpTypeLabel": "Identitet leverandør type",
"roleMappingExpressionPlaceholder": "F.eks. inneholder(grupper, 'admin') && 'Admin' ⋅'Medlem'", "roleMappingExpressionPlaceholder": "F.eks. inneholder(grupper, 'admin') && 'Admin' ⋅'Medlem'",
"roleMappingModeFixedRoles": "Fast roller",
"roleMappingModeMappingBuilder": "Kartlegger bygger",
"roleMappingModeRawExpression": "Rå uttrykk",
"roleMappingFixedRolesPlaceholderSelect": "Velg en eller flere roller",
"roleMappingFixedRolesPlaceholderFreeform": "Skriv inn rollenavn (eksakt treff per organisasjon)",
"roleMappingFixedRolesDescriptionSameForAll": "Tilordne den samme rollen som er satt til hver automatisk midlertidig bruker.",
"roleMappingFixedRolesDescriptionDefaultPolicy": "For standard policyer, type rollenavn som eksisterer i hver organisasjon der brukerne tilbys. Navn må stemmer nøyaktig.",
"roleMappingClaimPath": "Krev sti",
"roleMappingClaimPathPlaceholder": "grupper",
"roleMappingClaimPathDescription": "Sti i i token nyttelast som inneholder kildeverdier (for eksempel grupper).",
"roleMappingMatchValue": "Treff verdi",
"roleMappingAssignRoles": "Tilordne roller",
"roleMappingAddMappingRule": "Legg til tilordningsregel",
"roleMappingRawExpressionResultDescription": "Uttrykk skal vurderes til en streng eller en tekststreng.",
"roleMappingRawExpressionResultDescriptionSingleRole": "Uttrykk må evaluere til en streng (en rollenavn).",
"roleMappingMatchValuePlaceholder": "Match verdi (for eksempel: admin)",
"roleMappingAssignRolesPlaceholderFreeform": "Angi rollenavn (eksakt per org)",
"roleMappingBuilderFreeformRowHint": "Rollenavn må samsvare med en rolle i hver målorganisasjon.",
"roleMappingRemoveRule": "Fjern",
"idpGoogleConfiguration": "Google Konfigurasjon", "idpGoogleConfiguration": "Google Konfigurasjon",
"idpGoogleConfigurationDescription": "Konfigurer Google OAuth2 legitimasjonen", "idpGoogleConfigurationDescription": "Konfigurer Google OAuth2 legitimasjonen",
"idpGoogleClientIdDescription": "Google OAuth2 Client ID", "idpGoogleClientIdDescription": "Google OAuth2 Client ID",
@@ -2311,9 +1916,6 @@
"authPageBrandingQuestionRemove": "Er du sikker på at du vil fjerne merkevarebyggingen for autentiseringssider?", "authPageBrandingQuestionRemove": "Er du sikker på at du vil fjerne merkevarebyggingen for autentiseringssider?",
"authPageBrandingDeleteConfirm": "Bekreft sletting av merkevarebygging", "authPageBrandingDeleteConfirm": "Bekreft sletting av merkevarebygging",
"brandingLogoURL": "Logo URL", "brandingLogoURL": "Logo URL",
"brandingLogoURLOrPath": "Logoen URL eller sti",
"brandingLogoPathDescription": "Skriv inn en URL eller en lokal bane.",
"brandingLogoURLDescription": "Skriv inn en offentlig tilgjengelig nettadresse til din logobilde.",
"brandingPrimaryColor": "Primærfarge", "brandingPrimaryColor": "Primærfarge",
"brandingLogoWidth": "Bredde (px)", "brandingLogoWidth": "Bredde (px)",
"brandingLogoHeight": "Høyde (px)", "brandingLogoHeight": "Høyde (px)",
@@ -2338,11 +1940,9 @@
"selectDomainForOrgAuthPage": "Velg et domene for organisasjonens autentiseringsside", "selectDomainForOrgAuthPage": "Velg et domene for organisasjonens autentiseringsside",
"domainPickerProvidedDomain": "Gitt domene", "domainPickerProvidedDomain": "Gitt domene",
"domainPickerFreeProvidedDomain": "Gratis oppgitt domene", "domainPickerFreeProvidedDomain": "Gratis oppgitt domene",
"domainPickerFreeDomainsPaidFeature": "Angitte domener er en betalingsfunksjon. Abonner for å få et domene inkludert i din plan ingen behov for å ta med ditt eget.",
"domainPickerVerified": "Bekreftet", "domainPickerVerified": "Bekreftet",
"domainPickerUnverified": "Uverifisert", "domainPickerUnverified": "Uverifisert",
"domainPickerManual": "Manuell", "domainPickerInvalidSubdomainStructure": "Dette underdomenet inneholder ugyldige tegn eller struktur. Det vil automatisk bli utsatt når du lagrer.",
"domainPickerInvalidSubdomainStructure": "Ugyldige tegn vil bli sanitert når de er lagret.",
"domainPickerError": "Feil", "domainPickerError": "Feil",
"domainPickerErrorLoadDomains": "Kan ikke laste organisasjonens domener", "domainPickerErrorLoadDomains": "Kan ikke laste organisasjonens domener",
"domainPickerErrorCheckAvailability": "Kunne ikke kontrollere domenetilgjengelighet", "domainPickerErrorCheckAvailability": "Kunne ikke kontrollere domenetilgjengelighet",
@@ -2355,7 +1955,7 @@
"orgAuthChooseIdpDescription": "Velg din identitet leverandør for å fortsette", "orgAuthChooseIdpDescription": "Velg din identitet leverandør for å fortsette",
"orgAuthNoIdpConfigured": "Denne organisasjonen har ikke noen identitetstjeneste konfigurert. Du kan i stedet logge inn med Pangolin identiteten din.", "orgAuthNoIdpConfigured": "Denne organisasjonen har ikke noen identitetstjeneste konfigurert. Du kan i stedet logge inn med Pangolin identiteten din.",
"orgAuthSignInWithPangolin": "Logg inn med Pangolin", "orgAuthSignInWithPangolin": "Logg inn med Pangolin",
"orgAuthSignInToOrg": "Organisasjonens identitetsleverandør (SSO)", "orgAuthSignInToOrg": "Logg inn på en organisasjon",
"orgAuthSelectOrgTitle": "Organisasjonsinnlogging", "orgAuthSelectOrgTitle": "Organisasjonsinnlogging",
"orgAuthSelectOrgDescription": "Skriv inn organisasjons-ID-en din for å fortsette", "orgAuthSelectOrgDescription": "Skriv inn organisasjons-ID-en din for å fortsette",
"orgAuthOrgIdPlaceholder": "din-organisasjon", "orgAuthOrgIdPlaceholder": "din-organisasjon",
@@ -2571,10 +2171,10 @@
}, },
"scale": { "scale": {
"title": "Skala", "title": "Skala",
"description": "Funksjoner for bedrifter, 50 brukere, 100 nettsteder og prioritert support." "description": "Enterprise features, 50 brukere, 50 nettsteder og prioritetsstøtte."
} }
}, },
"personalUseOnly": "Kun personlig bruk (gratis lisens - ingen kasse)", "personalUseOnly": "Kun personlig bruk (gratis lisens - ingen utsjekking)",
"buttons": { "buttons": {
"continueToCheckout": "Fortsett til kassen" "continueToCheckout": "Fortsett til kassen"
}, },
@@ -2648,7 +2248,6 @@
"validPassword": "Gyldig passord", "validPassword": "Gyldig passord",
"validEmail": "Valid email", "validEmail": "Valid email",
"validSSO": "Valid SSO", "validSSO": "Valid SSO",
"connectedClient": "Tilkoblet klient",
"resourceBlocked": "Ressurs blokkert", "resourceBlocked": "Ressurs blokkert",
"droppedByRule": "Legg i regelen", "droppedByRule": "Legg i regelen",
"noSessions": "Ingen økter", "noSessions": "Ingen økter",
@@ -2674,8 +2273,6 @@
"logRetentionAccessDescription": "Hvor lenge du vil beholde adgangslogger", "logRetentionAccessDescription": "Hvor lenge du vil beholde adgangslogger",
"logRetentionActionLabel": "Handlings logg nytt", "logRetentionActionLabel": "Handlings logg nytt",
"logRetentionActionDescription": "Hvor lenge handlingen skal lagres", "logRetentionActionDescription": "Hvor lenge handlingen skal lagres",
"logRetentionConnectionLabel": "Logg nyhet",
"logRetentionConnectionDescription": "Hvor lenge du vil beholde tilkoblingslogger",
"logRetentionDisabled": "Deaktivert", "logRetentionDisabled": "Deaktivert",
"logRetention3Days": "3 dager", "logRetention3Days": "3 dager",
"logRetention7Days": "7 dager", "logRetention7Days": "7 dager",
@@ -2686,15 +2283,8 @@
"logRetentionEndOfFollowingYear": "Slutt på neste år", "logRetentionEndOfFollowingYear": "Slutt på neste år",
"actionLogsDescription": "Vis historikk for handlinger som er utført i denne organisasjonen", "actionLogsDescription": "Vis historikk for handlinger som er utført i denne organisasjonen",
"accessLogsDescription": "Vis autoriseringsforespørsler for ressurser i denne organisasjonen", "accessLogsDescription": "Vis autoriseringsforespørsler for ressurser i denne organisasjonen",
"connectionLogs": "Loggfiler for tilkobling", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"connectionLogsDescription": "Vis tilkoblingslogger for tunneler i denne organisasjonen", "ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"sidebarLogsConnection": "Loggfiler for tilkobling",
"sidebarLogsStreaming": "Strømming",
"sourceAddress": "Kilde adresse",
"destinationAddress": "Måladresse (Automatic Translation)",
"duration": "Varighet",
"licenseRequiredToUse": "En <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lisens eller <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> er påkrevd for å bruke denne funksjonen. <bookADemoLink>Bestill en demo eller POC prøveversjon</bookADemoLink>.",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> er nødvendig for å bruke denne funksjonen. Denne funksjonen er også tilgjengelig i <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Bestill en demo eller POC studie</bookADemoLink>.",
"certResolver": "Sertifikat løser", "certResolver": "Sertifikat løser",
"certResolverDescription": "Velg sertifikatløser som skal brukes for denne ressursen.", "certResolverDescription": "Velg sertifikatløser som skal brukes for denne ressursen.",
"selectCertResolver": "Velg sertifikatløser", "selectCertResolver": "Velg sertifikatløser",
@@ -2836,9 +2426,6 @@
"machineClients": "Maskinklienter", "machineClients": "Maskinklienter",
"install": "Installer", "install": "Installer",
"run": "Kjør", "run": "Kjør",
"envFile": "Miljøfil",
"serviceFile": "Tjenestefil",
"enableAndStart": "Aktiver og start",
"clientNameDescription": "Visningsnavnet til klienten som kan endres senere.", "clientNameDescription": "Visningsnavnet til klienten som kan endres senere.",
"clientAddress": "Klientadresse (avansert)", "clientAddress": "Klientadresse (avansert)",
"setupFailedToFetchSubnet": "Kunne ikke hente standard undernett", "setupFailedToFetchSubnet": "Kunne ikke hente standard undernett",
@@ -2887,30 +2474,13 @@
"editInternalResourceDialogAddClients": "Legg til klienter", "editInternalResourceDialogAddClients": "Legg til klienter",
"editInternalResourceDialogDestinationLabel": "Destinasjon", "editInternalResourceDialogDestinationLabel": "Destinasjon",
"editInternalResourceDialogDestinationDescription": "Spesifiser destinasjonsadressen for den interne ressursen. Dette kan være et vertsnavn, IP-adresse eller CIDR-sjikt avhengig av valgt modus. Valgfrie oppsett av intern DNS-alias for enklere identifikasjon.", "editInternalResourceDialogDestinationDescription": "Spesifiser destinasjonsadressen for den interne ressursen. Dette kan være et vertsnavn, IP-adresse eller CIDR-sjikt avhengig av valgt modus. Valgfrie oppsett av intern DNS-alias for enklere identifikasjon.",
"internalResourceFormMultiSiteRoutingHelp": "Valg av flere nettsteder muliggjør motstandskraftig ruting og failover for høy tilgjengelighet.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Lær mer",
"editInternalResourceDialogPortRestrictionsDescription": "Begrens tilgang til spesifikke TCP/UDP-porter eller tillate/blokkere alle porter.", "editInternalResourceDialogPortRestrictionsDescription": "Begrens tilgang til spesifikke TCP/UDP-porter eller tillate/blokkere alle porter.",
"createInternalResourceDialogHttpConfiguration": "HTTP-konfigurasjon",
"createInternalResourceDialogHttpConfigurationDescription": "Velg domenet klienter vil bruke for å nå denne ressursen via HTTP eller HTTPS.",
"editInternalResourceDialogHttpConfiguration": "HTTP-konfigurasjon",
"editInternalResourceDialogHttpConfigurationDescription": "Velg domenet klienter vil bruke for å nå denne ressursen via HTTP eller HTTPS.",
"editInternalResourceDialogTcp": "TCP", "editInternalResourceDialogTcp": "TCP",
"editInternalResourceDialogUdp": "UDP", "editInternalResourceDialogUdp": "UDP",
"editInternalResourceDialogIcmp": "ICMP", "editInternalResourceDialogIcmp": "ICMP",
"editInternalResourceDialogAccessControl": "Tilgangskontroll", "editInternalResourceDialogAccessControl": "Tilgangskontroll",
"editInternalResourceDialogAccessControlDescription": "Kontroller hvilke roller, brukere og maskinklienter som har tilgang til denne ressursen når den er koblet til. Administratorer har alltid tilgang.", "editInternalResourceDialogAccessControlDescription": "Kontroller hvilke roller, brukere og maskinklienter som har tilgang til denne ressursen når den er koblet til. Administratorer har alltid tilgang.",
"editInternalResourceDialogPortRangeValidationError": "Portsjiktet må være \"*\" for alle porter, eller en kommaseparert liste med porter og sjikt (f.eks. \"80,443,8000-9000\"). Porter må være mellom 1 og 65535.", "editInternalResourceDialogPortRangeValidationError": "Portsjiktet må være \"*\" for alle porter, eller en kommaseparert liste med porter og sjikt (f.eks. \"80,443,8000-9000\"). Porter må være mellom 1 og 65535.",
"internalResourceAuthDaemonStrategy": "SSH Auth Daemon Sted",
"internalResourceAuthDaemonStrategyDescription": "Velg hvor SSH-autentisering daemon kjører: på nettstedet (Newt) eller på en ekstern vert.",
"internalResourceAuthDaemonDescription": "SSH-godkjenning daemon håndterer SSH-nøkkel signering og PAM autentisering for denne ressursen. Velg om den kjører på nettstedet (Newt) eller på en separat ekstern vert. Se <docsLink>dokumentasjonen</docsLink> for mer.",
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
"internalResourceAuthDaemonStrategyPlaceholder": "Velg strategi",
"internalResourceAuthDaemonStrategyLabel": "Sted",
"internalResourceAuthDaemonSite": "På nettsted",
"internalResourceAuthDaemonSiteDescription": "Autentiser daemon kjører på nettstedet (Newt).",
"internalResourceAuthDaemonRemote": "Ekstern vert",
"internalResourceAuthDaemonRemoteDescription": "Autentiser daemon kjører på en vert som ikke er nettstedet.",
"internalResourceAuthDaemonPort": "Daemon Port (valgfritt)",
"orgAuthWhatsThis": "Hvor kan jeg finne min organisasjons-ID?", "orgAuthWhatsThis": "Hvor kan jeg finne min organisasjons-ID?",
"learnMore": "Lær mer", "learnMore": "Lær mer",
"backToHome": "Gå tilbake til start", "backToHome": "Gå tilbake til start",
@@ -2932,9 +2502,6 @@
"maintenancePageMessagePlaceholder": "Vi kommer snart tilbake! Vårt nettsted gjennomgår for øyeblikket planlagt vedlikehold.", "maintenancePageMessagePlaceholder": "Vi kommer snart tilbake! Vårt nettsted gjennomgår for øyeblikket planlagt vedlikehold.",
"maintenancePageMessageDescription": "Detaljert beskjed som forklarer vedlikeholdet", "maintenancePageMessageDescription": "Detaljert beskjed som forklarer vedlikeholdet",
"maintenancePageTimeTitle": "Estimert ferdigstillelsestid (Valgfritt)", "maintenancePageTimeTitle": "Estimert ferdigstillelsestid (Valgfritt)",
"privateMaintenanceScreenTitle": "Privat plassholder skjerm",
"privateMaintenanceScreenMessage": "Dette domenet brukes på en privatressurs. Koble til ved å bruke Pangolin-klienten for å få tilgang til denne ressursen.",
"privateMaintenanceScreenSteps": "Når du er koblet til, hvis du fortsatt ser denne meldingen, peker kanskje DNS-cachen til nettleseren din fortsatt til den gamle adressen. For å rette på dette: lukk og åpne denne fanen eller nettleseren på nytt, og naviger deretter tilbake til denne siden.",
"maintenanceTime": "f.eks. 2 timer, 1. november kl. 17:00", "maintenanceTime": "f.eks. 2 timer, 1. november kl. 17:00",
"maintenanceEstimatedTimeDescription": "Når du forventer at vedlikeholdet er ferdigstilt", "maintenanceEstimatedTimeDescription": "Når du forventer at vedlikeholdet er ferdigstilt",
"editDomain": "Rediger domene", "editDomain": "Rediger domene",
@@ -3043,166 +2610,5 @@
"approvalsEmptyStateStep2Title": "Aktiver enhetsgodkjenninger", "approvalsEmptyStateStep2Title": "Aktiver enhetsgodkjenninger",
"approvalsEmptyStateStep2Description": "Rediger en rolle og aktiver alternativet 'Kreve enhetsgodkjenninger'. Brukere med denne rollen vil trenge administratorgodkjenning for nye enheter.", "approvalsEmptyStateStep2Description": "Rediger en rolle og aktiver alternativet 'Kreve enhetsgodkjenninger'. Brukere med denne rollen vil trenge administratorgodkjenning for nye enheter.",
"approvalsEmptyStatePreviewDescription": "Forhåndsvisning: Når aktivert, ventende enhets forespørsler vil vises her for vurdering", "approvalsEmptyStatePreviewDescription": "Forhåndsvisning: Når aktivert, ventende enhets forespørsler vil vises her for vurdering",
"approvalsEmptyStateButtonText": "Administrer Roller", "approvalsEmptyStateButtonText": "Administrer Roller"
"domainErrorTitle": "Vi har problemer med å verifisere domenet ditt",
"idpAdminAutoProvisionPoliciesTabHint": "Konfigurer rollegartlegging og organisasjonspolicyer på <policiesTabLink>Auto leveringsinnstillinger</policiesTabLink> fanen.",
"streamingTitle": "Hendelse Strømming",
"streamingDescription": "Stream hendelser fra din organisasjon til eksterne destinasjoner i sanntid.",
"streamingUnnamedDestination": "Plassering uten navn",
"streamingNoUrlConfigured": "Ingen URL konfigurert",
"streamingAddDestination": "Legg til mål",
"streamingHttpWebhookTitle": "HTTP Webhook",
"streamingHttpWebhookDescription": "Send hendelser til alle HTTP-endepunkter med fleksibel autentisering og maling.",
"streamingS3Title": "Amazon S3",
"streamingS3Description": "Strøm hendelser til en S3-kompatibel objektlagringskjøt. Kommer snart.",
"streamingDatadogTitle": "Datadog",
"streamingDatadogDescription": "Videresend arrangementer direkte til din Datadog-konto. Kommer snart.",
"streamingTypePickerDescription": "Velg en måltype for å komme i gang.",
"streamingFailedToLoad": "Kan ikke laste inn destinasjoner",
"streamingUnexpectedError": "En uventet feil oppstod.",
"streamingFailedToUpdate": "Kunne ikke oppdatere destinasjon",
"streamingDeletedSuccess": "Målet ble slettet",
"streamingFailedToDelete": "Kunne ikke slette destinasjon",
"streamingDeleteTitle": "Slett mål",
"streamingDeleteButtonText": "Slett mål",
"streamingDeleteDialogAreYouSure": "Er du sikker på at du vil slette",
"streamingDeleteDialogThisDestination": "denne destinasjonen",
"streamingDeleteDialogPermanentlyRemoved": "? Alle konfigurasjoner vil bli slettet permanent.",
"httpDestEditTitle": "Rediger mål",
"httpDestAddTitle": "Legg til HTTP-destinasjon",
"httpDestEditDescription": "Oppdater konfigurasjonen for denne HTTP-hendelsesstrømmedestinasjonen.",
"httpDestAddDescription": "Konfigurer et nytt HTTP endepunkt for å motta organisasjonens hendelser.",
"S3DestEditTitle": "Rediger destinasjon",
"S3DestAddTitle": "Legg til S3 destinasjon",
"S3DestEditDescription": "Oppdatere konfigurasjonen for denne S3-hendelsesstrømmingsdestinasjonen.",
"S3DestAddDescription": "Konfigurer et nytt S3-endepunkt for å motta organisasjonens hendelser.",
"datadogDestEditTitle": "Rediger destinasjon",
"datadogDestAddTitle": "Legg til Datadog destinasjon",
"datadogDestEditDescription": "Oppdatere konfigurasjonen for denne Datadog-hendelsesstrømmingsdestinasjonen.",
"datadogDestAddDescription": "Konfigurer et nytt Datadog-endepunkt for å motta organisasjonens hendelser.",
"httpDestTabSettings": "Innstillinger",
"httpDestTabHeaders": "Overskrifter",
"httpDestTabBody": "Innhold",
"httpDestTabLogs": "Logger",
"httpDestNamePlaceholder": "Min HTTP destinasjon",
"httpDestUrlLabel": "Destinasjons URL",
"httpDestUrlErrorHttpRequired": "URL-adressen må bruke httpp eller https",
"httpDestUrlErrorHttpsRequired": "HTTPS er nødvendig for distribusjon av sky",
"httpDestUrlErrorInvalid": "Skriv inn en gyldig nettadresse (f.eks. https://eksempel.com/webhook)",
"httpDestAuthTitle": "Autentisering",
"httpDestAuthDescription": "Velg hvordan ønsker til sluttpunktet ditt er autentisert.",
"httpDestAuthNoneTitle": "Ingen godkjenning",
"httpDestAuthNoneDescription": "Sender forespørsler uten autorisasjonsoverskrift.",
"httpDestAuthBearerTitle": "Bærer Symbol",
"httpDestAuthBearerDescription": "Legger til en Autorisasjon: Bearer '<token>' header til hver forespørsel.",
"httpDestAuthBearerPlaceholder": "Din API-nøkkel eller token",
"httpDestAuthBasicTitle": "Standard Auth",
"httpDestAuthBasicDescription": "Legger til en Autorisasjon: Basic '<credentials>' header. Gi legitimasjon som brukernavn:passord.",
"httpDestAuthBasicPlaceholder": "brukernavn:passord",
"httpDestAuthCustomTitle": "Egendefinert topptekst",
"httpDestAuthCustomDescription": "Angi et egendefinert HTTP headers navn og verdi for autentisering (f.eks X-API-Key).",
"httpDestAuthCustomHeaderNamePlaceholder": "Topptekst navn (f.eks X-API-Key)",
"httpDestAuthCustomHeaderValuePlaceholder": "Header verdi",
"httpDestCustomHeadersTitle": "Egendefinerte HTTP-overskrifter",
"httpDestCustomHeadersDescription": "Legg til egendefinerte overskrifter til hver utgående forespørsel. Nyttig for statisk tokens eller en egendefinert innholdstype. Som standard blir innholdstype: applikasjon/json sendt.",
"httpDestNoHeadersConfigured": "Ingen egendefinerte overskrifter konfigurert. Klikk \"Legg til topptekst\" for å legge til en.",
"httpDestHeaderNamePlaceholder": "Navn på topptekst",
"httpDestHeaderValuePlaceholder": "Verdi",
"httpDestAddHeader": "Legg til topptekst",
"httpDestBodyTemplateTitle": "Egendefinert hovedmal",
"httpDestBodyTemplateDescription": "Kontroller JSON nyttelaststrukturen sendt til ditt endepunkt. Hvis deaktivert, sendes et standard JSON-objekt for hver hendelse.",
"httpDestEnableBodyTemplate": "Aktiver egendefinert meldingsmal",
"httpDestBodyTemplateLabel": "Kroppsmal (JSON)",
"httpDestBodyTemplateHint": "Bruk designmal variabler for å referere til eventfelt i din betaling.",
"httpDestPayloadFormatTitle": "Mål format",
"httpDestPayloadFormatDescription": "Hvordan blir hendelser serialisert inn i hver forespørselsorgan.",
"httpDestFormatJsonArrayTitle": "JSON liste",
"httpDestFormatJsonArrayDescription": "Én forespørsel per batch, innholdet er en JSON-liste. Kompatibel med de mest generiske webhooks og Datadog.",
"httpDestFormatNdjsonTitle": "NDJSON",
"httpDestFormatNdjsonDescription": "Én forespørsel per sats, innholdet er nytt avgrenset JSON - et objekt per linje, ingen ytterarray. Kreves av Splunk HEC, Elastisk/OpenSearch, og Grafana Loki.",
"httpDestFormatSingleTitle": "En hendelse per forespørsel",
"httpDestFormatSingleDescription": "Sender en separat HTTP POST for hver enkelt hendelse. Bruk bare for endepunkter som ikke kan håndtere batcher.",
"httpDestLogTypesTitle": "Logg typer",
"httpDestLogTypesDescription": "Velg hvilke loggtyper som blir videresendt til dette målet. Bare aktiverte loggtyper vil bli strømmet.",
"httpDestAccessLogsTitle": "Tilgangslogger (Automatic Translation)",
"httpDestAccessLogsDescription": "Adgangsforsøk for ressurser, inkludert godkjente og nektet forespørsler.",
"httpDestActionLogsTitle": "Handlingslogger",
"httpDestActionLogsDescription": "Administrative tiltak som utføres av brukere innenfor organisasjonen.",
"httpDestConnectionLogsTitle": "Loggfiler for tilkobling",
"httpDestConnectionLogsDescription": "Utstyrs- og tunneltilkoblingshendelser, inkludert forbindelser og frakobling.",
"httpDestRequestLogsTitle": "Forespørselslogger (Automatic Translation)",
"httpDestRequestLogsDescription": "HTTP-forespørsel logger for bekreftede ressurser, inkludert metode, bane og responskode.",
"httpDestSaveChanges": "Lagre endringer",
"httpDestCreateDestination": "Opprett mål",
"httpDestUpdatedSuccess": "Målet er oppdatert",
"httpDestCreatedSuccess": "Målet er opprettet",
"httpDestUpdateFailed": "Kunne ikke oppdatere destinasjon",
"httpDestCreateFailed": "Kan ikke opprette mål",
"followRedirects": "Følg videresendinger",
"followRedirectsDescription": "Følg automatisk HTTP-videresendinger for forespørsler.",
"alertingErrorWebhookUrl": "Vennligst skriv inn en gyldig URL for webhooken.",
"healthCheckStrategyHttp": "Validerer tilkobling og sjekker HTTP-responsstatus.",
"healthCheckStrategyTcp": "Bekrefter kun TCP-tilkobling, uten å inspisere responsen.",
"healthCheckStrategySnmp": "Utfører en SNMP get-forespørsel for å sjekke helsen til nettverksenheter og infrastruktur.",
"healthCheckStrategyIcmp": "Bruker ICMP ekko forespørsler (ping) for å sjekke om en ressurs er tilgjengelig og responsiv.",
"healthCheckTabStrategy": "Strategi",
"healthCheckTabConnection": "Tilkobling",
"healthCheckTabAdvanced": "Avansert",
"healthCheckStrategyNotAvailable": "Denne strategien er ikke tilgjengelig. Vennligst kontakt salgsavdelingen for å aktivere denne funksjonen.",
"uptime30d": "Oppetid (30d)",
"idpAddActionCreateNew": "Opprett ny identitetsleverandør",
"idpAddActionImportFromOrg": "Importer fra en annen organisasjon",
"idpImportDialogTitle": "Importer identitetsleverandør",
"idpImportDialogDescription": "Velg en identitetsleverandør fra en organisasjon der du er admin. Den vil bli knyttet til denne organisasjonen.",
"idpImportSearchPlaceholder": "Søk etter organisasjons- eller leverandørnavn...",
"idpImportEmpty": "Ingen identitetsleverandører funnet.",
"idpImportedDescription": "Identitetsleverandøren ble importert vellykket.",
"idpDeleteGlobalQuestion": "Er du sikker på at du vil slette denne identitetsleverandøren permanent?",
"idpDeleteGlobalDescription": "Dette vil slette identitetsleverandøren permanent fra alle organisasjoner den er tilknyttet.",
"idpUnassociateTitle": "Frakoble identitetsleverandør",
"idpUnassociateQuestion": "Er du sikker på at du vil frakoble denne identitetsleverandøren fra denne organisasjonen?",
"idpUnassociateDescription": "Alle brukere knyttet til denne identitetsleverandøren vil bli fjernet fra denne organisasjonen, men identitetsleverandøren vil fortsatt eksistere for andre tilknyttede organisasjoner.",
"idpUnassociateConfirm": "Bekreft frakobling av identitetsleverandør",
"idpUnassociateWarning": "Dette kan ikke angres for denne organisasjonen.",
"idpUnassociatedDescription": "Identitetsleverandør er vellykket frakoblet fra denne organisasjonen",
"idpUnassociateMenu": "Frakoble",
"idpDeleteAllOrgsMenu": "Slett",
"publicIpEndpoint": "Endepunkt",
"lastTriggeredAt": "Siste utløste",
"reject": "Avvis",
"uptimeDaysAgo": "{count} days ago",
"uptimeToday": "I dag",
"uptimeNoDataAvailable": "Ingen data tilgjengelig",
"uptimeSuffix": "oppetid",
"uptimeDowntimeSuffix": "nedetid",
"uptimeTooltipUptimeLabel": "Oppetid",
"uptimeTooltipDowntimeLabel": "Nedetid",
"uptimeOngoing": "pågående",
"uptimeNoMonitoringData": "Ingen overvåkingsdata",
"uptimeNoData": "Ingen data",
"uptimeMiniBarDown": "Nede",
"uptimeSectionTitle": "Oppetid",
"uptimeSectionDescription": "Tilgjengelighet de siste {days} dagene",
"uptimeAddAlert": "Legg til varsling",
"uptimeViewAlerts": "Vis varsler",
"uptimeCreateEmailAlert": "Opprett e-postvarsel",
"uptimeAlertDescriptionSite": "Få beskjed på e-post når dette nettstedet går offline eller kommer tilbake online.",
"uptimeAlertDescriptionResource": "Få beskjed på e-post når denne ressursen går offline eller kommer tilbake online.",
"uptimeAlertNamePlaceholder": "Varslingsnavn",
"uptimeAdditionalEmails": "Flere e-poster",
"uptimeCreateAlert": "Opprett varsling",
"uptimeAlertNoRecipients": "Ingen mottakere",
"uptimeAlertNoRecipientsDescription": "Vennligst legg til minst én bruker, rolle, eller e-post for å varsle.",
"uptimeAlertCreated": "Varsel opprettet",
"uptimeAlertCreatedDescription": "Du vil bli varslet når dette endrer status.",
"uptimeAlertCreateFailed": "Kunne ikke opprette varsel",
"webhookUrlLabel": "URL",
"webhookHeaderKeyPlaceholder": "Nøkkel",
"webhookHeaderValuePlaceholder": "Verdi",
"alertLabel": "Varsel",
"domainPickerWildcardSubdomainNotAllowed": "Jokertegnsubdomener er ikke tillatt.",
"domainPickerWildcardCertWarning": "Jokertegnressurser kan kreve ekstra konfigurasjon for å fungere skikkelig.",
"domainPickerWildcardCertWarningLink": "Lær mer",
"health": "Helse",
"domainPendingErrorTitle": "Verifiseringsproblem"
} }
+18 -612
View File
@@ -1,8 +1,4 @@
{ {
"contactSalesEnable": "Neem contact op met de verkoopafdeling om deze functie in te schakelen.",
"contactSalesBookDemo": "Boek een demo",
"contactSalesOr": "of",
"contactSalesContactUs": "neem contact met ons op",
"setupCreate": "Maak de organisatie, site en bronnen aan", "setupCreate": "Maak de organisatie, site en bronnen aan",
"headerAuthCompatibilityInfo": "Schakel dit in om een 401 Niet Geautoriseerd antwoord af te dwingen wanneer een authenticatietoken ontbreekt. Dit is vereist voor browsers of specifieke HTTP-bibliotheken die geen referenties verzenden zonder een serveruitdaging.", "headerAuthCompatibilityInfo": "Schakel dit in om een 401 Niet Geautoriseerd antwoord af te dwingen wanneer een authenticatietoken ontbreekt. Dit is vereist voor browsers of specifieke HTTP-bibliotheken die geen referenties verzenden zonder een serveruitdaging.",
"headerAuthCompatibility": "Uitgebreide compatibiliteit", "headerAuthCompatibility": "Uitgebreide compatibiliteit",
@@ -23,14 +19,6 @@
"componentsInvalidKey": "Ongeldige of verlopen licentiesleutels gedetecteerd. Volg de licentievoorwaarden om alle functies te blijven gebruiken.", "componentsInvalidKey": "Ongeldige of verlopen licentiesleutels gedetecteerd. Volg de licentievoorwaarden om alle functies te blijven gebruiken.",
"dismiss": "Uitschakelen", "dismiss": "Uitschakelen",
"subscriptionViolationMessage": "U overschrijdt uw huidige abonnement. Corrigeer het probleem door sites, gebruikers of andere bronnen te verwijderen om binnen uw plan te blijven.", "subscriptionViolationMessage": "U overschrijdt uw huidige abonnement. Corrigeer het probleem door sites, gebruikers of andere bronnen te verwijderen om binnen uw plan te blijven.",
"trialBannerMessage": "Uw proefversie verloopt over {countdown}. Upgrade om toegang te behouden.",
"trialBannerExpired": "Uw proefperiode is verlopen. Upgrade nu om toegang te herstellen.",
"trialActive": "Gratis proefversie actief",
"trialExpired": "Proefversie verlopen",
"trialHasEnded": "Uw proefperiode is geëindigd.",
"trialDaysRemaining": "{count, plural, one {# dag resterend} other {# dagen resterend}}",
"trialDaysLeftShort": "{days}d over in proefversie",
"trialGoToBilling": "Ga naar factureringspagina",
"subscriptionViolationViewBilling": "Facturering bekijken", "subscriptionViolationViewBilling": "Facturering bekijken",
"componentsLicenseViolation": "Licentie overtreding: Deze server gebruikt {usedSites} sites die de gelicentieerde limiet van {maxSites} sites overschrijden. Volg de licentievoorwaarden om door te gaan met het gebruik van alle functies.", "componentsLicenseViolation": "Licentie overtreding: Deze server gebruikt {usedSites} sites die de gelicentieerde limiet van {maxSites} sites overschrijden. Volg de licentievoorwaarden om door te gaan met het gebruik van alle functies.",
"componentsSupporterMessage": "Bedankt voor het ondersteunen van Pangolin als {tier}!", "componentsSupporterMessage": "Bedankt voor het ondersteunen van Pangolin als {tier}!",
@@ -93,8 +81,6 @@
"siteConfirmCopy": "Ik heb de configuratie gekopieerd", "siteConfirmCopy": "Ik heb de configuratie gekopieerd",
"searchSitesProgress": "Sites zoeken...", "searchSitesProgress": "Sites zoeken...",
"siteAdd": "Site toevoegen", "siteAdd": "Site toevoegen",
"sitesTableViewPublicResources": "Openbare bronnen bekijken",
"sitesTableViewPrivateResources": "Privébronnen bekijken",
"siteInstallNewt": "Installeer Newt", "siteInstallNewt": "Installeer Newt",
"siteInstallNewtDescription": "Laat Newt draaien op uw systeem", "siteInstallNewtDescription": "Laat Newt draaien op uw systeem",
"WgConfiguration": "WireGuard Configuratie", "WgConfiguration": "WireGuard Configuratie",
@@ -112,21 +98,6 @@
"siteUpdatedDescription": "De site is bijgewerkt.", "siteUpdatedDescription": "De site is bijgewerkt.",
"siteGeneralDescription": "Algemene instellingen voor deze site configureren", "siteGeneralDescription": "Algemene instellingen voor deze site configureren",
"siteSettingDescription": "Configureer de instellingen van de site", "siteSettingDescription": "Configureer de instellingen van de site",
"siteResourcesTab": "Bronnen",
"siteResourcesNoneOnSite": "Deze site heeft nog geen openbare of privébronnen.",
"siteResourcesSectionPublic": "Openbare bronnen",
"siteResourcesSectionPrivate": "Privébronnen",
"siteResourcesSectionPublicDescription": "Bronnen extern blootgesteld via domeinen of poorten.",
"siteResourcesSectionPrivateDescription": "Bronnen beschikbaar op uw privénetwerk via de site.",
"siteResourcesViewAllPublic": "Bekijk alle bronnen",
"siteResourcesViewAllPrivate": "Bekijk alle bronnen",
"siteResourcesDialogDescription": "Overzicht van openbare en privébronnen die geassocieerd zijn met deze site.",
"siteResourcesShowMore": "Meer weergeven",
"siteResourcesPermissionDenied": "U heeft geen toestemming om deze bronnen te vermelden.",
"siteResourcesEmptyPublic": "Geen openbare bronnen richten zich nog op deze site.",
"siteResourcesEmptyPrivate": "Er zijn nog geen privébronnen gekoppeld aan deze site.",
"siteResourcesHowToAccess": "Hoe te openen",
"siteResourcesTargetsOnSite": "Doelen op deze site",
"siteSetting": "{siteName} instellingen", "siteSetting": "{siteName} instellingen",
"siteNewtTunnel": "Nieuwste site (Aanbevolen)", "siteNewtTunnel": "Nieuwste site (Aanbevolen)",
"siteNewtTunnelDescription": "Makkelijkste manier om een ingangspunt in een netwerk te maken. Geen extra opzet.", "siteNewtTunnelDescription": "Makkelijkste manier om een ingangspunt in een netwerk te maken. Geen extra opzet.",
@@ -177,11 +148,6 @@
"createLink": "Koppeling aanmaken", "createLink": "Koppeling aanmaken",
"resourcesNotFound": "Geen bronnen gevonden", "resourcesNotFound": "Geen bronnen gevonden",
"resourceSearch": "Zoek bronnen", "resourceSearch": "Zoek bronnen",
"machineSearch": "Zoek machines",
"machinesSearch": "Zoek machine-clients...",
"machineNotFound": "Geen machines gevonden",
"userDeviceSearch": "Gebruikersapparaten zoeken",
"userDevicesSearch": "Gebruikersapparaten zoeken...",
"openMenu": "Menu openen", "openMenu": "Menu openen",
"resource": "Bron", "resource": "Bron",
"title": "Aanspreektitel", "title": "Aanspreektitel",
@@ -209,7 +175,6 @@
"resourceHTTPDescription": "Proxyverzoeken via HTTPS met een volledig gekwalificeerde domeinnaam.", "resourceHTTPDescription": "Proxyverzoeken via HTTPS met een volledig gekwalificeerde domeinnaam.",
"resourceRaw": "TCP/UDP bron", "resourceRaw": "TCP/UDP bron",
"resourceRawDescription": "Proxyverzoeken via ruwe TCP/UDP met een poortnummer.", "resourceRawDescription": "Proxyverzoeken via ruwe TCP/UDP met een poortnummer.",
"resourceRawDescriptionCloud": "Proxy verzoeken over rauwe TCP/UDP met behulp van een poortnummer. Vereist sites om verbinding te maken met een remote node.",
"resourceCreate": "Bron maken", "resourceCreate": "Bron maken",
"resourceCreateDescription": "Volg de onderstaande stappen om een nieuwe bron te maken", "resourceCreateDescription": "Volg de onderstaande stappen om een nieuwe bron te maken",
"resourceSeeAll": "Alle bronnen bekijken", "resourceSeeAll": "Alle bronnen bekijken",
@@ -236,7 +201,6 @@
"protocolSelect": "Selecteer een protocol", "protocolSelect": "Selecteer een protocol",
"resourcePortNumber": "Nummer van poort", "resourcePortNumber": "Nummer van poort",
"resourcePortNumberDescription": "Het externe poortnummer naar proxyverzoeken.", "resourcePortNumberDescription": "Het externe poortnummer naar proxyverzoeken.",
"back": "Achterzijde",
"cancel": "Annuleren", "cancel": "Annuleren",
"resourceConfig": "Configuratie tekstbouwstenen", "resourceConfig": "Configuratie tekstbouwstenen",
"resourceConfigDescription": "Kopieer en plak deze configuratie-snippets om de TCP/UDP-bron in te stellen", "resourceConfigDescription": "Kopieer en plak deze configuratie-snippets om de TCP/UDP-bron in te stellen",
@@ -282,25 +246,11 @@
"orgErrorDeleteMessage": "Er is een fout opgetreden tijdens het verwijderen van de organisatie.", "orgErrorDeleteMessage": "Er is een fout opgetreden tijdens het verwijderen van de organisatie.",
"orgDeleted": "Organisatie verwijderd", "orgDeleted": "Organisatie verwijderd",
"orgDeletedMessage": "De organisatie en haar gegevens zijn verwijderd.", "orgDeletedMessage": "De organisatie en haar gegevens zijn verwijderd.",
"deleteAccount": "Verwijder account",
"deleteAccountDescription": "Verwijdert permanent uw account, alle organisaties die u bezit, en alle gegevens binnen deze organisaties. Dit kan niet ongedaan worden gemaakt.",
"deleteAccountButton": "Verwijder account",
"deleteAccountConfirmTitle": "Verwijder account",
"deleteAccountConfirmMessage": "Dit zal uw account permanent wissen, alle organisaties die u bezit, en alle gegevens binnen deze organisaties. Dit kan niet ongedaan worden gemaakt.",
"deleteAccountConfirmString": "verwijder account",
"deleteAccountSuccess": "Account verwijderd",
"deleteAccountSuccessMessage": "Uw account is verwijderd.",
"deleteAccountError": "Kan account niet verwijderen",
"deleteAccountPreviewAccount": "Uw account",
"deleteAccountPreviewOrgs": "Organisaties die je bezit (en al hun gegevens)",
"orgMissing": "Organisatie-ID ontbreekt", "orgMissing": "Organisatie-ID ontbreekt",
"orgMissingMessage": "Niet in staat om de uitnodiging te regenereren zonder organisatie-ID.", "orgMissingMessage": "Niet in staat om de uitnodiging te regenereren zonder organisatie-ID.",
"accessUsersManage": "Gebruikers beheren", "accessUsersManage": "Gebruikers beheren",
"accessUserManage": "Beheer gebruiker",
"accessUsersDescription": "Nodig uit en beheer gebruikers met toegang tot deze organisatie", "accessUsersDescription": "Nodig uit en beheer gebruikers met toegang tot deze organisatie",
"accessUsersSearch": "Gebruikers zoeken...", "accessUsersSearch": "Gebruikers zoeken...",
"accessUsersRoleFilterCount": "{count, plural, one {# rol} other {# rollen}}",
"accessUsersRoleFilterClear": "Rolfilters wissen",
"accessUserCreate": "Gebruiker aanmaken", "accessUserCreate": "Gebruiker aanmaken",
"accessUserRemove": "Gebruiker verwijderen", "accessUserRemove": "Gebruiker verwijderen",
"username": "Gebruikersnaam", "username": "Gebruikersnaam",
@@ -360,54 +310,6 @@
"apiKeysDelete": "API-sleutel verwijderen", "apiKeysDelete": "API-sleutel verwijderen",
"apiKeysManage": "API-sleutels beheren", "apiKeysManage": "API-sleutels beheren",
"apiKeysDescription": "API-sleutels worden gebruikt om te verifiëren met de integratie-API", "apiKeysDescription": "API-sleutels worden gebruikt om te verifiëren met de integratie-API",
"provisioningKeysTitle": "Vertrekkende sleutel",
"provisioningKeysManage": "Beheren van Provisioning Sleutels",
"provisioningKeysDescription": "Provisionerende sleutels worden gebruikt om geautomatiseerde sitebepaling voor uw organisatie te verifiëren.",
"provisioningManage": "Provisie",
"provisioningDescription": "Voorzieningssleutels beheren en sites beoordelen in afwachting van goedkeuring.",
"pendingSites": "Openstaande sites",
"siteApproveSuccess": "Site succesvol goedgekeurd",
"siteApproveError": "Fout bij goedkeuren website",
"provisioningKeys": "Verhelderende sleutels",
"searchProvisioningKeys": "Zoek provisioningsleutels ...",
"provisioningKeysAdd": "Genereer Provisioning Sleutel",
"provisioningKeysErrorDelete": "Fout bij verwijderen provisioning sleutel",
"provisioningKeysErrorDeleteMessage": "Fout bij verwijderen provisioning sleutel",
"provisioningKeysQuestionRemove": "Weet u zeker dat u deze proefsleutel van de organisatie wilt verwijderen?",
"provisioningKeysMessageRemove": "Eenmaal verwijderd, kan de sleutel niet meer worden gebruikt voor site-instructie.",
"provisioningKeysDeleteConfirm": "Bevestig Verwijderen Provisione-sleutel",
"provisioningKeysDelete": "Provisione-sleutel verwijderen",
"provisioningKeysCreate": "Genereer Provisioning Sleutel",
"provisioningKeysCreateDescription": "Een nieuwe provisioningsleutel voor de organisatie genereren",
"provisioningKeysSeeAll": "Bekijk alle provisioning sleutels",
"provisioningKeysSave": "Sla de provisioning sleutel op",
"provisioningKeysSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een veilige plaats.",
"provisioningKeysErrorCreate": "Fout bij aanmaken provisioning sleutel",
"provisioningKeysList": "Nieuwe provisioning sleutel",
"provisioningKeysMaxBatchSize": "Maximale batchgrootte",
"provisioningKeysUnlimitedBatchSize": "Onbeperkte batchgrootte (geen limiet)",
"provisioningKeysMaxBatchUnlimited": "Onbeperkt",
"provisioningKeysMaxBatchSizeInvalid": "Voer een geldige maximale batchgrootte in (11.000,000).",
"provisioningKeysValidUntil": "Geldig tot",
"provisioningKeysValidUntilHint": "Laat leeg voor geen vervaldatum.",
"provisioningKeysValidUntilInvalid": "Voer een geldige datum en tijd in.",
"provisioningKeysNumUsed": "Aantal keer gebruikt",
"provisioningKeysLastUsed": "Laatst gebruikt",
"provisioningKeysNoExpiry": "Geen vervaldatum",
"provisioningKeysNeverUsed": "Nooit",
"provisioningKeysEdit": "Wijzig Provisioning Sleutel",
"provisioningKeysEditDescription": "Werk de maximale batchgrootte en verlooptijd voor deze sleutel bij.",
"provisioningKeysApproveNewSites": "Goedkeuren van nieuwe sites",
"provisioningKeysApproveNewSitesDescription": "Automatisch sites goedkeuren die zich registreren met deze sleutel.",
"provisioningKeysUpdateError": "Fout tijdens bijwerken provisioning sleutel",
"provisioningKeysUpdated": "Provisie sleutel bijgewerkt",
"provisioningKeysUpdatedDescription": "Uw wijzigingen zijn opgeslagen.",
"provisioningKeysBannerTitle": "Bewerkingssleutels voor websites",
"provisioningKeysBannerDescription": "Genereer een inrichtingssleutel en gebruik deze met de Newt-connector om automatisch sites te maken bij de eerste opstart - er is geen behoefte om aparte inloggegevens voor elke site in te stellen.",
"provisioningKeysBannerButtonText": "Meer informatie",
"pendingSitesBannerTitle": "Openstaande sites",
"pendingSitesBannerDescription": "Sites die verbinding maken met een inrichtingssleutel verschijnen hier voor beoordeling.",
"pendingSitesBannerButtonText": "Meer informatie",
"apiKeysSettings": "{apiKeyName} instellingen", "apiKeysSettings": "{apiKeyName} instellingen",
"userTitle": "Alle gebruikers beheren", "userTitle": "Alle gebruikers beheren",
"userDescription": "Bekijk en beheer alle gebruikers in het systeem", "userDescription": "Bekijk en beheer alle gebruikers in het systeem",
@@ -437,10 +339,6 @@
"licenseErrorKeyActivate": "Licentiesleutel activeren mislukt", "licenseErrorKeyActivate": "Licentiesleutel activeren mislukt",
"licenseErrorKeyActivateDescription": "Er is een fout opgetreden tijdens het activeren van de licentiesleutel.", "licenseErrorKeyActivateDescription": "Er is een fout opgetreden tijdens het activeren van de licentiesleutel.",
"licenseAbout": "Over licenties", "licenseAbout": "Over licenties",
"licenseBannerTitle": "Activeer Uw Enterprise Licentie",
"licenseBannerDescription": "Ontgrendel enterprise-functies voor uw zelf-gehoste Pangolin-instantie. Koop een licentiesleutel om premium mogelijkheden te activeren, voeg deze vervolgens hieronder toe.",
"licenseBannerGetLicense": "Koop een Licentie",
"licenseBannerViewDocs": "Bekijk Documentatie",
"communityEdition": "Community editie", "communityEdition": "Community editie",
"licenseAboutDescription": "Dit geldt voor gebruikers van bedrijven en ondernemingen die Pangolin in gebruiken in een commerciële omgeving. Als u Pangolin gebruikt voor persoonlijk gebruik, kunt u dit gedeelte negeren.", "licenseAboutDescription": "Dit geldt voor gebruikers van bedrijven en ondernemingen die Pangolin in gebruiken in een commerciële omgeving. Als u Pangolin gebruikt voor persoonlijk gebruik, kunt u dit gedeelte negeren.",
"licenseKeyActivated": "Licentiesleutel geactiveerd", "licenseKeyActivated": "Licentiesleutel geactiveerd",
@@ -563,8 +461,6 @@
"filterByApprovalState": "Filter op goedkeuringsstatus", "filterByApprovalState": "Filter op goedkeuringsstatus",
"approvalListEmpty": "Geen goedkeuringen", "approvalListEmpty": "Geen goedkeuringen",
"approvalState": "Goedkeuring status", "approvalState": "Goedkeuring status",
"approvalLoadMore": "Meer laden",
"loadingApprovals": "Goedkeuringen laden",
"approve": "Goedkeuren", "approve": "Goedkeuren",
"approved": "Goedgekeurd", "approved": "Goedgekeurd",
"denied": "Geweigerd", "denied": "Geweigerd",
@@ -598,12 +494,9 @@
"userSaved": "Gebruiker opgeslagen", "userSaved": "Gebruiker opgeslagen",
"userSavedDescription": "De gebruiker is bijgewerkt.", "userSavedDescription": "De gebruiker is bijgewerkt.",
"autoProvisioned": "Automatisch bevestigen", "autoProvisioned": "Automatisch bevestigen",
"autoProvisionSettings": "Auto Provisie Instellingen",
"autoProvisionedDescription": "Toestaan dat deze gebruiker automatisch wordt beheerd door een identiteitsprovider", "autoProvisionedDescription": "Toestaan dat deze gebruiker automatisch wordt beheerd door een identiteitsprovider",
"accessControlsDescription": "Beheer wat deze gebruiker toegang heeft tot en doet in de organisatie", "accessControlsDescription": "Beheer wat deze gebruiker toegang heeft tot en doet in de organisatie",
"accessControlsSubmit": "Bewaar Toegangsbesturing", "accessControlsSubmit": "Bewaar Toegangsbesturing",
"singleRolePerUserPlanNotice": "Uw plan ondersteunt slechts één rol per gebruiker.",
"singleRolePerUserEditionNotice": "Deze editie ondersteunt slechts één rol per gebruiker.",
"roles": "Rollen", "roles": "Rollen",
"accessUsersRoles": "Beheer Gebruikers & Rollen", "accessUsersRoles": "Beheer Gebruikers & Rollen",
"accessUsersRolesDescription": "Nodig gebruikers uit en voeg ze toe aan de rollen om toegang tot de organisatie te beheren", "accessUsersRolesDescription": "Nodig gebruikers uit en voeg ze toe aan de rollen om toegang tot de organisatie te beheren",
@@ -660,8 +553,6 @@
"targetErrorInvalidPortDescription": "Voer een geldig poortnummer in", "targetErrorInvalidPortDescription": "Voer een geldig poortnummer in",
"targetErrorNoSite": "Geen site geselecteerd", "targetErrorNoSite": "Geen site geselecteerd",
"targetErrorNoSiteDescription": "Selecteer een site voor het doel", "targetErrorNoSiteDescription": "Selecteer een site voor het doel",
"targetTargetsCleared": "Doelen gewist",
"targetTargetsClearedDescription": "Alle doelen zijn verwijderd van deze bron",
"targetCreated": "Doel aangemaakt", "targetCreated": "Doel aangemaakt",
"targetCreatedDescription": "Doel is succesvol aangemaakt", "targetCreatedDescription": "Doel is succesvol aangemaakt",
"targetErrorCreate": "Kan doel niet aanmaken", "targetErrorCreate": "Kan doel niet aanmaken",
@@ -745,7 +636,6 @@
"resourcesErrorUpdate": "Bron wisselen mislukt", "resourcesErrorUpdate": "Bron wisselen mislukt",
"resourcesErrorUpdateDescription": "Er is een fout opgetreden tijdens het bijwerken van het document", "resourcesErrorUpdateDescription": "Er is een fout opgetreden tijdens het bijwerken van het document",
"access": "Toegangsrechten", "access": "Toegangsrechten",
"accessControl": "Toegangs controle",
"shareLink": "{resource} Share link", "shareLink": "{resource} Share link",
"resourceSelect": "Selecteer resource", "resourceSelect": "Selecteer resource",
"shareLinks": "Links delen", "shareLinks": "Links delen",
@@ -763,7 +653,6 @@
"newtEndpoint": "Endpoint", "newtEndpoint": "Endpoint",
"newtId": "ID", "newtId": "ID",
"newtSecretKey": "Geheim", "newtSecretKey": "Geheim",
"newtVersion": "Versie",
"architecture": "Architectuur", "architecture": "Architectuur",
"sites": "Sites", "sites": "Sites",
"siteWgAnyClients": "Gebruik een willekeurige WireGuard client om verbinding te maken. Je zult interne bronnen moeten aanspreken met behulp van de peer IP.", "siteWgAnyClients": "Gebruik een willekeurige WireGuard client om verbinding te maken. Je zult interne bronnen moeten aanspreken met behulp van de peer IP.",
@@ -887,7 +776,6 @@
"accessRoleRemoved": "Rol verwijderd", "accessRoleRemoved": "Rol verwijderd",
"accessRoleRemovedDescription": "De rol is succesvol verwijderd.", "accessRoleRemovedDescription": "De rol is succesvol verwijderd.",
"accessRoleRequiredRemove": "Voordat u deze rol verwijdert, selecteer een nieuwe rol om bestaande leden aan te dragen.", "accessRoleRequiredRemove": "Voordat u deze rol verwijdert, selecteer een nieuwe rol om bestaande leden aan te dragen.",
"network": "Netwerk",
"manage": "Beheren", "manage": "Beheren",
"sitesNotFound": "Geen sites gevonden.", "sitesNotFound": "Geen sites gevonden.",
"pangolinServerAdmin": "Serverbeheer - Pangolin", "pangolinServerAdmin": "Serverbeheer - Pangolin",
@@ -903,9 +791,9 @@
"sitestCountIncrease": "Toename van site vergroten", "sitestCountIncrease": "Toename van site vergroten",
"idpManage": "Identiteitsaanbieders beheren", "idpManage": "Identiteitsaanbieders beheren",
"idpManageDescription": "Identiteitsaanbieders in het systeem bekijken en beheren", "idpManageDescription": "Identiteitsaanbieders in het systeem bekijken en beheren",
"idpGlobalModeBanner": "Identiteitsaanbieders (IdPs) per organisatie zijn uitgeschakeld op deze server. Het gebruikt globale IdPs (gedeeld tussen alle organisaties). Beheer globale IdPs in het <adminPanelLink>beheerderspaneel</adminPanelLink>. Om IdPs per organisatie in te schakelen, bewerk de server configuratie en zet IdP modus op org. <configDocsLink>Zie de documenten</configDocsLink>. Als je globale IdPs wilt blijven gebruiken en dit uit de organisatie-instellingen wilt laten verdwijnen, zet dan expliciet de modus naar globaal in de config.", "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 organisatie zijn uitgeschakeld op deze server. Het gebruikt globale IdPs (gedeeld in alle organisaties) Beheer globale IdPs in het <adminPanelLink>beheerderspaneel</adminPanelLink>. Om identiteitsproviders per organisatie te gebruiken, moet u upgraden naar de Enterprise editie.", "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 organisatie zijn uitgeschakeld op deze server. Het gebruikt globale IdPs (gedeeld in alle organisaties) Beheer globale IdPs in het <adminPanelLink>beheerderspaneel</adminPanelLink>. Om identiteitsaanbieders per organisatie te gebruiken, is een Enterprise-licentie vereist.", "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 succesvol verwijderd", "idpDeletedDescription": "Identity provider succesvol verwijderd",
"idpOidc": "OAuth2/OIDC", "idpOidc": "OAuth2/OIDC",
"idpQuestionRemove": "Weet u zeker dat u de identiteitsprovider permanent wilt verwijderen?", "idpQuestionRemove": "Weet u zeker dat u de identiteitsprovider permanent wilt verwijderen?",
@@ -931,7 +819,6 @@
"idpDisplayName": "Een weergavenaam voor deze identiteitsprovider", "idpDisplayName": "Een weergavenaam voor deze identiteitsprovider",
"idpAutoProvisionUsers": "Auto Provisie Gebruikers", "idpAutoProvisionUsers": "Auto Provisie Gebruikers",
"idpAutoProvisionUsersDescription": "Wanneer ingeschakeld, worden gebruikers automatisch in het systeem aangemaakt wanneer ze de eerste keer inloggen met de mogelijkheid om gebruikers toe te wijzen aan rollen en organisaties.", "idpAutoProvisionUsersDescription": "Wanneer ingeschakeld, worden gebruikers automatisch in het systeem aangemaakt wanneer ze de eerste keer inloggen met de mogelijkheid om gebruikers toe te wijzen aan rollen en organisaties.",
"idpAutoProvisionConfigureAfterCreate": "U kunt automatische voorzieningsinstellingen configureren zodra de identiteitsprovider is aangemaakt.",
"licenseBadge": "EE", "licenseBadge": "EE",
"idpType": "Type provider", "idpType": "Type provider",
"idpTypeDescription": "Selecteer het type identiteitsprovider dat u wilt configureren", "idpTypeDescription": "Selecteer het type identiteitsprovider dat u wilt configureren",
@@ -983,7 +870,7 @@
"defaultMappingsRole": "Standaard Rol Toewijzing", "defaultMappingsRole": "Standaard Rol Toewijzing",
"defaultMappingsRoleDescription": "Het resultaat van deze uitdrukking moet de rolnaam zoals gedefinieerd in de organisatie als tekenreeks teruggeven.", "defaultMappingsRoleDescription": "Het resultaat van deze uitdrukking moet de rolnaam zoals gedefinieerd in de organisatie als tekenreeks teruggeven.",
"defaultMappingsOrg": "Standaard organisatie mapping", "defaultMappingsOrg": "Standaard organisatie mapping",
"defaultMappingsOrgDescription": "Wanneer ingesteld, moet deze expressie de organisatie-ID of waar retourneren voor de gebruiker om toegang te krijgen tot die organisatie. Als het niet is ingesteld, is het definiëren van een roltoewijzing voldoende: de gebruiker is toegestaan zolang een geldige roltoewijzing voor hen binnen de organisatie kan worden opgelost.", "defaultMappingsOrgDescription": "Deze expressie moet de org-ID teruggeven of waar om de gebruiker toegang te geven tot de organisatie.",
"defaultMappingsSubmit": "Standaard toewijzingen opslaan", "defaultMappingsSubmit": "Standaard toewijzingen opslaan",
"orgPoliciesEdit": "Organisatie beleid bewerken", "orgPoliciesEdit": "Organisatie beleid bewerken",
"org": "Organisatie", "org": "Organisatie",
@@ -1130,12 +1017,12 @@
"pangolinSetup": "Instellen - Pangolin", "pangolinSetup": "Instellen - Pangolin",
"orgNameRequired": "Organisatienaam is vereist", "orgNameRequired": "Organisatienaam is vereist",
"orgIdRequired": "Organisatie-ID is vereist", "orgIdRequired": "Organisatie-ID is vereist",
"orgIdMaxLength": "Organisatie-ID mag maximaal 32 tekens lang zijn",
"orgErrorCreate": "Fout opgetreden tijdens het aanmaken org", "orgErrorCreate": "Fout opgetreden tijdens het aanmaken org",
"pageNotFound": "Pagina niet gevonden", "pageNotFound": "Pagina niet gevonden",
"pageNotFoundDescription": "Oeps! De pagina die je zoekt bestaat niet.", "pageNotFoundDescription": "Oeps! De pagina die je zoekt bestaat niet.",
"overview": "Overzicht.", "overview": "Overzicht.",
"home": "Startpagina", "home": "Startpagina",
"accessControl": "Toegangs controle",
"settings": "Instellingen", "settings": "Instellingen",
"usersAll": "Alle gebruikers", "usersAll": "Alle gebruikers",
"license": "Licentie", "license": "Licentie",
@@ -1198,12 +1085,6 @@
"actionGetUser": "Gebruiker ophalen", "actionGetUser": "Gebruiker ophalen",
"actionGetOrgUser": "Krijg organisatie-gebruiker", "actionGetOrgUser": "Krijg organisatie-gebruiker",
"actionListOrgDomains": "Lijst organisatie domeinen", "actionListOrgDomains": "Lijst organisatie domeinen",
"actionGetDomain": "Domein verkrijgen",
"actionCreateOrgDomain": "Domein aanmaken",
"actionUpdateOrgDomain": "Domein bijwerken",
"actionDeleteOrgDomain": "Domein verwijderen",
"actionGetDNSRecords": "Krijg DNS Records",
"actionRestartOrgDomain": "Domein opnieuw starten",
"actionCreateSite": "Site aanmaken", "actionCreateSite": "Site aanmaken",
"actionDeleteSite": "Site verwijderen", "actionDeleteSite": "Site verwijderen",
"actionGetSite": "Site ophalen", "actionGetSite": "Site ophalen",
@@ -1215,7 +1096,6 @@
"setupTokenDescription": "Voer het setup-token in vanaf de serverconsole.", "setupTokenDescription": "Voer het setup-token in vanaf de serverconsole.",
"setupTokenRequired": "Setup-token is vereist", "setupTokenRequired": "Setup-token is vereist",
"actionUpdateSite": "Site bijwerken", "actionUpdateSite": "Site bijwerken",
"actionResetSiteBandwidth": "Reset organisatieschandbreedte",
"actionListSiteRoles": "Toon toegestane sitenollen", "actionListSiteRoles": "Toon toegestane sitenollen",
"actionCreateResource": "Bron maken", "actionCreateResource": "Bron maken",
"actionDeleteResource": "Document verwijderen", "actionDeleteResource": "Document verwijderen",
@@ -1245,7 +1125,6 @@
"actionRemoveUser": "Gebruiker verwijderen", "actionRemoveUser": "Gebruiker verwijderen",
"actionListUsers": "Gebruikers weergeven", "actionListUsers": "Gebruikers weergeven",
"actionAddUserRole": "Gebruikersrol toevoegen", "actionAddUserRole": "Gebruikersrol toevoegen",
"actionSetUserOrgRoles": "Stel gebruikersrollen in",
"actionGenerateAccessToken": "Genereer Toegangstoken", "actionGenerateAccessToken": "Genereer Toegangstoken",
"actionDeleteAccessToken": "Verwijder toegangstoken", "actionDeleteAccessToken": "Verwijder toegangstoken",
"actionListAccessTokens": "Lijst toegangstokens", "actionListAccessTokens": "Lijst toegangstokens",
@@ -1290,9 +1169,7 @@
"actionViewLogs": "Logboeken bekijken", "actionViewLogs": "Logboeken bekijken",
"noneSelected": "Niet geselecteerd", "noneSelected": "Niet geselecteerd",
"orgNotFound2": "Geen organisaties gevonden.", "orgNotFound2": "Geen organisaties gevonden.",
"search": "Zoeken", "searchProgress": "Zoeken...",
"searchPlaceholder": "Zoeken...",
"emptySearchOptions": "Geen opties gevonden",
"create": "Aanmaken", "create": "Aanmaken",
"orgs": "Organisaties", "orgs": "Organisaties",
"loginError": "Er is een onverwachte fout opgetreden. Probeer het opnieuw.", "loginError": "Er is een onverwachte fout opgetreden. Probeer het opnieuw.",
@@ -1356,14 +1233,12 @@
"sidebarClientResources": "Privé", "sidebarClientResources": "Privé",
"sidebarAccessControl": "Toegangs controle", "sidebarAccessControl": "Toegangs controle",
"sidebarLogsAndAnalytics": "Logs & Analytics", "sidebarLogsAndAnalytics": "Logs & Analytics",
"sidebarTeam": "Team",
"sidebarUsers": "Gebruikers", "sidebarUsers": "Gebruikers",
"sidebarAdmin": "Beheerder", "sidebarAdmin": "Beheerder",
"sidebarInvitations": "Uitnodigingen", "sidebarInvitations": "Uitnodigingen",
"sidebarRoles": "Rollen", "sidebarRoles": "Rollen",
"sidebarShareableLinks": "Koppelingen", "sidebarShareableLinks": "Koppelingen",
"sidebarApiKeys": "API sleutels", "sidebarApiKeys": "API sleutels",
"sidebarProvisioning": "Provisie",
"sidebarSettings": "Instellingen", "sidebarSettings": "Instellingen",
"sidebarAllUsers": "Alle gebruikers", "sidebarAllUsers": "Alle gebruikers",
"sidebarIdentityProviders": "Identiteit aanbieders", "sidebarIdentityProviders": "Identiteit aanbieders",
@@ -1375,167 +1250,8 @@
"sidebarGeneral": "Beheren", "sidebarGeneral": "Beheren",
"sidebarLogAndAnalytics": "Log & Analytics", "sidebarLogAndAnalytics": "Log & Analytics",
"sidebarBluePrints": "Blauwdrukken", "sidebarBluePrints": "Blauwdrukken",
"sidebarAlerting": "Waarschuwingen",
"sidebarHealthChecks": "Gezondheidscontroles",
"sidebarOrganization": "Organisatie", "sidebarOrganization": "Organisatie",
"sidebarManagement": "Beheer",
"sidebarBillingAndLicenses": "Facturatie & Licenties",
"sidebarLogsAnalytics": "Analyses", "sidebarLogsAnalytics": "Analyses",
"alertingTitle": "Waarschuwingen",
"alertingDescription": "Definieer bronnen, triggers en acties voor meldingen",
"alertingRules": "Waarschuwingsregels",
"alertingSearchRules": "Zoek regels…",
"alertingAddRule": "Regel aanmaken",
"alertingColumnSource": "Bron",
"alertingColumnTrigger": "Trigger",
"alertingColumnActions": "Acties",
"alertingColumnEnabled": "Ingeschakeld",
"alertingDeleteQuestion": "Bevestig alstublieft dat u deze waarschuwingsregel wilt verwijderen.",
"alertingDeleteRule": "Verwijder waarschuwingsregel",
"alertingRuleDeleted": "Waarschuwingsregel verwijderd",
"alertingRuleSaved": "Waarschuwingsregel opgeslagen",
"alertingRuleSavedCreatedDescription": "Uw nieuwe waarschuwingsregel is aangemaakt. U kunt deze op deze pagina blijven bewerken.",
"alertingRuleSavedUpdatedDescription": "Uw wijzigingen in deze waarschuwingsregel zijn opgeslagen.",
"alertingEditRule": "Bewerk waarschuwingsregel",
"alertingCreateRule": "Waarschuwingsregel aanmaken",
"alertingRuleCredenzaDescription": "Kies wat te bekijken, wanneer het moet gebeuren en hoe te waarschuwen",
"alertingRuleNamePlaceholder": "Productiesite offline",
"alertingRuleEnabled": "Regel ingeschakeld",
"alertingSectionSource": "Bron",
"alertingSourceType": "Brontype",
"alertingSourceSite": "Site",
"alertingSourceHealthCheck": "Gezondheidscontrole",
"alertingPickSites": "Sites",
"alertingPickHealthChecks": "Gezondheidscontroles",
"alertingPickResources": "Bronnen",
"alertingAllSites": "Alle sites",
"alertingAllSitesDescription": "Waarschuwing voor elke site",
"alertingSpecificSites": "Specifieke sites",
"alertingSpecificSitesDescription": "Kies specifieke sites om in de gaten te houden",
"alertingAllHealthChecks": "Alle Gezondheidscontroles",
"alertingAllHealthChecksDescription": "Waarschuwing voor elke gezondheidscontrole",
"alertingSpecificHealthChecks": "Specifieke Gezondheidscontroles",
"alertingSpecificHealthChecksDescription": "Kies specifieke gezondheidscontroles om in de gaten te houden",
"alertingAllResources": "Alle bronnen",
"alertingAllResourcesDescription": "Waarschuwing voor elke bron",
"alertingSpecificResources": "Specifieke bronnen",
"alertingSpecificResourcesDescription": "Kies specifieke bronnen om in de gaten te houden",
"alertingSelectResources": "Selecteer bronnen…",
"alertingResourcesSelected": "{count} bronnen geselecteerd",
"alertingResourcesEmpty": "Geen bronnen met doelen in de eerste 10 resultaten.",
"alertingSectionTrigger": "Trigger",
"alertingTrigger": "Wanneer te waarschuwen",
"alertingTriggerSiteOnline": "Site online",
"alertingTriggerSiteOffline": "Site offline",
"alertingTriggerSiteToggle": "Site status wijzigt",
"alertingTriggerHcHealthy": "Gezondheidscontrole gezond",
"alertingTriggerHcUnhealthy": "Gezondheidscontrole ongezond",
"alertingTriggerHcToggle": "Gezondheidscontrole status verandert",
"alertingTriggerResourceHealthy": "Bron gezond",
"alertingTriggerResourceUnhealthy": "Bron ongezond",
"alertingTriggerResourceDegraded": "Bron gedegradeerd",
"alertingSearchHealthChecks": "Zoek gezondheidscontroles…",
"alertingHealthChecksEmpty": "Geen gezondheidscontroles beschikbaar.",
"alertingTriggerResourceToggle": "Bronstatus wijzigt",
"alertingSourceResource": "Bron",
"alertingSectionActions": "Acties",
"alertingAddAction": "Actie toevoegen",
"alertingActionNotify": "E-mail",
"alertingActionNotifyDescription": "Stuur e-mailmeldingen naar gebruikers of rollen",
"alertingActionWebhook": "Webhook",
"alertingActionWebhookDescription": "Stuur een HTTP-verzoek naar een aangepast eindpunt",
"alertingExternalIntegration": "Externe integratie",
"alertingExternalPagerDutyDescription": "Stuur waarschuwingen naar PagerDuty voor incidentbeheer",
"alertingExternalOpsgenieDescription": "Routeer waarschuwingen naar Opsgenie voor wachtdienstbeheer",
"alertingExternalServiceNowDescription": "Maak ServiceNow-incidenten aan vanuit waarschuwingsgebeurtenissen",
"alertingExternalIncidentIoDescription": "Trigger Incident.io workflows van waarschuwingsgebeurtenissen",
"alertingActionType": "Actietype",
"alertingNotifyUsers": "Gebruikers",
"alertingNotifyRoles": "Rollen",
"alertingNotifyEmails": "E-mailadressen",
"alertingEmailPlaceholder": "Voeg e-mail toe en druk op Enter",
"alertingWebhookMethod": "HTTP-methode",
"alertingWebhookSecret": "Ondertekengeheim (optioneel)",
"alertingWebhookSecretPlaceholder": "HMAC-geheim",
"alertingWebhookHeaders": "Headers",
"alertingAddHeader": "Header toevoegen",
"alertingSelectSites": "Selecteer sites…",
"alertingSitesSelected": "{count} sites geselecteerd",
"alertingSelectHealthChecks": "Selecteer gezondheidscontroles…",
"alertingHealthChecksSelected": "{count} gezondheidscontroles geselecteerd",
"alertingNoHealthChecks": "Geen doelen met ingeschakelde gezondheidscontroles",
"alertingHealthCheckStub": "Gezondheidscontrole brondeselectie is nog niet gekoppeld - u kunt nog steeds triggers en acties configureren.",
"alertingSelectUsers": "Selecteer gebruikers…",
"alertingUsersSelected": "{count} gebruikers geselecteerd",
"alertingSelectRoles": "Selecteer rollen…",
"alertingRolesSelected": "{count} rollen geselecteerd",
"alertingSummarySites": "Sites ({count})",
"alertingSummaryAllSites": "Alle sites",
"alertingSummaryHealthChecks": "Gezondheidscontroles ({count})",
"alertingSummaryAllHealthChecks": "Alle gezondheidscontroles",
"alertingSummaryResources": "Bronnen ({count})",
"alertingSummaryAllResources": "Alle bronnen",
"alertingErrorNameRequired": "Voer een naam in",
"alertingErrorActionsMin": "Voeg minimaal één actie toe",
"alertingErrorPickSites": "Selecteer minimaal één site",
"alertingErrorPickHealthChecks": "Selecteer minimaal één gezondheidscontrole",
"alertingErrorPickResources": "Selecteer minimaal één bron",
"alertingErrorTriggerSite": "Kies een site-trigger",
"alertingErrorTriggerHealth": "Kies een gezondheidscontrole-trigger",
"alertingErrorTriggerResource": "Kies een bron-trigger",
"alertingErrorNotifyRecipients": "Kies gebruikers, rollen of ten minste één e-mail",
"alertingConfigureSource": "Bron configureren",
"alertingConfigureTrigger": "Trigger configureren",
"alertingConfigureActions": "Acties configureren",
"alertingBackToRules": "Terug naar regels",
"alertingRuleCooldown": "Aflkoelperiode (seconden)",
"alertingRuleCooldownDescription": "Minimale tijd tussen herhaalwaarschuwingen voor dezelfde regel. Zet op 0 om elke keer te laten vuren.",
"alertingDraftBadge": "Concept - opslaan om deze regel op te slaan",
"alertingSidebarHint": "Klik op een stap in het canvas om deze hier te bewerken.",
"alertingGraphCanvasTitle": "Regelstroom",
"alertingGraphCanvasDescription": "Visueel overzicht van bron, trigger en acties. Selecteer een node om deze in het paneel te bewerken.",
"alertingNodeNotConfigured": "Nog niet geconfigureerd",
"alertingNodeActionsCount": "{count, plural, one {# actie} other {# acties}}",
"alertingNodeRoleSource": "Bron",
"alertingNodeRoleTrigger": "Trigger",
"alertingNodeRoleAction": "Actie",
"alertingTabRules": "Waarschuwingsregels",
"alertingTabHealthChecks": "Gezondheidscontroles",
"alertingRulesBannerTitle": "Meldingen ontvangen",
"alertingRulesBannerDescription": "Elke regel koppelt wat te bekijken (een site, gezondheidscontrole of bron), wanneer te vuren (bijvoorbeeld offline of ongezond), en hoe uw team te waarschuwen via e-mail, webhooks of integraties. Gebruik deze lijst om die regels te maken, in te schakelen en te beheren.",
"alertingHealthChecksBannerTitle": "Gezondheid & bronnen bewaken",
"alertingHealthChecksBannerDescription": "Gezondheidscontroles zijn HTTP- of TCP-monitoren die u één keer definieert. U kunt ze vervolgens als bronnen in waarschuwingsregels gebruiken, zodat u meldingen krijgt wanneer een doelwit gezond of ongezond wordt. Gezondheidscontroles van bronnen verschijnen ook hier.",
"standaloneHcTableTitle": "Gezondheidscontroles",
"standaloneHcSearchPlaceholder": "Zoek gezondheidscontroles…",
"standaloneHcAddButton": "Gezondheidscontrole aanmaken",
"standaloneHcCreateTitle": "Gezondheidscontrole aanmaken",
"standaloneHcEditTitle": "Gezondheidscontrole bewerken",
"standaloneHcDescription": "Configureer een HTTP- of TCP-gezondheidscontrole voor gebruik in waarschuwingsregels.",
"standaloneHcNameLabel": "Naam",
"standaloneHcNamePlaceholder": "Mijn HTTP-monitor",
"standaloneHcDeleteTitle": "Gezondheidscontrole verwijderen",
"standaloneHcDeleteQuestion": "Bevestig alstublieft dat u deze gezondheidscontrole wilt verwijderen.",
"standaloneHcDeleted": "Gezondheidscontrole verwijderd",
"standaloneHcSaved": "Gezondheidscontrole opgeslagen",
"standaloneHcColumnHealth": "Gezondheid",
"standaloneHcColumnMode": "Modus",
"standaloneHcColumnTarget": "Doelwit",
"standaloneHcHealthStateHealthy": "Gezond",
"standaloneHcHealthStateUnhealthy": "Ongezond",
"standaloneHcHealthStateUnknown": "Onbekend",
"standaloneHcFilterAnySite": "Alle sites",
"standaloneHcFilterAnyResource": "Alle bronnen",
"standaloneHcFilterMode": "Modus",
"standaloneHcFilterModeHttp": "HTTP",
"standaloneHcFilterModeTcp": "TCP",
"standaloneHcFilterModeSnmp": "SNMP",
"standaloneHcFilterModePing": "Ping",
"standaloneHcFilterHealth": "Gezondheid",
"standaloneHcFilterEnabled": "Ingeschakeld",
"standaloneHcFilterEnabledOn": "Ingeschakeld",
"standaloneHcFilterEnabledOff": "Uitgeschakeld",
"standaloneHcFilterSiteIdFallback": "Site {id}",
"standaloneHcFilterResourceIdFallback": "Bron {id}",
"blueprints": "Blauwdrukken", "blueprints": "Blauwdrukken",
"blueprintsDescription": "Gebruik declaratieve configuraties en bekijk vorige uitvoeringen.", "blueprintsDescription": "Gebruik declaratieve configuraties en bekijk vorige uitvoeringen.",
"blueprintAdd": "Blauwdruk toevoegen", "blueprintAdd": "Blauwdruk toevoegen",
@@ -1556,6 +1272,7 @@
"parsedContents": "Geparseerde inhoud (alleen lezen)", "parsedContents": "Geparseerde inhoud (alleen lezen)",
"enableDockerSocket": "Schakel Docker Blauwdruk in", "enableDockerSocket": "Schakel Docker Blauwdruk in",
"enableDockerSocketDescription": "Schakel Docker Socket label in voor blauwdruk labels. Pad naar Nieuw.", "enableDockerSocketDescription": "Schakel Docker Socket label in voor blauwdruk labels. Pad naar Nieuw.",
"enableDockerSocketLink": "Meer informatie",
"viewDockerContainers": "Bekijk Docker containers", "viewDockerContainers": "Bekijk Docker containers",
"containersIn": "Containers in {siteName}", "containersIn": "Containers in {siteName}",
"selectContainerDescription": "Selecteer een container om als hostnaam voor dit doel te gebruiken. Klik op een poort om een poort te gebruiken.", "selectContainerDescription": "Selecteer een container om als hostnaam voor dit doel te gebruiken. Klik op een poort om een poort te gebruiken.",
@@ -1597,8 +1314,7 @@
"initialSetupDescription": "Maak het eerste serverbeheeraccount aan. Er kan slechts één serverbeheerder bestaan. U kunt deze inloggegevens later altijd wijzigen.", "initialSetupDescription": "Maak het eerste serverbeheeraccount aan. Er kan slechts één serverbeheerder bestaan. U kunt deze inloggegevens later altijd wijzigen.",
"createAdminAccount": "Maak een beheeraccount aan", "createAdminAccount": "Maak een beheeraccount aan",
"setupErrorCreateAdmin": "Er is een fout opgetreden bij het maken van het serverbeheerdersaccount.", "setupErrorCreateAdmin": "Er is een fout opgetreden bij het maken van het serverbeheerdersaccount.",
"certificateStatus": "Certificaat", "certificateStatus": "Certificaatstatus",
"certificateStatusAutoRefreshHint": "Status ververst automatisch.",
"loading": "Bezig met laden", "loading": "Bezig met laden",
"loadingAnalytics": "Laden van Analytics", "loadingAnalytics": "Laden van Analytics",
"restart": "Herstarten", "restart": "Herstarten",
@@ -1667,7 +1383,6 @@
"pangolinUpdateAvailableReleaseNotes": "Uitgaveopmerkingen bekijken", "pangolinUpdateAvailableReleaseNotes": "Uitgaveopmerkingen bekijken",
"newtUpdateAvailable": "Update beschikbaar", "newtUpdateAvailable": "Update beschikbaar",
"newtUpdateAvailableInfo": "Er is een nieuwe versie van Newt beschikbaar. Update naar de nieuwste versie voor de beste ervaring.", "newtUpdateAvailableInfo": "Er is een nieuwe versie van Newt beschikbaar. Update naar de nieuwste versie voor de beste ervaring.",
"pangolinNodeUpdateAvailableInfo": "Er is een nieuwe versie van Pangolin Node beschikbaar. Update naar de nieuwste versie voor de beste ervaring.",
"domainPickerEnterDomain": "Domein", "domainPickerEnterDomain": "Domein",
"domainPickerPlaceholder": "mijnapp.voorbeeld.nl", "domainPickerPlaceholder": "mijnapp.voorbeeld.nl",
"domainPickerDescription": "Voer de volledige domein van de bron in om beschikbare opties te zien.", "domainPickerDescription": "Voer de volledige domein van de bron in om beschikbare opties te zien.",
@@ -1685,7 +1400,6 @@
"domainPickerNamespace": "Naamruimte: {namespace}", "domainPickerNamespace": "Naamruimte: {namespace}",
"domainPickerShowMore": "Meer weergeven", "domainPickerShowMore": "Meer weergeven",
"regionSelectorTitle": "Selecteer Regio", "regionSelectorTitle": "Selecteer Regio",
"domainPickerRemoteExitNodeWarning": "Opgegeven domeinen worden niet ondersteund wanneer websites verbinding maken met externe sluitnodes. Gebruik in plaats daarvan een aangepast domein. Om bronnen beschikbaar te maken op externe nodes.",
"regionSelectorInfo": "Het selecteren van een regio helpt ons om betere prestaties te leveren voor uw locatie. U hoeft niet in dezelfde regio als uw server te zijn.", "regionSelectorInfo": "Het selecteren van een regio helpt ons om betere prestaties te leveren voor uw locatie. U hoeft niet in dezelfde regio als uw server te zijn.",
"regionSelectorPlaceholder": "Kies een regio", "regionSelectorPlaceholder": "Kies een regio",
"regionSelectorComingSoon": "Komt binnenkort", "regionSelectorComingSoon": "Komt binnenkort",
@@ -1698,7 +1412,6 @@
"billingSites": "Sites", "billingSites": "Sites",
"billingUsers": "Gebruikers", "billingUsers": "Gebruikers",
"billingDomains": "Domeinen", "billingDomains": "Domeinen",
"billingOrganizations": "Ordenen",
"billingRemoteExitNodes": "Externe knooppunten", "billingRemoteExitNodes": "Externe knooppunten",
"billingNoLimitConfigured": "Geen limiet ingesteld", "billingNoLimitConfigured": "Geen limiet ingesteld",
"billingEstimatedPeriod": "Geschatte Facturatie Periode", "billingEstimatedPeriod": "Geschatte Facturatie Periode",
@@ -1741,7 +1454,6 @@
"failed": "Mislukt", "failed": "Mislukt",
"createNewOrgDescription": "Maak een nieuwe organisatie", "createNewOrgDescription": "Maak een nieuwe organisatie",
"organization": "Organisatie", "organization": "Organisatie",
"primary": "Primair",
"port": "Poort", "port": "Poort",
"securityKeyManage": "Beveiligingssleutels beheren", "securityKeyManage": "Beveiligingssleutels beheren",
"securityKeyDescription": "Voeg beveiligingssleutels toe of verwijder ze voor wachtwoordloze authenticatie", "securityKeyDescription": "Voeg beveiligingssleutels toe of verwijder ze voor wachtwoordloze authenticatie",
@@ -1839,16 +1551,6 @@
"billingFeatureLossWarning": "Kennisgeving beschikbaarheid", "billingFeatureLossWarning": "Kennisgeving beschikbaarheid",
"billingFeatureLossDescription": "Door downgraden worden functies die niet beschikbaar zijn in het nieuwe abonnement automatisch uitgeschakeld. Sommige instellingen en configuraties kunnen verloren gaan. Raadpleeg de prijsmatrix om te begrijpen welke functies niet langer beschikbaar zijn.", "billingFeatureLossDescription": "Door downgraden worden functies die niet beschikbaar zijn in het nieuwe abonnement automatisch uitgeschakeld. Sommige instellingen en configuraties kunnen verloren gaan. Raadpleeg de prijsmatrix om te begrijpen welke functies niet langer beschikbaar zijn.",
"billingUsageExceedsLimit": "Huidig gebruik ({current}) overschrijdt limiet ({limit})", "billingUsageExceedsLimit": "Huidig gebruik ({current}) overschrijdt limiet ({limit})",
"billingPastDueTitle": "Vervaldatum betaling",
"billingPastDueDescription": "Uw betaling is verlopen. Werk uw betaalmethode bij om uw huidige abonnementsfuncties te blijven gebruiken. Als dit niet is opgelost, zal je abonnement worden geannuleerd en zal je worden teruggezet naar de vrije rang.",
"billingUnpaidTitle": "Abonnement Onbetaald",
"billingUnpaidDescription": "Uw abonnement is niet betaald en u bent teruggekeerd naar het gratis niveau. Update uw betalingsmethode om uw abonnement te herstellen.",
"billingIncompleteTitle": "Betaling onvolledig",
"billingIncompleteDescription": "Uw betaling is onvolledig. Voltooi alstublieft het betalingsproces om uw abonnement te activeren.",
"billingIncompleteExpiredTitle": "Betaling verlopen",
"billingIncompleteExpiredDescription": "Uw betaling is nooit voltooid en verlopen. U bent teruggekeerd naar de gratis niveaus. Abonneer u opnieuw om de toegang tot betaalde functies te herstellen.",
"billingManageSubscription": "Beheer uw abonnement",
"billingResolvePaymentIssue": "Gelieve uw betalingsprobleem op te lossen voor het upgraden of downgraden",
"signUpTerms": { "signUpTerms": {
"IAgreeToThe": "Ik ga akkoord met de", "IAgreeToThe": "Ik ga akkoord met de",
"termsOfService": "servicevoorwaarden", "termsOfService": "servicevoorwaarden",
@@ -1907,7 +1609,6 @@
"configureHealthCheck": "Configureer Gezondheidscontrole", "configureHealthCheck": "Configureer Gezondheidscontrole",
"configureHealthCheckDescription": "Stel gezondheid monitor voor {target} in", "configureHealthCheckDescription": "Stel gezondheid monitor voor {target} in",
"enableHealthChecks": "Inschakelen Gezondheidscontroles", "enableHealthChecks": "Inschakelen Gezondheidscontroles",
"healthCheckDisabledStateDescription": "Wanneer uitgeschakeld, zal de site geen gezondheidscontroles uitvoeren en wordt de staat als onbekend beschouwd.",
"enableHealthChecksDescription": "Controleer de gezondheid van dit doel. U kunt een ander eindpunt monitoren dan het doel indien vereist.", "enableHealthChecksDescription": "Controleer de gezondheid van dit doel. U kunt een ander eindpunt monitoren dan het doel indien vereist.",
"healthScheme": "Methode", "healthScheme": "Methode",
"healthSelectScheme": "Selecteer methode", "healthSelectScheme": "Selecteer methode",
@@ -1923,24 +1624,6 @@
"timeIsInSeconds": "Tijd is in seconden", "timeIsInSeconds": "Tijd is in seconden",
"requireDeviceApproval": "Vereist goedkeuring van apparaat", "requireDeviceApproval": "Vereist goedkeuring van apparaat",
"requireDeviceApprovalDescription": "Gebruikers met deze rol hebben nieuwe apparaten nodig die door een beheerder zijn goedgekeurd voordat ze verbinding kunnen maken met bronnen en deze kunnen gebruiken.", "requireDeviceApprovalDescription": "Gebruikers met deze rol hebben nieuwe apparaten nodig die door een beheerder zijn goedgekeurd voordat ze verbinding kunnen maken met bronnen en deze kunnen gebruiken.",
"sshAccess": "SSH toegang",
"roleAllowSsh": "SSH toestaan",
"roleAllowSshAllow": "Toestaan",
"roleAllowSshDisallow": "Weigeren",
"roleAllowSshDescription": "Sta gebruikers met deze rol toe om verbinding te maken met bronnen via SSH. Indien uitgeschakeld kan de rol geen gebruik maken van SSH toegang.",
"sshSudoMode": "Sudo toegang",
"sshSudoModeNone": "geen",
"sshSudoModeNoneDescription": "Gebruiker kan geen commando's uitvoeren met sudo.",
"sshSudoModeFull": "Volledige Sudo",
"sshSudoModeFullDescription": "Gebruiker kan elk commando uitvoeren met een sudo.",
"sshSudoModeCommands": "Opdrachten",
"sshSudoModeCommandsDescription": "Gebruiker kan alleen de opgegeven commando's uitvoeren met de sudo.",
"sshSudo": "sudo toestaan",
"sshSudoCommands": "Sudo Commando's",
"sshSudoCommandsDescription": "Komma's gescheiden lijst van commando's waar de gebruiker een sudo mee mag uitvoeren.",
"sshCreateHomeDir": "Maak Home Directory",
"sshUnixGroups": "Unix groepen",
"sshUnixGroupsDescription": "Door komma's gescheiden Unix-groepen om de gebruiker toe te voegen aan de doelhost.",
"retryAttempts": "Herhaal Pogingen", "retryAttempts": "Herhaal Pogingen",
"expectedResponseCodes": "Verwachte Reactiecodes", "expectedResponseCodes": "Verwachte Reactiecodes",
"expectedResponseCodesDescription": "HTTP-statuscode die gezonde status aangeeft. Indien leeg wordt 200-300 als gezond beschouwd.", "expectedResponseCodesDescription": "HTTP-statuscode die gezonde status aangeeft. Indien leeg wordt 200-300 als gezond beschouwd.",
@@ -1957,20 +1640,9 @@
"healthCheckIntervalMin": "Controle interval moet minimaal 5 seconden zijn", "healthCheckIntervalMin": "Controle interval moet minimaal 5 seconden zijn",
"healthCheckTimeoutMin": "Timeout moet minimaal 1 seconde zijn", "healthCheckTimeoutMin": "Timeout moet minimaal 1 seconde zijn",
"healthCheckRetryMin": "Herhaal pogingen moet minimaal 1 zijn", "healthCheckRetryMin": "Herhaal pogingen moet minimaal 1 zijn",
"healthCheckMode": "Controlemodus",
"healthCheckStrategy": "Strategie",
"healthCheckModeDescription": "TCP-modus verifieert alleen connectiviteit. HTTP-modus valideert de HTTP-respons.",
"healthyThreshold": "Gezonde drempel",
"healthyThresholdDescription": "Opeenvolgende successen vereist voordat gemarkeerd wordt als gezond.",
"unhealthyThreshold": "Ongezonde drempel",
"unhealthyThresholdDescription": "Opeenvolgende fouten vereist voordat gemarkeerd wordt als ongezond.",
"healthCheckHealthyThresholdMin": "Gezonde drempel moet minimaal 1 zijn",
"healthCheckUnhealthyThresholdMin": "Ongezonde drempel moet minimaal 1 zijn",
"httpMethod": "HTTP-methode", "httpMethod": "HTTP-methode",
"selectHttpMethod": "Selecteer HTTP-methode", "selectHttpMethod": "Selecteer HTTP-methode",
"domainPickerSubdomainLabel": "Subdomein", "domainPickerSubdomainLabel": "Subdomein",
"domainPickerWildcard": "Wildcard",
"domainPickerWildcardPaidOnly": "Wildcard-subdomeinen zijn een betaalde functie. Upgrade om deze functie te gebruiken.",
"domainPickerBaseDomainLabel": "Basisdomein", "domainPickerBaseDomainLabel": "Basisdomein",
"domainPickerSearchDomains": "Zoek domeinen...", "domainPickerSearchDomains": "Zoek domeinen...",
"domainPickerNoDomainsFound": "Geen domeinen gevonden", "domainPickerNoDomainsFound": "Geen domeinen gevonden",
@@ -1996,12 +1668,12 @@
"resourcesTableAliasAddressInfo": "Dit adres is onderdeel van het hulpprogramma subnet van de organisatie. Het wordt gebruikt om aliasrecords op te lossen met behulp van interne DNS-resolutie.", "resourcesTableAliasAddressInfo": "Dit adres is onderdeel van het hulpprogramma subnet van de organisatie. Het wordt gebruikt om aliasrecords op te lossen met behulp van interne DNS-resolutie.",
"resourcesTableClients": "Clienten", "resourcesTableClients": "Clienten",
"resourcesTableAndOnlyAccessibleInternally": "en zijn alleen intern toegankelijk wanneer verbonden met een client.", "resourcesTableAndOnlyAccessibleInternally": "en zijn alleen intern toegankelijk wanneer verbonden met een client.",
"resourcesTableNoTargets": "Geen doelen",
"resourcesTableHealthy": "Gezond", "resourcesTableHealthy": "Gezond",
"resourcesTableDegraded": "Verminderde", "resourcesTableDegraded": "Verminderde",
"resourcesTableUnhealthy": "Ongezond", "resourcesTableOffline": "Offline",
"resourcesTableUnknown": "onbekend", "resourcesTableUnknown": "onbekend",
"resourcesTableNotMonitored": "Niet gecontroleerd", "resourcesTableNotMonitored": "Niet gecontroleerd",
"resourcesTableNoTargets": "Geen doelen",
"editInternalResourceDialogEditClientResource": "Privépagina bewerken", "editInternalResourceDialogEditClientResource": "Privépagina bewerken",
"editInternalResourceDialogUpdateResourceProperties": "Update de resource configuratie en access control voor {resourceName}", "editInternalResourceDialogUpdateResourceProperties": "Update de resource configuratie en access control voor {resourceName}",
"editInternalResourceDialogResourceProperties": "Bron eigenschappen", "editInternalResourceDialogResourceProperties": "Bron eigenschappen",
@@ -2027,11 +1699,6 @@
"editInternalResourceDialogModePort": "Poort", "editInternalResourceDialogModePort": "Poort",
"editInternalResourceDialogModeHost": "Hostnaam", "editInternalResourceDialogModeHost": "Hostnaam",
"editInternalResourceDialogModeCidr": "CIDR", "editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogScheme": "Schema",
"editInternalResourceDialogEnableSsl": "SSL inschakelen",
"editInternalResourceDialogEnableSslDescription": "Schakel SSL/TLS-encryptie in voor beveiligde HTTPS-verbindingen met de bestemming.",
"editInternalResourceDialogDestination": "Bestemming", "editInternalResourceDialogDestination": "Bestemming",
"editInternalResourceDialogDestinationHostDescription": "Het IP-adres of de hostnaam van de bron op het netwerk van de site.", "editInternalResourceDialogDestinationHostDescription": "Het IP-adres of de hostnaam van de bron op het netwerk van de site.",
"editInternalResourceDialogDestinationIPDescription": "Het IP of hostnaam adres van de bron op het netwerk van de site.", "editInternalResourceDialogDestinationIPDescription": "Het IP of hostnaam adres van de bron op het netwerk van de site.",
@@ -2047,7 +1714,6 @@
"createInternalResourceDialogName": "Naam", "createInternalResourceDialogName": "Naam",
"createInternalResourceDialogSite": "Site", "createInternalResourceDialogSite": "Site",
"selectSite": "Selecteer site...", "selectSite": "Selecteer site...",
"multiSitesSelectorSitesCount": "{count, plural, one {# site} other {# sites}}",
"noSitesFound": "Geen sites gevonden.", "noSitesFound": "Geen sites gevonden.",
"createInternalResourceDialogProtocol": "Protocol", "createInternalResourceDialogProtocol": "Protocol",
"createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogTcp": "TCP",
@@ -2076,19 +1742,11 @@
"createInternalResourceDialogModePort": "Poort", "createInternalResourceDialogModePort": "Poort",
"createInternalResourceDialogModeHost": "Hostnaam", "createInternalResourceDialogModeHost": "Hostnaam",
"createInternalResourceDialogModeCidr": "CIDR", "createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"scheme": "Schema",
"createInternalResourceDialogScheme": "Schema",
"createInternalResourceDialogEnableSsl": "SSL inschakelen",
"createInternalResourceDialogEnableSslDescription": "Schakel SSL/TLS-encryptie in voor beveiligde HTTPS-verbindingen met de bestemming.",
"createInternalResourceDialogDestination": "Bestemming", "createInternalResourceDialogDestination": "Bestemming",
"createInternalResourceDialogDestinationHostDescription": "Het IP-adres of de hostnaam van de bron op het netwerk van de site.", "createInternalResourceDialogDestinationHostDescription": "Het IP-adres of de hostnaam van de bron op het netwerk van de site.",
"createInternalResourceDialogDestinationCidrDescription": "Het CIDR-bereik van het document op het netwerk van de site.", "createInternalResourceDialogDestinationCidrDescription": "Het CIDR-bereik van het document op het netwerk van de site.",
"createInternalResourceDialogAlias": "Alias", "createInternalResourceDialogAlias": "Alias",
"createInternalResourceDialogAliasDescription": "Een optionele interne DNS-alias voor dit document.", "createInternalResourceDialogAliasDescription": "Een optionele interne DNS-alias voor dit document.",
"internalResourceDownstreamSchemeRequired": "Schema is vereist voor HTTP-bronnen",
"internalResourceHttpPortRequired": "Bestemmingspoort is vereist voor HTTP-bronnen",
"siteConfiguration": "Configuratie", "siteConfiguration": "Configuratie",
"siteAcceptClientConnections": "Accepteer clientverbindingen", "siteAcceptClientConnections": "Accepteer clientverbindingen",
"siteAcceptClientConnectionsDescription": "Sta gebruikersapparaten en clients toegang toe tot bronnen op deze site. Dit kan later worden gewijzigd.", "siteAcceptClientConnectionsDescription": "Sta gebruikersapparaten en clients toegang toe tot bronnen op deze site. Dit kan later worden gewijzigd.",
@@ -2174,40 +1832,6 @@
"exitNode": "Exit Node", "exitNode": "Exit Node",
"country": "Land", "country": "Land",
"rulesMatchCountry": "Momenteel gebaseerd op bron IP", "rulesMatchCountry": "Momenteel gebaseerd op bron IP",
"region": "Regio",
"selectRegion": "Selecteer regio",
"searchRegions": "Zoek regio's...",
"noRegionFound": "Geen regio gevonden.",
"rulesMatchRegion": "Selecteer een regionale groepering van landen",
"rulesErrorInvalidRegion": "Ongeldige regio",
"rulesErrorInvalidRegionDescription": "Selecteer een geldige regio.",
"regionAfrica": "Afrika",
"regionNorthernAfrica": "Noord-Afrika",
"regionEasternAfrica": "Oost Afrika",
"regionMiddleAfrica": "Midden Afrika",
"regionSouthernAfrica": "Zuidelijk Afrika",
"regionWesternAfrica": "Westelijk Afrika",
"regionAmericas": "Amerika's",
"regionCaribbean": "Caraïben",
"regionCentralAmerica": "Midden-Amerika",
"regionSouthAmerica": "Zuid Amerika",
"regionNorthernAmerica": "Noord-Amerika",
"regionAsia": "Azië",
"regionCentralAsia": "Centraal-Azië",
"regionEasternAsia": "Oost-Azië",
"regionSouthEasternAsia": "Zuid-Oost-Azië",
"regionSouthernAsia": "Zuid-Azië",
"regionWesternAsia": "Westelijk Azië",
"regionEurope": "Europa",
"regionEasternEurope": "Oost-Europa",
"regionNorthernEurope": "Noord-Europa",
"regionSouthernEurope": "Zuid-Europa",
"regionWesternEurope": "West-Europa",
"regionOceania": "Oceania",
"regionAustraliaAndNewZealand": "Australië en Nieuw-Zeeland",
"regionMelanesia": "Melanesia",
"regionMicronesia": "Micronesia",
"regionPolynesia": "Polynesia",
"managedSelfHosted": { "managedSelfHosted": {
"title": "Beheerde Self-Hosted", "title": "Beheerde Self-Hosted",
"description": "betrouwbaardere en slecht onderhouden Pangolin server met extra klokken en klokkenluiders", "description": "betrouwbaardere en slecht onderhouden Pangolin server met extra klokken en klokkenluiders",
@@ -2246,7 +1870,7 @@
}, },
"internationaldomaindetected": "Internationaal Domein Gedetecteerd", "internationaldomaindetected": "Internationaal Domein Gedetecteerd",
"willbestoredas": "Zal worden opgeslagen als:", "willbestoredas": "Zal worden opgeslagen als:",
"roleMappingDescription": "Bepaal hoe rollen aan gebruikers worden toegewezen wanneer ze zich aanmelden met deze identiteitsprovider.", "roleMappingDescription": "Bepaal hoe rollen worden toegewezen aan gebruikers wanneer ze inloggen wanneer Auto Provision is ingeschakeld.",
"selectRole": "Selecteer een rol", "selectRole": "Selecteer een rol",
"roleMappingExpression": "Expressie", "roleMappingExpression": "Expressie",
"selectRolePlaceholder": "Kies een rol", "selectRolePlaceholder": "Kies een rol",
@@ -2256,25 +1880,6 @@
"invalidValue": "Ongeldige waarde", "invalidValue": "Ongeldige waarde",
"idpTypeLabel": "Identiteit provider type", "idpTypeLabel": "Identiteit provider type",
"roleMappingExpressionPlaceholder": "bijvoorbeeld bevat (groepen, 'admin') && 'Admin' ½ 'Member'", "roleMappingExpressionPlaceholder": "bijvoorbeeld bevat (groepen, 'admin') && 'Admin' ½ 'Member'",
"roleMappingModeFixedRoles": "Vaste rollen",
"roleMappingModeMappingBuilder": "Toewijzing Bouwer",
"roleMappingModeRawExpression": "Ruwe expressie",
"roleMappingFixedRolesPlaceholderSelect": "Selecteer één of meer rollen",
"roleMappingFixedRolesPlaceholderFreeform": "Typ rolnamen (exacte overeenkomst per organisatie)",
"roleMappingFixedRolesDescriptionSameForAll": "Wijs dezelfde rolset toe aan elke auto-provisioned gebruiker.",
"roleMappingFixedRolesDescriptionDefaultPolicy": "Voor standaardbeleid, typ rolnamen die bestaan in elke organisatie waar gebruikers worden opgegeven. Namen moeten exact overeenkomen.",
"roleMappingClaimPath": "Claim pad",
"roleMappingClaimPathPlaceholder": "Groepen",
"roleMappingClaimPathDescription": "Pad in de token payload die bronwaarden bevat (bijvoorbeeld groepen).",
"roleMappingMatchValue": "Kies een waarde",
"roleMappingAssignRoles": "Rollen toewijzen",
"roleMappingAddMappingRule": "Toewijzingsregel toevoegen",
"roleMappingRawExpressionResultDescription": "Expressie moet een tekenreeks of tekenreeks evalueren.",
"roleMappingRawExpressionResultDescriptionSingleRole": "Expressie moet evalueren naar een tekenreeks (een naam met één rol).",
"roleMappingMatchValuePlaceholder": "Overeenkomende waarde (bijvoorbeeld: admin)",
"roleMappingAssignRolesPlaceholderFreeform": "Typ rolnamen (exact per org)",
"roleMappingBuilderFreeformRowHint": "Rol namen moeten overeenkomen met een rol in elke doelorganisatie.",
"roleMappingRemoveRule": "Verwijderen",
"idpGoogleConfiguration": "Google Configuratie", "idpGoogleConfiguration": "Google Configuratie",
"idpGoogleConfigurationDescription": "Configureer de Google OAuth2-referenties", "idpGoogleConfigurationDescription": "Configureer de Google OAuth2-referenties",
"idpGoogleClientIdDescription": "Google OAuth2 Client ID", "idpGoogleClientIdDescription": "Google OAuth2 Client ID",
@@ -2311,9 +1916,6 @@
"authPageBrandingQuestionRemove": "Weet u zeker dat u de branding voor Auth-pagina's wilt verwijderen?", "authPageBrandingQuestionRemove": "Weet u zeker dat u de branding voor Auth-pagina's wilt verwijderen?",
"authPageBrandingDeleteConfirm": "Bevestig verwijder Branding", "authPageBrandingDeleteConfirm": "Bevestig verwijder Branding",
"brandingLogoURL": "Het logo-URL", "brandingLogoURL": "Het logo-URL",
"brandingLogoURLOrPath": "Logo URL of pad",
"brandingLogoPathDescription": "Voer een URL of een lokaal pad in.",
"brandingLogoURLDescription": "Voer een openbaar toegankelijke URL in voor uw logo afbeelding.",
"brandingPrimaryColor": "Primaire kleur", "brandingPrimaryColor": "Primaire kleur",
"brandingLogoWidth": "Breedte (px)", "brandingLogoWidth": "Breedte (px)",
"brandingLogoHeight": "Hoogte (px)", "brandingLogoHeight": "Hoogte (px)",
@@ -2338,11 +1940,9 @@
"selectDomainForOrgAuthPage": "Selecteer een domein voor de authenticatiepagina van de organisatie", "selectDomainForOrgAuthPage": "Selecteer een domein voor de authenticatiepagina van de organisatie",
"domainPickerProvidedDomain": "Opgegeven domein", "domainPickerProvidedDomain": "Opgegeven domein",
"domainPickerFreeProvidedDomain": "Gratis verstrekt domein", "domainPickerFreeProvidedDomain": "Gratis verstrekt domein",
"domainPickerFreeDomainsPaidFeature": "Geleverde domeinen zijn een betaalde functie. Abonneer je om een domein bij je plan te krijgen - je hoeft er zelf geen mee te brengen.",
"domainPickerVerified": "Geverifieerd", "domainPickerVerified": "Geverifieerd",
"domainPickerUnverified": "Ongeverifieerd", "domainPickerUnverified": "Ongeverifieerd",
"domainPickerManual": "Handleiding", "domainPickerInvalidSubdomainStructure": "Dit subdomein bevat ongeldige tekens of structuur. Het zal automatisch worden gesaneerd wanneer u opslaat.",
"domainPickerInvalidSubdomainStructure": "Ongeldige tekens worden gesaneerd bij het opslaan.",
"domainPickerError": "Foutmelding", "domainPickerError": "Foutmelding",
"domainPickerErrorLoadDomains": "Fout bij het laden van organisatiedomeinen", "domainPickerErrorLoadDomains": "Fout bij het laden van organisatiedomeinen",
"domainPickerErrorCheckAvailability": "Kan domein beschikbaarheid niet controleren", "domainPickerErrorCheckAvailability": "Kan domein beschikbaarheid niet controleren",
@@ -2355,7 +1955,7 @@
"orgAuthChooseIdpDescription": "Kies uw identiteitsprovider om door te gaan", "orgAuthChooseIdpDescription": "Kies uw identiteitsprovider om door te gaan",
"orgAuthNoIdpConfigured": "Deze organisatie heeft geen identiteitsproviders geconfigureerd. Je kunt in plaats daarvan inloggen met je Pangolin-identiteit.", "orgAuthNoIdpConfigured": "Deze organisatie heeft geen identiteitsproviders geconfigureerd. Je kunt in plaats daarvan inloggen met je Pangolin-identiteit.",
"orgAuthSignInWithPangolin": "Log in met Pangolin", "orgAuthSignInWithPangolin": "Log in met Pangolin",
"orgAuthSignInToOrg": "Organisatie Identiteitsprovider (SSO)", "orgAuthSignInToOrg": "Log in bij een organisatie",
"orgAuthSelectOrgTitle": "Organisatie Inloggen", "orgAuthSelectOrgTitle": "Organisatie Inloggen",
"orgAuthSelectOrgDescription": "Voer je organisatie-ID in om verder te gaan", "orgAuthSelectOrgDescription": "Voer je organisatie-ID in om verder te gaan",
"orgAuthOrgIdPlaceholder": "jouw-organisatie", "orgAuthOrgIdPlaceholder": "jouw-organisatie",
@@ -2571,10 +2171,10 @@
}, },
"scale": { "scale": {
"title": "Schaal", "title": "Schaal",
"description": "Enterprise-functies, 50 gebruikers, 100 sites en prioritaire ondersteuning." "description": "Enterprise functies, 50 gebruikers, 50 sites en prioriteit ondersteuning."
} }
}, },
"personalUseOnly": "Alleen voor persoonlijk gebruik (gratis licentie - geen afrekening)", "personalUseOnly": "Alleen persoonlijk gebruik (gratis licentie - geen afrekenen)",
"buttons": { "buttons": {
"continueToCheckout": "Doorgaan naar afrekenen" "continueToCheckout": "Doorgaan naar afrekenen"
}, },
@@ -2648,7 +2248,6 @@
"validPassword": "Geldig wachtwoord", "validPassword": "Geldig wachtwoord",
"validEmail": "Valid email", "validEmail": "Valid email",
"validSSO": "Valid SSO", "validSSO": "Valid SSO",
"connectedClient": "Verbonden Client",
"resourceBlocked": "Bron geblokkeerd", "resourceBlocked": "Bron geblokkeerd",
"droppedByRule": "Achtergelaten door regel", "droppedByRule": "Achtergelaten door regel",
"noSessions": "Geen sessies", "noSessions": "Geen sessies",
@@ -2674,8 +2273,6 @@
"logRetentionAccessDescription": "Hoe lang de toegangslogboeken behouden blijven", "logRetentionAccessDescription": "Hoe lang de toegangslogboeken behouden blijven",
"logRetentionActionLabel": "Actie log bewaring", "logRetentionActionLabel": "Actie log bewaring",
"logRetentionActionDescription": "Hoe lang de action logs behouden moeten blijven", "logRetentionActionDescription": "Hoe lang de action logs behouden moeten blijven",
"logRetentionConnectionLabel": "Connectie log bewaring",
"logRetentionConnectionDescription": "Hoe lang de verbindingslogs onderhouden",
"logRetentionDisabled": "Uitgeschakeld", "logRetentionDisabled": "Uitgeschakeld",
"logRetention3Days": "3 dagen", "logRetention3Days": "3 dagen",
"logRetention7Days": "7 dagen", "logRetention7Days": "7 dagen",
@@ -2686,15 +2283,8 @@
"logRetentionEndOfFollowingYear": "Einde van volgend jaar", "logRetentionEndOfFollowingYear": "Einde van volgend jaar",
"actionLogsDescription": "Bekijk een geschiedenis van acties die worden uitgevoerd in deze organisatie", "actionLogsDescription": "Bekijk een geschiedenis van acties die worden uitgevoerd in deze organisatie",
"accessLogsDescription": "Toegangsverificatieverzoeken voor resources in deze organisatie bekijken", "accessLogsDescription": "Toegangsverificatieverzoeken voor resources in deze organisatie bekijken",
"connectionLogs": "Connectie Logs", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"connectionLogsDescription": "Toon verbindingslogs voor tunnels in deze organisatie", "ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
"sidebarLogsConnection": "Connectie Logs",
"sidebarLogsStreaming": "Streamen",
"sourceAddress": "Bron adres",
"destinationAddress": "Adres bestemming",
"duration": "Duur",
"licenseRequiredToUse": "Een <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> licentie of <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is vereist om deze functie te gebruiken. <bookADemoLink>Boek een demo of POC trial</bookADemoLink>.",
"ossEnterpriseEditionRequired": "De <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is vereist om deze functie te gebruiken. Deze functie is ook beschikbaar in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Boek een demo of POC trial</bookADemoLink>.",
"certResolver": "Certificaat Resolver", "certResolver": "Certificaat Resolver",
"certResolverDescription": "Selecteer de certificaat resolver die moet worden gebruikt voor deze resource.", "certResolverDescription": "Selecteer de certificaat resolver die moet worden gebruikt voor deze resource.",
"selectCertResolver": "Certificaat Resolver selecteren", "selectCertResolver": "Certificaat Resolver selecteren",
@@ -2836,9 +2426,6 @@
"machineClients": "Machine Clienten", "machineClients": "Machine Clienten",
"install": "Installeren", "install": "Installeren",
"run": "Uitvoeren", "run": "Uitvoeren",
"envFile": "Omgevingsbestand",
"serviceFile": "Servicebestand",
"enableAndStart": "Inschakelen en Starten",
"clientNameDescription": "De weergavenaam van de client die later gewijzigd kan worden.", "clientNameDescription": "De weergavenaam van de client die later gewijzigd kan worden.",
"clientAddress": "Klant adres (Geavanceerd)", "clientAddress": "Klant adres (Geavanceerd)",
"setupFailedToFetchSubnet": "Kan standaard subnet niet ophalen", "setupFailedToFetchSubnet": "Kan standaard subnet niet ophalen",
@@ -2887,30 +2474,13 @@
"editInternalResourceDialogAddClients": "Clienten toevoegen", "editInternalResourceDialogAddClients": "Clienten toevoegen",
"editInternalResourceDialogDestinationLabel": "Bestemming", "editInternalResourceDialogDestinationLabel": "Bestemming",
"editInternalResourceDialogDestinationDescription": "Specificeer het bestemmingsadres voor de interne bron. Dit kan een hostnaam, IP-adres of CIDR-bereik zijn, afhankelijk van de geselecteerde modus. Stel optioneel een interne DNS-alias in voor eenvoudigere identificatie.", "editInternalResourceDialogDestinationDescription": "Specificeer het bestemmingsadres voor de interne bron. Dit kan een hostnaam, IP-adres of CIDR-bereik zijn, afhankelijk van de geselecteerde modus. Stel optioneel een interne DNS-alias in voor eenvoudigere identificatie.",
"internalResourceFormMultiSiteRoutingHelp": "Selecteren van meerdere sites maakt veerkrachtige routing en failover mogelijk voor hoge beschikbaarheid.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Meer informatie",
"editInternalResourceDialogPortRestrictionsDescription": "Beperk toegang tot specifieke TCP/UDP-poorten of sta alle poorten toe/blokkeer.", "editInternalResourceDialogPortRestrictionsDescription": "Beperk toegang tot specifieke TCP/UDP-poorten of sta alle poorten toe/blokkeer.",
"createInternalResourceDialogHttpConfiguration": "HTTP-configuratie",
"createInternalResourceDialogHttpConfigurationDescription": "Kies het domein dat cliënten zullen gebruiken om deze bron via HTTP of HTTPS te bereiken.",
"editInternalResourceDialogHttpConfiguration": "HTTP-configuratie",
"editInternalResourceDialogHttpConfigurationDescription": "Kies het domein dat cliënten zullen gebruiken om deze bron via HTTP of HTTPS te bereiken.",
"editInternalResourceDialogTcp": "TCP", "editInternalResourceDialogTcp": "TCP",
"editInternalResourceDialogUdp": "UDP", "editInternalResourceDialogUdp": "UDP",
"editInternalResourceDialogIcmp": "ICMP", "editInternalResourceDialogIcmp": "ICMP",
"editInternalResourceDialogAccessControl": "Toegangs controle", "editInternalResourceDialogAccessControl": "Toegangs controle",
"editInternalResourceDialogAccessControlDescription": "Beheer welke rollen, gebruikers en machineclients toegang hebben tot deze bron wanneer ze zijn verbonden. Beheerders hebben altijd toegang.", "editInternalResourceDialogAccessControlDescription": "Beheer welke rollen, gebruikers en machineclients toegang hebben tot deze bron wanneer ze zijn verbonden. Beheerders hebben altijd toegang.",
"editInternalResourceDialogPortRangeValidationError": "Poortbereik moet \"*\" zijn voor alle poorten, of een komma-gescheiden lijst van poorten en bereiken (bijv. \"80,443,8000-9000\"). Poorten moeten tussen 1 en 65535 zijn.", "editInternalResourceDialogPortRangeValidationError": "Poortbereik moet \"*\" zijn voor alle poorten, of een komma-gescheiden lijst van poorten en bereiken (bijv. \"80,443,8000-9000\"). Poorten moeten tussen 1 en 65535 zijn.",
"internalResourceAuthDaemonStrategy": "SSH Auth Daemon locatie",
"internalResourceAuthDaemonStrategyDescription": "Kies waar de SSH authenticatie daemon wordt uitgevoerd: op de website (Newt) of op een externe host.",
"internalResourceAuthDaemonDescription": "De SSH authenticatie daemon zorgt voor SSH sleutelondertekening en PAM authenticatie voor deze resource. Kies of het wordt uitgevoerd op de website (Nieuw) of op een afzonderlijke externe host. Zie <docsLink>de documentatie</docsLink> voor meer.",
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
"internalResourceAuthDaemonStrategyPlaceholder": "Selecteer strategie",
"internalResourceAuthDaemonStrategyLabel": "Locatie",
"internalResourceAuthDaemonSite": "In de site",
"internalResourceAuthDaemonSiteDescription": "Auth daemon draait op de site (Newt).",
"internalResourceAuthDaemonRemote": "Externe host",
"internalResourceAuthDaemonRemoteDescription": "Authenticatiedaemon draait op een host die niet de site is.",
"internalResourceAuthDaemonPort": "Daemon poort (optioneel)",
"orgAuthWhatsThis": "Waar kan ik mijn organisatie-ID vinden?", "orgAuthWhatsThis": "Waar kan ik mijn organisatie-ID vinden?",
"learnMore": "Meer informatie", "learnMore": "Meer informatie",
"backToHome": "Ga terug naar startpagina", "backToHome": "Ga terug naar startpagina",
@@ -2932,9 +2502,6 @@
"maintenancePageMessagePlaceholder": "We keren snel terug! Onze site ondergaat momenteel gepland onderhoud.", "maintenancePageMessagePlaceholder": "We keren snel terug! Onze site ondergaat momenteel gepland onderhoud.",
"maintenancePageMessageDescription": "Gedetailleerd bericht dat het onderhoud uitlegt", "maintenancePageMessageDescription": "Gedetailleerd bericht dat het onderhoud uitlegt",
"maintenancePageTimeTitle": "Geschatte voltooiingstijd (optioneel)", "maintenancePageTimeTitle": "Geschatte voltooiingstijd (optioneel)",
"privateMaintenanceScreenTitle": "Privéscherm maintenance screen",
"privateMaintenanceScreenMessage": "Dit domein wordt gebruikt op een privébron. Verbind met de Pangolin client om toegang te krijgen tot deze bron.",
"privateMaintenanceScreenSteps": "Eenmaal verbonden, als u dit bericht nog steeds ziet, kan het DNS-cache van uw browser nog steeds naar het oude adres wijzen. Om dit te corrigeren: sluit en heropen dit tabblad, of uw browser, dan navigeer weer naar deze pagina.",
"maintenanceTime": "bijv. 2 uur, 1 nov om 17:00", "maintenanceTime": "bijv. 2 uur, 1 nov om 17:00",
"maintenanceEstimatedTimeDescription": "Wanneer u verwacht dat het onderhoud voltooid is", "maintenanceEstimatedTimeDescription": "Wanneer u verwacht dat het onderhoud voltooid is",
"editDomain": "Domein bewerken", "editDomain": "Domein bewerken",
@@ -3043,166 +2610,5 @@
"approvalsEmptyStateStep2Title": "Toestel goedkeuringen inschakelen", "approvalsEmptyStateStep2Title": "Toestel goedkeuringen inschakelen",
"approvalsEmptyStateStep2Description": "Bewerk een rol en schakel de optie 'Vereist Apparaat Goedkeuringen' in. Gebruikers met deze rol hebben admin goedkeuring nodig voor nieuwe apparaten.", "approvalsEmptyStateStep2Description": "Bewerk een rol en schakel de optie 'Vereist Apparaat Goedkeuringen' in. Gebruikers met deze rol hebben admin goedkeuring nodig voor nieuwe apparaten.",
"approvalsEmptyStatePreviewDescription": "Voorbeeld: Indien ingeschakeld, zullen in afwachting van apparaatverzoeken hier verschijnen om te beoordelen", "approvalsEmptyStatePreviewDescription": "Voorbeeld: Indien ingeschakeld, zullen in afwachting van apparaatverzoeken hier verschijnen om te beoordelen",
"approvalsEmptyStateButtonText": "Rollen beheren", "approvalsEmptyStateButtonText": "Rollen beheren"
"domainErrorTitle": "We ondervinden problemen bij het controleren van uw domein",
"idpAdminAutoProvisionPoliciesTabHint": "Configureer rolverrekening en organisatie beleid in het <policiesTabLink>Auto Provision Settings</policiesTabLink> tab.",
"streamingTitle": "Event streaming",
"streamingDescription": "Stream events van uw organisatie naar externe bestemmingen in realtime.",
"streamingUnnamedDestination": "Naamloze bestemming",
"streamingNoUrlConfigured": "Geen URL ingesteld",
"streamingAddDestination": "Bestemming toevoegen",
"streamingHttpWebhookTitle": "HTTP Webhook",
"streamingHttpWebhookDescription": "Stuur gebeurtenissen naar elk HTTP eindpunt met flexibele authenticatie en template.",
"streamingS3Title": "Amazon S3",
"streamingS3Description": "Stream events naar een S3-compatibele object-opslagemmer. Binnenkort beschikbaar.",
"streamingDatadogTitle": "Datadog",
"streamingDatadogDescription": "Stuur gebeurtenissen rechtstreeks door naar je Datadog account. Binnenkort beschikbaar.",
"streamingTypePickerDescription": "Kies een bestemmingstype om te beginnen.",
"streamingFailedToLoad": "Laden van bestemmingen mislukt",
"streamingUnexpectedError": "Er is een onverwachte fout opgetreden.",
"streamingFailedToUpdate": "Bijwerken bestemming mislukt",
"streamingDeletedSuccess": "Bestemming succesvol verwijderd",
"streamingFailedToDelete": "Verwijderen van bestemming mislukt",
"streamingDeleteTitle": "Verwijder bestemming",
"streamingDeleteButtonText": "Verwijder bestemming",
"streamingDeleteDialogAreYouSure": "Weet u zeker dat u wilt verwijderen",
"streamingDeleteDialogThisDestination": "deze bestemming",
"streamingDeleteDialogPermanentlyRemoved": "? Alle configuratie zal permanent worden verwijderd.",
"httpDestEditTitle": "Bewerk bestemming",
"httpDestAddTitle": "Voeg HTTP bestemming toe",
"httpDestEditDescription": "Werk de configuratie voor deze HTTP-event streaming bestemming bij.",
"httpDestAddDescription": "Configureer een nieuw HTTP-eindpunt om de gebeurtenissen van uw organisatie te ontvangen.",
"S3DestEditTitle": "Bestemming bewerken",
"S3DestAddTitle": "S3-bestemming toevoegen",
"S3DestEditDescription": "Werk de configuratie bij voor deze S3-gebeurtenisstreamingbestemming.",
"S3DestAddDescription": "Configureer een nieuw S3-eindpunt om de gebeurtenissen van uw organisatie te ontvangen.",
"datadogDestEditTitle": "Bestemming bewerken",
"datadogDestAddTitle": "Datadog-bestemming toevoegen",
"datadogDestEditDescription": "Werk de configuratie bij voor deze Datadog-gebeurtenisstreamingbestemming.",
"datadogDestAddDescription": "Configureer een nieuw Datadog-eindpunt om de gebeurtenissen van uw organisatie te ontvangen.",
"httpDestTabSettings": "Instellingen",
"httpDestTabHeaders": "Kopteksten",
"httpDestTabBody": "Lichaam",
"httpDestTabLogs": "Logboeken",
"httpDestNamePlaceholder": "Mijn HTTP-bestemming",
"httpDestUrlLabel": "Bestemming URL",
"httpDestUrlErrorHttpRequired": "URL moet http of https gebruiken",
"httpDestUrlErrorHttpsRequired": "HTTPS is vereist op cloud implementaties",
"httpDestUrlErrorInvalid": "Voer een geldige URL in (bijv. https://example.com/webhook)",
"httpDestAuthTitle": "Authenticatie",
"httpDestAuthDescription": "Kies hoe verzoeken voor uw eindpunt zijn geverifieerd.",
"httpDestAuthNoneTitle": "Geen authenticatie",
"httpDestAuthNoneDescription": "Stuurt verzoeken zonder toestemmingskop.",
"httpDestAuthBearerTitle": "Betere Token",
"httpDestAuthBearerDescription": "Voegt een Authorization: Bearer '<token>' header toe aan elk verzoek.",
"httpDestAuthBearerPlaceholder": "Uw API-sleutel of -token",
"httpDestAuthBasicTitle": "Basis authenticatie",
"httpDestAuthBasicDescription": "Voegt een Authorization: Basic '<credentials>' header toe. Verstrek inloggegevens als gebruikersnaam:wachtwoord.",
"httpDestAuthBasicPlaceholder": "Gebruikersnaam:wachtwoord",
"httpDestAuthCustomTitle": "Aangepaste koptekst",
"httpDestAuthCustomDescription": "Specificeer een aangepaste HTTP header naam en waarde voor authenticatie (bijv. X-API-Key).",
"httpDestAuthCustomHeaderNamePlaceholder": "Header naam (bijv. X-API-Key)",
"httpDestAuthCustomHeaderValuePlaceholder": "Header waarde",
"httpDestCustomHeadersTitle": "Aangepaste HTTP Headers",
"httpDestCustomHeadersDescription": "Voeg aangepaste headers toe aan elk uitgaande verzoek. Handig voor statische tokens of een aangepast Content-Type. Standaard Content-Type: application/json wordt verzonden.",
"httpDestNoHeadersConfigured": "Geen aangepaste headers geconfigureerd. Klik op \"Header\" om er een toe te voegen.",
"httpDestHeaderNamePlaceholder": "Naam koptekst",
"httpDestHeaderValuePlaceholder": "Waarde",
"httpDestAddHeader": "Koptekst toevoegen",
"httpDestBodyTemplateTitle": "Aangepaste Body Sjabloon",
"httpDestBodyTemplateDescription": "Bestuur de JSON payload structuur verzonden naar uw eindpunt. Indien uitgeschakeld, wordt een standaard JSON object verzonden voor elke event.",
"httpDestEnableBodyTemplate": "Aangepaste lichaam sjabloon inschakelen",
"httpDestBodyTemplateLabel": "Body sjabloon (JSON)",
"httpDestBodyTemplateHint": "Gebruik sjabloonvariabelen om te verwijzen naar gebeurtenisvelden in uw payload.",
"httpDestPayloadFormatTitle": "Payload formaat",
"httpDestPayloadFormatDescription": "Hoe evenementen worden geserialiseerd in elk verzoeklichaam.",
"httpDestFormatJsonArrayTitle": "JSON matrix",
"httpDestFormatJsonArrayDescription": "Eén verzoek per batch, lichaam is een JSON-array. Compatibel met de meeste algemene webhooks en Datadog.",
"httpDestFormatNdjsonTitle": "NDJSON",
"httpDestFormatNdjsonDescription": "Eén aanvraag per batch, lichaam is nieuwe JSON gescheiden - één object per regel, geen buitenste array. Vereist door Splunk HEC, Elastic / OpenSearch, en Grafana Loki.",
"httpDestFormatSingleTitle": "Eén afspraak per verzoek",
"httpDestFormatSingleDescription": "Stuurt een aparte HTTP POST voor elk individueel event. Gebruik alleen voor eindpunten die geen batches kunnen verwerken.",
"httpDestLogTypesTitle": "Log soorten",
"httpDestLogTypesDescription": "Kies welke log types doorgestuurd worden naar deze bestemming. Alleen ingeschakelde log types worden gestreden.",
"httpDestAccessLogsTitle": "Toegang tot logboek",
"httpDestAccessLogsDescription": "Hulpbrontoegangspogingen, inclusief geauthenticeerde en weigerde aanvragen.",
"httpDestActionLogsTitle": "Actie logs",
"httpDestActionLogsDescription": "Administratieve acties uitgevoerd door gebruikers binnen de organisatie.",
"httpDestConnectionLogsTitle": "Connectie Logs",
"httpDestConnectionLogsDescription": "Verbinding met de Site en tunnel maken verbroken, inclusief verbindingen en verbindingen.",
"httpDestRequestLogsTitle": "Logboeken aanvragen",
"httpDestRequestLogsDescription": "HTTP request logs voor proxied hulpmiddelen, waaronder methode, pad en response code.",
"httpDestSaveChanges": "Wijzigingen opslaan",
"httpDestCreateDestination": "Maak bestemming aan",
"httpDestUpdatedSuccess": "Bestemming succesvol bijgewerkt",
"httpDestCreatedSuccess": "Bestemming succesvol aangemaakt",
"httpDestUpdateFailed": "Bijwerken bestemming mislukt",
"httpDestCreateFailed": "Aanmaken bestemming mislukt",
"followRedirects": "Volg omleidingen",
"followRedirectsDescription": "Volg automatisch HTTP-omleidingen voor verzoeken.",
"alertingErrorWebhookUrl": "Voer een geldige URL voor de webhook in.",
"healthCheckStrategyHttp": "Valideert connectiviteit en controleert de HTTP-responsstatus.",
"healthCheckStrategyTcp": "Verifieert alleen TCP-connectiviteit zonder de respons te inspecteren.",
"healthCheckStrategySnmp": "Maakt een SNMP-verzoek om de gezondheid van netwerkapparaten en infrastructuur te controleren.",
"healthCheckStrategyIcmp": "Gebruikt ICMP-verzoeken (pings) om te controleren of een bron bereikbaar en responsief is.",
"healthCheckTabStrategy": "Strategie",
"healthCheckTabConnection": "Verbinding",
"healthCheckTabAdvanced": "Geavanceerd",
"healthCheckStrategyNotAvailable": "Deze strategie is niet beschikbaar. Neem contact op met sales om deze functie in te schakelen.",
"uptime30d": "Beschikbaarheid (30d)",
"idpAddActionCreateNew": "Nieuwe identiteitsprovider aanmaken",
"idpAddActionImportFromOrg": "Importeer vanuit een andere organisatie",
"idpImportDialogTitle": "Importeer Identiteitsprovider",
"idpImportDialogDescription": "Kies een identiteitsprovider van een organisatie waar u beheerder bent. Het wordt gekoppeld aan deze organisatie.",
"idpImportSearchPlaceholder": "Zoek op organisatie- of providernamen...",
"idpImportEmpty": "Geen identiteitsproviders gevonden.",
"idpImportedDescription": "Identiteitsprovider succesvol geïmporteerd.",
"idpDeleteGlobalQuestion": "Weet u zeker dat u deze identiteitsprovider permanent wilt verwijderen?",
"idpDeleteGlobalDescription": "Hiermee wordt de identiteitsprovider permanent verwijderd uit alle organisaties waarmee het is geassocieerd.",
"idpUnassociateTitle": "Koppel Identiteitsprovider los",
"idpUnassociateQuestion": "Weet u zeker dat u deze identiteitsprovider van deze organisatie wilt loskoppelen?",
"idpUnassociateDescription": "Alle gebruikers die aan deze identiteitsprovider zijn gekoppeld, worden uit deze organisatie verwijderd, maar de identiteitsprovider blijft bestaan voor andere gerelateerde organisaties.",
"idpUnassociateConfirm": "Bevestig ontkoppelen identiteitsprovider",
"idpUnassociateWarning": "Dit kan niet ongedaan worden gemaakt voor deze organisatie.",
"idpUnassociatedDescription": "Identiteitsprovider succesvol losgekoppeld van deze organisatie",
"idpUnassociateMenu": "Ontkoppelen",
"idpDeleteAllOrgsMenu": "Verwijderen",
"publicIpEndpoint": "Eindpunt",
"lastTriggeredAt": "Laatste Trigger",
"reject": "Afwijzen",
"uptimeDaysAgo": "{count} dagen geleden",
"uptimeToday": "Vandaag",
"uptimeNoDataAvailable": "Geen gegevens beschikbaar",
"uptimeSuffix": "werktijd",
"uptimeDowntimeSuffix": "uitvaltijd",
"uptimeTooltipUptimeLabel": "Werktijd",
"uptimeTooltipDowntimeLabel": "Uitvaltijd",
"uptimeOngoing": "lopend",
"uptimeNoMonitoringData": "Geen monitoringgegevens",
"uptimeNoData": "Geen gegevens",
"uptimeMiniBarDown": "Onder",
"uptimeSectionTitle": "Werktijd",
"uptimeSectionDescription": "Beschikbaarheid over de laatste {days} dagen",
"uptimeAddAlert": "Alarm toevoegen",
"uptimeViewAlerts": "Meldingen bekijken",
"uptimeCreateEmailAlert": "E-mailalert aanmaken",
"uptimeAlertDescriptionSite": "Ontvang een e-mailbericht wanneer deze site offline gaat of weer online komt.",
"uptimeAlertDescriptionResource": "Ontvang een e-mailbericht wanneer deze bron offline gaat of weer online komt.",
"uptimeAlertNamePlaceholder": "Waarschuwingsnaam",
"uptimeAdditionalEmails": "Extra e-mails",
"uptimeCreateAlert": "Alarm aanmaken",
"uptimeAlertNoRecipients": "Geen ontvangers",
"uptimeAlertNoRecipientsDescription": "Voeg ten minste één gebruiker, rol of e-mail toe om te melden.",
"uptimeAlertCreated": "Alarm aangemaakt",
"uptimeAlertCreatedDescription": "U wordt op de hoogte gebracht wanneer dit van status verandert.",
"uptimeAlertCreateFailed": "Kon alarm niet aanmaken",
"webhookUrlLabel": "URL",
"webhookHeaderKeyPlaceholder": "Sleutel",
"webhookHeaderValuePlaceholder": "Waarde",
"alertLabel": "Waarschuwing",
"domainPickerWildcardSubdomainNotAllowed": "Wildcard-subdomeinen zijn niet toegestaan.",
"domainPickerWildcardCertWarning": "Wildcard-bronnen hebben mogelijk extra configuratie nodig om correct te werken.",
"domainPickerWildcardCertWarningLink": "Meer informatie",
"health": "Gezondheid",
"domainPendingErrorTitle": "Verificatieprobleem"
} }
+19 -613
View File
File diff suppressed because it is too large Load Diff
+20 -614
View File
File diff suppressed because it is too large Load Diff
+22 -616
View File
File diff suppressed because it is too large Load Diff
+20 -614
View File
File diff suppressed because it is too large Load Diff
+20 -614
View File
File diff suppressed because it is too large Load Diff
+2391 -2396
View File
File diff suppressed because it is too large Load Diff

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