Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 03ab01ca11 Bump next-intl from 4.8.3 to 4.9.1
Bumps [next-intl](https://github.com/amannn/next-intl) from 4.8.3 to 4.9.1.
- [Release notes](https://github.com/amannn/next-intl/releases)
- [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-intl/compare/v4.8.3...v4.9.1)

---
updated-dependencies:
- dependency-name: next-intl
  dependency-version: 4.9.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-22 04:34:00 +00:00
864 changed files with 27508 additions and 89003 deletions
-5
View File
@@ -1,5 +0,0 @@
---
alwaysApply: true
---
When adding submit buttons, don't change the text of the button during the loading state. Text should stay static and you should use the loading prop on the button.
-5
View File
@@ -1,5 +0,0 @@
---
alwaysApply: true
---
When creating UI for popup dialogs or modals, use the Credenza componennt. This component is mobile responsive and works on desktop and wraps the dialog component and sheet into one.
-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
-7
View File
@@ -1,7 +0,0 @@
---
alwaysApply: true
---
When writing TypeScript:
Prefer to use types instead of interfaces.
@@ -1,5 +0,0 @@
---
alwaysApply: true
---
When creating forms, use React form for validation and use Zod schemas.
-2
View File
@@ -34,5 +34,3 @@ build.ts
tsconfig.json tsconfig.json
Dockerfile* Dockerfile*
drizzle.config.ts drizzle.config.ts
allowedDevOrigins.json
scratch/
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [fosrl]
+2 -3
View File
@@ -14,13 +14,12 @@ body:
label: Environment label: Environment
description: Please fill out the relevant details below for your environment. description: Please fill out the relevant details below for your environment.
value: | value: |
- OS Type & Version: - OS Type & Version: (e.g., Ubuntu 22.04)
- Pangolin Version: - Pangolin Version:
- Edition (Community or Enterprise):
- Gerbil Version: - Gerbil Version:
- Traefik Version: - Traefik Version:
- Newt Version: - Newt Version:
- Client Version: - Olm Version: (if applicable)
validations: validations:
required: true required: true
+91 -35
View File
@@ -77,7 +77,7 @@ jobs:
fi fi
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with: with:
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -149,7 +149,7 @@ jobs:
fi fi
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with: with:
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -204,7 +204,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with: with:
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -264,7 +264,7 @@ jobs:
shell: bash shell: bash
- name: Install Go - name: Install Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with: with:
go-version: 1.25 go-version: 1.25
@@ -407,27 +407,35 @@ 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@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
with:
cosign-release: v3.0.6
- 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)
# Track failures
FAILED_TAGS=()
SUCCESSFUL_TAGS=()
# 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
@@ -455,47 +463,95 @@ 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}"
TAG_FAILED=false
for IMAGE_TAG in "${IMAGE_TAGS[@]}"; do # Wrap the entire tag processing in error handling
echo "Processing ${GHCR_IMAGE}:${IMAGE_TAG}" (
TAG_FAILED=false set -e
DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${IMAGE_TAG} | jq -r '.Digest')"
REF="${BASE_IMAGE}@${DIGEST}"
echo "Resolved digest: ${REF}"
( echo "==> cosign sign (keyless) --recursive ${REF}"
set -e cosign sign --recursive "${REF}"
DIGEST="$(skopeo inspect --retry-times 3 docker://${GHCR_IMAGE}:${IMAGE_TAG} | jq -r '.Digest')"
REF="${GHCR_IMAGE}@${DIGEST}"
echo "Resolved digest: ${REF}"
echo "==> cosign sign (keyless) --recursive ${REF}" echo "==> cosign sign (key) --recursive ${REF}"
cosign sign --recursive "${REF}" cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
) || TAG_FAILED=true
if [ "$TAG_FAILED" = "true" ]; then # Retry wrapper for verification to handle registry propagation delays
echo "⚠️ WARNING: Failed to sign ${GHCR_IMAGE}:${IMAGE_TAG}" retry_verify() {
FAILED_TAGS+=("${GHCR_IMAGE}:${IMAGE_TAG}") local cmd="$1"
else local attempts=6
echo "✓ Successfully signed ${GHCR_IMAGE}:${IMAGE_TAG}" local delay=5
SUCCESSFUL_TAGS+=("${GHCR_IMAGE}:${IMAGE_TAG}") local i=1
fi until eval "$cmd"; do
if [ $i -ge $attempts ]; then
echo "Verification failed after $attempts attempts"
return 1
fi
echo "Verification not yet available. Retry $i/$attempts after ${delay}s..."
sleep $delay
i=$((i+1))
delay=$((delay*2))
# Cap the delay to avoid very long waits
if [ $delay -gt 60 ]; then delay=60; fi
done
return 0
}
echo "==> cosign verify (public key) ${REF}"
if retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${REF}' -o text"; then
VERIFIED_INDEX=true
else
VERIFIED_INDEX=false
fi
echo "==> cosign verify (keyless policy) ${REF}"
if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${REF}' -o text"; then
VERIFIED_INDEX_KEYLESS=true
else
VERIFIED_INDEX_KEYLESS=false
fi
# Check if verification succeeded
if [ "${VERIFIED_INDEX}" != "true" ] && [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then
echo "⚠️ WARNING: Verification not available for ${BASE_IMAGE}:${IMAGE_TAG}"
echo "This may be due to registry propagation delays. Continuing anyway."
fi
) || TAG_FAILED=true
if [ "$TAG_FAILED" = "true" ]; then
echo "⚠️ WARNING: Failed to sign/verify ${BASE_IMAGE}:${IMAGE_TAG}"
FAILED_TAGS+=("${BASE_IMAGE}:${IMAGE_TAG}")
else
echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}"
SUCCESSFUL_TAGS+=("${BASE_IMAGE}:${IMAGE_TAG}")
fi
done
done done
# Report summary
echo "" echo ""
echo "==========================================" echo "=========================================="
echo "Sign Summary" echo "Sign and Verify Summary"
echo "==========================================" echo "=========================================="
echo "Successful: ${#SUCCESSFUL_TAGS[@]}" echo "Successful: ${#SUCCESSFUL_TAGS[@]}"
echo "Failed: ${#FAILED_TAGS[@]}" echo "Failed: ${#FAILED_TAGS[@]}"
echo ""
if [ ${#FAILED_TAGS[@]} -gt 0 ]; then if [ ${#FAILED_TAGS[@]} -gt 0 ]; then
echo "Failed tags:" echo "Failed tags:"
for tag in "${FAILED_TAGS[@]}"; do for tag in "${FAILED_TAGS[@]}"; do
echo " - $tag" echo " - $tag"
done done
echo "⚠️ WARNING: Some tags failed to sign, but continuing anyway" echo ""
echo "⚠️ WARNING: Some tags failed to sign/verify, but continuing anyway"
else else
echo "✓ All images signed successfully!" echo "✓ All images signed and verified successfully!"
fi fi
shell: bash shell: bash
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.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@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
- name: Input check - name: Input check
run: | run: |
+39
View File
@@ -0,0 +1,39 @@
name: Restart Runners
on:
schedule:
- cron: '0 0 */7 * *'
permissions:
id-token: write
contents: read
jobs:
ec2-maintenance-prod:
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
aws-region: ${{ secrets.AWS_REGION }}
- name: Verify AWS identity
run: aws sts get-caller-identity
- name: Start EC2 instance
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"
- name: Wait
run: sleep 600
- name: Stop EC2 instance
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"
+160
View File
@@ -0,0 +1,160 @@
name: SAAS Pipeline
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
permissions:
contents: read
packages: write # for GHCR push
id-token: write # for Cosign Keyless (OIDC) Signing
on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+-s.[0-9]+"
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
pre-run:
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
aws-region: ${{ secrets.AWS_REGION }}
- name: Verify AWS identity
run: aws sts get-caller-identity
- name: Start EC2 instances
run: |
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
echo "EC2 instances started"
release-arm:
name: Build and Release (ARM64)
runs-on: [self-hosted, linux, arm64, us-east-1]
needs: [pre-run]
if: >-
${{
needs.pre-run.result == 'success'
}}
# Job-level timeout to avoid runaway or stuck runs
timeout-minutes: 120
env:
# Target images
AWS_IMAGE: ${{ secrets.aws_account_id }}.dkr.ecr.us-east-1.amazonaws.com/${{ github.event.repository.name }}
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download MaxMind GeoLite2 databases
env:
MAXMIND_LICENSE_KEY: ${{ secrets.MAXMIND_LICENSE_KEY }}
run: |
echo "Downloading MaxMind GeoLite2 databases..."
# Download GeoLite2-Country
curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz" \
-o GeoLite2-Country.tar.gz
# Download GeoLite2-ASN
curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz" \
-o GeoLite2-ASN.tar.gz
# Extract the .mmdb files
tar -xzf GeoLite2-Country.tar.gz --strip-components=1 --wildcards '*.mmdb'
tar -xzf GeoLite2-ASN.tar.gz --strip-components=1 --wildcards '*.mmdb'
# Verify files exist
if [ ! -f "GeoLite2-Country.mmdb" ]; then
echo "ERROR: Failed to download GeoLite2-Country.mmdb"
exit 1
fi
if [ ! -f "GeoLite2-ASN.mmdb" ]; then
echo "ERROR: Failed to download GeoLite2-ASN.mmdb"
exit 1
fi
# Clean up tar files
rm -f GeoLite2-Country.tar.gz GeoLite2-ASN.tar.gz
echo "MaxMind databases downloaded successfully"
ls -lh GeoLite2-*.mmdb
- name: Monitor storage space
run: |
THRESHOLD=75
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
echo "Used space: $USED_SPACE%"
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
echo "Used space is below the threshold of 75% free. Running Docker system prune."
echo y | docker system prune -a
else
echo "Storage space is above the threshold. No action needed."
fi
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
aws-region: ${{ secrets.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
shell: bash
- name: Update version in package.json
run: |
TAG=${{ env.TAG }}
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
cat server/lib/consts.ts
shell: bash
- name: Build and push Docker images (Docker Hub - ARM64)
run: |
TAG=${{ env.TAG }}
make build-saas tag=$TAG
echo "Built & pushed ARM64 images to: ${{ env.AWS_IMAGE }}:${TAG}"
shell: bash
post-run:
needs: [pre-run, release-arm]
if: >-
${{
always() &&
needs.pre-run.result == 'success' &&
(needs.release-arm.result == 'success' || needs.release-arm.result == 'skipped' || needs.release-arm.result == 'failure')
}}
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
aws-region: ${{ secrets.AWS_REGION }}
- name: Verify AWS identity
run: aws sts get-caller-identity
- name: Stop EC2 instances
run: |
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
echo "EC2 instances stopped"
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with: with:
days-before-stale: 14 days-before-stale: 14
days-before-close: 14 days-before-close: 14
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Node - name: Install Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version: '24' node-version: '24'
+2 -4
View File
@@ -17,9 +17,9 @@ yarn-error.log*
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
*.db *.db
*.sqlite* *.sqlite
!Dockerfile.sqlite !Dockerfile.sqlite
*.sqlite3* *.sqlite3
*.log *.log
.machinelogs*.json .machinelogs*.json
*-audit.json *-audit.json
@@ -54,5 +54,3 @@ hydrateSaas.ts
CLAUDE.md CLAUDE.md
drizzle.config.ts drizzle.config.ts
server/setup/migrations.ts server/setup/migrations.ts
solo.yml
allowedDevOrigins.json
+5 -5
View File
@@ -41,7 +41,7 @@
</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 with NAT traversal, all with granular access controls.
## Installation ## Installation
@@ -53,9 +53,9 @@ Pangolin is an open-source, identity-based remote access platform built on WireG
## Deployment Options ## Deployment Options
- **Pangolin Cloud** - Fully managed service - no infrastructure required. - **Pangolin Cloud** Fully managed service - no infrastructure required.
- **Self-Host: Community Edition** - Free, open source, and licensed under AGPL-3. - **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: Enterprise Edition** Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses making less than \$100K USD gross annual revenue.
## Key Features ## Key Features
@@ -107,7 +107,7 @@ the docs to illustrate some basic ideas.
## Licensing ## Licensing
Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://pangolin.net/fcl). 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).
## Contributions ## Contributions
+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"
}
}
-28
View File
@@ -1,28 +0,0 @@
import { CommandModule } from "yargs";
import { db, certificates } from "@server/db";
type ClearCertificatesArgs = {};
export const clearCertificates: CommandModule<{}, ClearCertificatesArgs> = {
command: "clear-certificates",
describe: "Delete all entries from the certificates table",
builder: (yargs) => {
return yargs;
},
handler: async (argv: {}) => {
try {
console.log("Clearing all certificates from the database...");
const deleted = await db.delete(certificates).returning();
console.log(
`Deleted ${deleted.length} certificate(s) from the database`
);
process.exit(0);
} catch (error) {
console.error("Error:", error);
process.exit(1);
}
}
};
-60
View File
@@ -1,60 +0,0 @@
import { CommandModule } from "yargs";
import { db, users } from "@server/db";
import { eq } from "drizzle-orm";
/**
* Disable 2FA for a user by email address.
*/
type DisableUser2faArgs = {
email: string;
};
export const disableUser2fa: CommandModule<{}, DisableUser2faArgs> = {
command: "disable-user-2fa",
describe: "Disable 2FA for a user (sets twoFactorEnabled=false, clears secret)",
builder: (yargs) => {
return yargs.option("email", {
type: "string",
demandOption: true,
describe: "User email address"
});
},
handler: async (argv: { email: string }) => {
try {
const { email } = argv;
console.log(`Looking for user with email: ${email}`);
// Find the user by email
const [user] = await db
.select()
.from(users)
.where(eq(users.email, email))
.limit(1);
if (!user) {
console.error(`User with email '${email}' not found`);
process.exit(1);
}
if (!user.twoFactorEnabled) {
console.log(`2FA is already disabled for user '${email}'.`);
process.exit(0);
}
// Update user: disable 2FA and clear secret
await db.update(users)
.set({
twoFactorEnabled: false,
twoFactorSecret: null,
twoFactorSetupRequested: false
})
.where(eq(users.userId, user.userId));
console.log(`2FA disabled for user '${email}'.`);
process.exit(0);
} catch (error) {
console.error("Error disabling 2FA:", 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.`
); );
-85
View File
@@ -1,85 +0,0 @@
import { CommandModule } from "yargs";
import { db, users } from "@server/db";
import { eq } from "drizzle-orm";
type SetServerAdminArgs = {
email: string;
remove: boolean;
};
export const setServerAdmin: CommandModule<{}, SetServerAdminArgs> = {
command: "set-server-admin",
describe: "Add or remove server admin by email address",
builder: (yargs) => {
return yargs
.option("email", {
type: "string",
demandOption: true,
describe: "User email address"
})
.option("remove", {
type: "boolean",
default: false,
describe: "Remove server admin status from the user"
});
},
handler: async (argv: SetServerAdminArgs) => {
try {
const email = argv.email.trim().toLowerCase();
const [user] = await db
.select()
.from(users)
.where(eq(users.email, email))
.limit(1);
if (!user) {
console.error(`User with email '${email}' not found`);
process.exit(1);
}
if (argv.remove) {
if (!user.serverAdmin) {
console.log(`User '${email}' is not a server admin`);
process.exit(0);
}
const serverAdmins = await db
.select()
.from(users)
.where(eq(users.serverAdmin, true));
if (serverAdmins.length <= 1) {
console.error(
"Cannot remove server admin: at least one server admin must exist"
);
process.exit(1);
}
await db
.update(users)
.set({ serverAdmin: false })
.where(eq(users.userId, user.userId));
console.log(`Server admin status removed from user '${email}'`);
process.exit(0);
}
if (user.serverAdmin) {
console.log(`User '${email}' is already a server admin`);
process.exit(0);
}
await db
.update(users)
.set({ serverAdmin: true })
.where(eq(users.userId, user.userId));
console.log(`User '${email}' has been marked as a server admin`);
process.exit(0);
} catch (error) {
console.error("Error:", error);
process.exit(1);
}
}
};
-6
View File
@@ -9,9 +9,6 @@ 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"; import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
import { clearCertificates } from "./commands/clearCertificates";
import { disableUser2fa } from "./commands/disableUser2fa";
import { setServerAdmin } from "./commands/setServerAdmin";
yargs(hideBin(process.argv)) yargs(hideBin(process.argv))
.scriptName("pangctl") .scriptName("pangctl")
@@ -22,8 +19,5 @@ yargs(hideBin(process.argv))
.command(clearLicenseKeys) .command(clearLicenseKeys)
.command(deleteClient) .command(deleteClient)
.command(generateOrgCaKeys) .command(generateOrgCaKeys)
.command(clearCertificates)
.command(disableUser2fa)
.command(setServerAdmin)
.demandCommand() .demandCommand()
.help().argv; .help().argv;
+22 -15
View File
@@ -1,47 +1,54 @@
api: api:
insecure: true insecure: true
dashboard: true dashboard: true
providers: providers:
http: http:
endpoint: http://pangolin:3001/api/v1/traefik-config endpoint: "http://pangolin:3001/api/v1/traefik-config"
pollInterval: 5s pollInterval: "5s"
file: file:
filename: /etc/traefik/dynamic_config.yml filename: "/etc/traefik/dynamic_config.yml"
experimental: experimental:
plugins: plugins:
badger: badger:
moduleName: github.com/fosrl/badger moduleName: "github.com/fosrl/badger"
version: v1.4.1 version: "{{.BadgerVersion}}"
log: log:
level: INFO level: "INFO"
format: common format: "common"
maxSize: 100 maxSize: 100
maxBackups: 3 maxBackups: 3
maxAge: 3 maxAge: 3
compress: true compress: true
certificatesResolvers: certificatesResolvers:
letsencrypt: letsencrypt:
acme: acme:
httpChallenge: httpChallenge:
entryPoint: web entryPoint: web
email: '{{.LetsEncryptEmail}}' email: "{{.LetsEncryptEmail}}"
storage: /letsencrypt/acme.json storage: "/letsencrypt/acme.json"
caServer: https://acme-v02.api.letsencrypt.org/directory caServer: "https://acme-v02.api.letsencrypt.org/directory"
entryPoints: entryPoints:
web: web:
address: ':80' address: ":80"
websecure: websecure:
address: ':443' address: ":443"
transport: transport:
respondingTimeouts: respondingTimeouts:
readTimeout: 30m readTimeout: "30m"
http: http:
tls: tls:
certResolver: letsencrypt certResolver: "letsencrypt"
encodedCharacters: encodedCharacters:
allowEncodedSlash: true allowEncodedSlash: true
allowEncodedQuestionMark: true allowEncodedQuestionMark: true
serversTransport: serversTransport:
insecureSkipVerify: true insecureSkipVerify: true
ping: ping:
entryPoint: web entryPoint: "web"
-12
View File
@@ -1,12 +0,0 @@
services:
mailer:
image: axllent/mailpit
ports:
- 8025:8025
- 1025:1025
volumes:
- mailpit-storage:/data
environment:
- MP_DATABASE=/data/mailpit.db
volumes:
mailpit-storage:
+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
+1 -1
View File
@@ -1,4 +1,4 @@
import { APP_PATH } from "./server/lib/consts"; import { APP_PATH } from "@server/lib/consts";
import { defineConfig } from "drizzle-kit"; import { defineConfig } from "drizzle-kit";
import path from "path"; import path from "path";
+1 -5
View File
@@ -22,8 +22,7 @@ server:
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"] methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
allowed_headers: ["X-CSRF-Token", "Content-Type"] allowed_headers: ["X-CSRF-Token", "Content-Type"]
credentials: false credentials: false
{{if .EnableMaxMind}}maxmind_db_path: "./config/GeoLite2-Country.mmdb"{{end}} {{if .EnableGeoblocking}}maxmind_db_path: "./config/GeoLite2-Country.mmdb"{{end}}
{{if .EnableMaxMind}}maxmind_asn_path: "./config/GeoLite2-ASN.mmdb"{{end}}
{{if .EnableEmail}} {{if .EnableEmail}}
email: email:
smtp_host: "{{.EmailSMTPHost}}" smtp_host: "{{.EmailSMTPHost}}"
@@ -37,6 +36,3 @@ flags:
disable_signup_without_invite: true disable_signup_without_invite: true
disable_user_create_org: false disable_user_create_org: false
allow_raw_resources: true allow_raw_resources: true
{{if .IsPostgreSQL}}postgres:
connection_string: postgresql://pangolin:{{.IsPostgreSQLPass}}@postgres:5432/pangolin{{end}}
+11 -60
View File
@@ -1,23 +1,15 @@
name: pangolin name: pangolin
services: services:
pangolin: pangolin:
image: docker.io/fosrl/pangolin:{{if .IsEnterprise}}ee-{{end}}{{if .IsPostgreSQL}}postgresql-{{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: deploy:
resources: resources:
limits: limits:
memory: 2g memory: 1g
reservations: reservations:
memory: 512m memory: 256m
{{if or .IsPostgreSQL .IsRedis}}depends_on:
{{if .IsPostgreSQL}}postgres:
condition: service_healthy{{end}}
{{if .IsRedis}}redis:
condition: service_healthy{{end}}
networks:
- default
- backend{{end}}
volumes: volumes:
- ./config:/app/config - ./config:/app/config
healthcheck: healthcheck:
@@ -25,8 +17,8 @@ services:
interval: "10s" interval: "10s"
timeout: "10s" timeout: "10s"
retries: 15 retries: 15
{{if .InstallGerbil}}
{{if .InstallGerbil}}gerbil: gerbil:
image: docker.io/fosrl/gerbil:{{.GerbilVersion}} image: docker.io/fosrl/gerbil:{{.GerbilVersion}}
container_name: gerbil container_name: gerbil
restart: unless-stopped restart: unless-stopped
@@ -47,16 +39,17 @@ services:
- 21820:21820/udp - 21820:21820/udp
- 443:443 - 443:443
- 443:443/udp # For http3 QUIC if desired - 443:443/udp # For http3 QUIC if desired
- 80:80{{end}} - 80:80
{{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{{end}} - 80:80
{{end}}
depends_on: depends_on:
pangolin: pangolin:
condition: service_healthy condition: service_healthy
@@ -67,50 +60,8 @@ services:
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates - ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs - ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
{{if .IsPostgreSQL}}postgres:
image: postgres:18
container_name: postgres
restart: unless-stopped
environment:
POSTGRES_USER: pangolin
POSTGRES_PASSWORD: {{.IsPostgreSQLPass}}
POSTGRES_DB: pangolin
volumes:
- ./postgres18:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pangolin"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend{{end}}
{{if .IsRedis}}redis:
image: redis:8-trixie
container_name: redis
restart: unless-stopped
command: >
redis-server
--save 3600 1000
--appendonly yes
--requirepass {{.IsRedisPass}}
volumes:
- ./redis8:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "{{.IsRedisPass}}", "ping"]
interval: 10s
timeout: 3s
retries: 3
start_period: 10s
networks:
- backend{{end}}
networks: networks:
default: default:
driver: bridge driver: bridge
name: pangolin_frontend name: pangolin
{{if .EnableIPv6}} enable_ipv6: true{{end}} {{if .EnableIPv6}} enable_ipv6: true{{end}}
{{if or .IsPostgreSQL .IsRedis}} backend:
driver: bridge
name: pangolin_backend
internal: true{{end}}
-4
View File
@@ -1,4 +0,0 @@
{{if .IsRedis}}redis:
host: "redis"
port: 6379
password: "{{.IsRedisPass}}"{{end}}
+1 -70
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)
@@ -41,8 +40,6 @@ func installCrowdsec(config Config, installDir string) error {
os.Exit(1) 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)
os.Exit(1) os.Exit(1)
@@ -211,69 +208,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)
}
+2 -2
View File
@@ -5,7 +5,7 @@ go 1.25.0
require ( require (
github.com/charmbracelet/huh v1.0.0 github.com/charmbracelet/huh v1.0.0
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
golang.org/x/term v0.43.0 golang.org/x/term v0.42.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@@ -33,6 +33,6 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.15.0 // indirect golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.44.0 // indirect golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.23.0 // indirect golang.org/x/text v0.23.0 // indirect
) )
+4 -4
View File
@@ -69,10 +69,10 @@ golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= 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 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 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=
+21 -58
View File
@@ -4,7 +4,6 @@ import (
"crypto/rand" "crypto/rand"
"embed" "embed"
"encoding/base64" "encoding/base64"
"flag"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
@@ -54,13 +53,9 @@ type Config struct {
InstallGerbil bool InstallGerbil bool
TraefikBouncerKey string TraefikBouncerKey string
DoCrowdsecInstall bool DoCrowdsecInstall bool
EnableMaxMind bool EnableGeoblocking bool
Secret string Secret string
IsEnterprise bool IsEnterprise bool
IsPostgreSQL bool
IsPostgreSQLPass string
IsRedis bool
IsRedisPass string
} }
type SupportedContainer string type SupportedContainer string
@@ -71,14 +66,8 @@ const (
Undefined SupportedContainer = "undefined" Undefined SupportedContainer = "undefined"
) )
var redisFlag *bool
func main() { func main() {
crowdsecFlag := flag.Bool("crowdsec", false, "Enable the CrowdSec installation prompt")
redisFlag = flag.Bool("redis", false, "Install Redis as cacheing solution. Required for HA. Not required for the Enterprise version.")
flag.Parse()
// print a banner about prerequisites - opening port 80, 443, 51820, and 21820 on the VPS and firewall and pointing your domain to the VPS IP with a records. Docs are at http://localhost:3000/Getting%20Started/dns-networking // print a banner about prerequisites - opening port 80, 443, 51820, and 21820 on the VPS and firewall and pointing your domain to the VPS IP with a records. Docs are at http://localhost:3000/Getting%20Started/dns-networking
fmt.Println("Welcome to the Pangolin installer!") fmt.Println("Welcome to the Pangolin installer!")
@@ -130,11 +119,11 @@ func main() {
fmt.Println("\nConfiguration files created successfully!") fmt.Println("\nConfiguration files created successfully!")
// Download MaxMind Country / ASN database if requested // Download MaxMind database if requested
if config.EnableMaxMind { if config.EnableGeoblocking {
fmt.Println("\n=== Downloading MaxMind Country and ASN Databases ===") fmt.Println("\n=== Downloading MaxMind Database ===")
if err := downloadMaxMindDatabase(); err != nil { if err := downloadMaxMindDatabase(); err != nil {
fmt.Printf("Error downloading MaxMind databases: %v\n", err) fmt.Printf("Error downloading MaxMind database: %v\n", err)
fmt.Println("You can download it manually later if needed.") fmt.Println("You can download it manually later if needed.")
} }
} }
@@ -195,15 +184,15 @@ 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 databases (Country and ASN) to the latest version?", false) { if readBool("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.")
} }
} }
} else { } else {
fmt.Println("MaxMind GeoLite2 Country and ASN databases not found.") fmt.Println("MaxMind GeoLite2 Country database not found.")
if readBool("Would you like to download the MaxMind GeoLite2 databases for blocking functionality?", false) { if readBool("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.")
@@ -211,15 +200,13 @@ func main() {
// Now you need to update your config file accordingly to enable geoblocking // Now you need to update your config file accordingly to enable geoblocking
fmt.Print("Please remember to update your config/config.yml file to enable geoblocking! \n\n") fmt.Print("Please remember to update your config/config.yml file to enable geoblocking! \n\n")
// add maxmind_db_path: "./config/GeoLite2-Country.mmdb" under server // add maxmind_db_path: "./config/GeoLite2-Country.mmdb" under server
// add maxmind_asn_path: "./config/GeoLite2-ASN.mmdb" under server fmt.Println("Add the following line under the 'server' section:")
fmt.Println("Add the following lines under the 'server' section:")
fmt.Println(" maxmind_db_path: \"./config/GeoLite2-Country.mmdb\"") fmt.Println(" maxmind_db_path: \"./config/GeoLite2-Country.mmdb\"")
fmt.Println(" maxmind_asn_path: \"./config/GeoLite2-ASN.mmdb\"")
} }
} }
} }
if *crowdsecFlag && !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("Would you like to install CrowdSec?", false) {
@@ -272,7 +259,7 @@ func main() {
} }
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
@@ -493,17 +480,6 @@ func collectUserInput() Config {
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("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.")
if config.IsEnterprise {
if *redisFlag {
config.IsRedis = true
config.IsRedisPass = readPassword("Enter a unique password for the Redis service.")
}
}
config.IsPostgreSQL = readBool("Do you want to use PostgreSQL (not recommended for most users)?", false)
if config.IsPostgreSQL {
config.IsPostgreSQLPass = readPassword("Enter a unique password for the PostgreSQL pangolin user.")
}
config.BaseDomain = readString("Enter your base domain (no subdomain e.g. example.com)", "") config.BaseDomain = readString("Enter your base domain (no subdomain e.g. example.com)", "")
@@ -547,7 +523,7 @@ 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("Is your server IPv6 capable?", true)
config.EnableMaxMind = readBool("Do you want to download the MaxMind GeoLite2 Country and ASN databases for blocking functionality?", true) config.EnableGeoblocking = readBool("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")
@@ -800,42 +776,29 @@ func checkPortsAvailable(port int) error {
} }
func downloadMaxMindDatabase() error { func downloadMaxMindDatabase() error {
fmt.Println("Downloading MaxMind GeoLite2 Country and ASN databases...") fmt.Println("Downloading MaxMind GeoLite2 Country database...")
// Download the GeoLite2 Country databases // Download the GeoLite2 Country database
if err := run("curl", "-L", "-o", "GeoLite2-Country.tar.gz", if err := run("curl", "-L", "-o", "GeoLite2-Country.tar.gz",
"https://github.com/GitSquared/node-geolite2-redist/raw/refs/heads/master/redist/GeoLite2-Country.tar.gz"); err != nil { "https://github.com/GitSquared/node-geolite2-redist/raw/refs/heads/master/redist/GeoLite2-Country.tar.gz"); err != nil {
return fmt.Errorf("failed to download GeoLite2 Country database: %v", err) return fmt.Errorf("failed to download GeoLite2 database: %v", err)
}
if err := run("curl", "-L", "-o", "GeoLite2-ASN.tar.gz",
"https://github.com/GitSquared/node-geolite2-redist/raw/refs/heads/master/redist/GeoLite2-ASN.tar.gz"); err != nil {
return fmt.Errorf("failed to download GeoLite2 ASN database: %v", err)
} }
// Extract the Country database // Extract the database
if err := run("tar", "-xzf", "GeoLite2-Country.tar.gz"); err != nil { if err := run("tar", "-xzf", "GeoLite2-Country.tar.gz"); err != nil {
return fmt.Errorf("failed to extract GeoLite2 Country database: %v", err) return fmt.Errorf("failed to extract GeoLite2 database: %v", err)
}
if err := run("tar", "-xzf", "GeoLite2-ASN.tar.gz"); err != nil {
return fmt.Errorf("failed to extract GeoLite2 ASN database: %v", err)
} }
// Find the .mmdb file and move it to the config directory // Find the .mmdb file and move it to the config directory
if err := run("bash", "-c", "mv GeoLite2-Country_*/GeoLite2-Country.mmdb config/"); err != nil { if err := run("bash", "-c", "mv GeoLite2-Country_*/GeoLite2-Country.mmdb config/"); err != nil {
return fmt.Errorf("failed to move GeoLite2 Country database to config directory: %v", err) return fmt.Errorf("failed to move GeoLite2 database to config directory: %v", err)
}
if err := run("bash", "-c", "mv GeoLite2-ASN_*/GeoLite2-ASN.mmdb config/"); err != nil {
return fmt.Errorf("failed to move GeoLite2 ASN database to config directory: %v", err)
} }
// Clean up the downloaded files // Clean up the downloaded files
if err := run("sh", "-c", "rm -rf GeoLite2-Country.tar.gz GeoLite2-Country_*"); err != nil { if err := run("rm", "-rf", "GeoLite2-Country.tar.gz", "GeoLite2-Country_*"); err != nil {
fmt.Printf("Warning: failed to clean up temporary country files: %v\n", err) fmt.Printf("Warning: failed to clean up temporary files: %v\n", err)
}
if err := run("sh", "-c", "rm -rf GeoLite2-ASN.tar.gz GeoLite2-ASN_*"); err != nil {
fmt.Printf("Warning: failed to clean up temporary ASN files: %v\n", err)
} }
fmt.Println("MaxMind GeoLite2 Country and ASN database downloaded successfully!") fmt.Println("MaxMind GeoLite2 Country database downloaded successfully!")
return nil return nil
} }
+2 -2
View File
@@ -96,7 +96,7 @@ def process_directory(root_dir):
if has_correct_header: if has_correct_header:
print(f"Header up-to-date: {file_path}") print(f"Header up-to-date: {file_path}")
else: else:
# Either no header exists or the header is outdated - write # Either no header exists or the header is outdated write
# the correct one. # the correct one.
action = "Replaced header in" if has_any_header else "Added header to" action = "Replaced header in" if has_any_header else "Added header to"
new_content = HEADER_NORMALIZED + '\n\n' + body new_content = HEADER_NORMALIZED + '\n\n' + body
@@ -106,7 +106,7 @@ def process_directory(root_dir):
files_modified += 1 files_modified += 1
else: else:
if has_any_header: if has_any_header:
# Remove the header - it shouldn't be here. # Remove the header it shouldn't be here.
with open(file_path, 'w', encoding='utf-8') as f: with open(file_path, 'w', encoding='utf-8') as f:
f.write(body) f.write(body)
print(f"Removed header from: {file_path}") print(f"Removed header from: {file_path}")
+51 -730
View File
File diff suppressed because it is too large Load Diff
+49 -728
View File
File diff suppressed because it is too large Load Diff
+78 -757
View File
File diff suppressed because it is too large Load Diff
+76 -755
View File
File diff suppressed because it is too large Load Diff
+52 -731
View File
File diff suppressed because it is too large Load Diff
+46 -725
View File
File diff suppressed because it is too large Load Diff
+51 -730
View File
File diff suppressed because it is too large Load Diff
+51 -730
View File
File diff suppressed because it is too large Load Diff
+50 -729
View File
File diff suppressed because it is too large Load Diff
+49 -728
View File
File diff suppressed because it is too large Load Diff
+49 -728
View File
File diff suppressed because it is too large Load Diff
+52 -731
View File
File diff suppressed because it is too large Load Diff
+48 -727
View File
File diff suppressed because it is too large Load Diff
+50 -729
View File
File diff suppressed because it is too large Load Diff
+49 -728
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -152,8 +152,8 @@
"shareErrorSelectResource": "請選擇一個資源", "shareErrorSelectResource": "請選擇一個資源",
"proxyResourceTitle": "管理公開資源", "proxyResourceTitle": "管理公開資源",
"proxyResourceDescription": "建立和管理可透過網頁瀏覽器公開存取的資源", "proxyResourceDescription": "建立和管理可透過網頁瀏覽器公開存取的資源",
"publicResourcesBannerTitle": "基於網頁的公開存取", "proxyResourcesBannerTitle": "基於網頁的公開存取",
"publicResourcesBannerDescription": "公開資源是任何人都可以透過網頁瀏覽器存取的 HTTPS 或 TCP/UDP 代理。與私有資源不同,它們不需要客戶端軟體,並且可以包含基於身份和情境感知的存取策略。", "proxyResourcesBannerDescription": "公開資源是任何人都可以透過網頁瀏覽器存取的 HTTPS 或 TCP/UDP 代理。與私有資源不同,它們不需要客戶端軟體,並且可以包含基於身份和情境感知的存取策略。",
"clientResourceTitle": "管理私有資源", "clientResourceTitle": "管理私有資源",
"clientResourceDescription": "建立和管理只能透過已連接的客戶端存取的資源", "clientResourceDescription": "建立和管理只能透過已連接的客戶端存取的資源",
"privateResourcesBannerTitle": "零信任私有存取", "privateResourcesBannerTitle": "零信任私有存取",
@@ -489,7 +489,7 @@
"createdAt": "創建於", "createdAt": "創建於",
"proxyErrorInvalidHeader": "無效的自訂主機 Header。使用域名格式,或將空保存為取消自訂 Header。", "proxyErrorInvalidHeader": "無效的自訂主機 Header。使用域名格式,或將空保存為取消自訂 Header。",
"proxyErrorTls": "無效的 TLS 伺服器名稱。使用域名格式,或保存空以刪除 TLS 伺服器名稱。", "proxyErrorTls": "無效的 TLS 伺服器名稱。使用域名格式,或保存空以刪除 TLS 伺服器名稱。",
"proxyEnableSSL": "啟用 TLS", "proxyEnableSSL": "啟用 SSL",
"proxyEnableSSLDescription": "啟用 SSL/TLS 加密以確保您目標的 HTTPS 連接。", "proxyEnableSSLDescription": "啟用 SSL/TLS 加密以確保您目標的 HTTPS 連接。",
"target": "目標", "target": "目標",
"configureTarget": "配置目標", "configureTarget": "配置目標",
@@ -1763,7 +1763,7 @@
"description": "更可靠、維護成本更低的自架 Pangolin 伺服器,並附帶額外的附加功能", "description": "更可靠、維護成本更低的自架 Pangolin 伺服器,並附帶額外的附加功能",
"introTitle": "託管式自架 Pangolin", "introTitle": "託管式自架 Pangolin",
"introDescription": "這是一種部署選擇,為那些希望簡潔和額外可靠的人設計,同時仍然保持他們的數據的私密性和自我託管性。", "introDescription": "這是一種部署選擇,為那些希望簡潔和額外可靠的人設計,同時仍然保持他們的數據的私密性和自我託管性。",
"introDetail": "通過此選項,您仍然運行您自己的 Pangolin 節點 - - 您的隧道、TLS 終止,並且流量在您的伺服器上保持所有狀態。 不同之處在於,管理和監測是通過我們的雲層儀錶板進行的,該儀錶板開啟了一些好處:", "introDetail": "通過此選項,您仍然運行您自己的 Pangolin 節點 — — 您的隧道、SSL 終止,並且流量在您的伺服器上保持所有狀態。 不同之處在於,管理和監測是通過我們的雲層儀錶板進行的,該儀錶板開啟了一些好處:",
"benefitSimplerOperations": { "benefitSimplerOperations": {
"title": "簡單的操作", "title": "簡單的操作",
"description": "無需運行您自己的郵件伺服器或設置複雜的警報。您將從方框中獲得健康檢查和下限提醒。" "description": "無需運行您自己的郵件伺服器或設置複雜的警報。您將從方框中獲得健康檢查和下限提醒。"
@@ -2035,7 +2035,7 @@
"alerts": { "alerts": {
"commercialUseDisclosure": { "commercialUseDisclosure": {
"title": "使用情況披露", "title": "使用情況披露",
"description": "選擇能準確反映您預定用途的許可等級。 個人許可證允許對個人、非商業性或小型商業活動免費使用軟體,年收入毛額不到 100,000 美元。 超出這些限度的任何用途,包括在企業、組織內的用途。 或其他創收環境--需要有效的企業許可證和支付適用的許可證費用。 所有用戶,不論是個人還是企業,都必須遵守寄養商業許可證條款。" "description": "選擇能準確反映您預定用途的許可等級。 個人許可證允許對個人、非商業性或小型商業活動免費使用軟體,年收入毛額不到 100,000 美元。 超出這些限度的任何用途,包括在企業、組織內的用途。 或其他創收環境——需要有效的企業許可證和支付適用的許可證費用。 所有用戶,不論是個人還是企業,都必須遵守寄養商業許可證條款。"
}, },
"trialPeriodInformation": { "trialPeriodInformation": {
"title": "試用期資訊", "title": "試用期資訊",
+7 -32
View File
@@ -1,42 +1,17 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin"; import createNextIntlPlugin from "next-intl/plugin";
import fs from "fs";
import path from "path";
const withNextIntl = createNextIntlPlugin(); const withNextIntl = createNextIntlPlugin();
// read allowedDevOrigins.json if it exists
let allowedDevOrigins: string[] = [];
const allowedDevOriginsPath = path.join(
process.cwd(),
"allowedDevOrigins.json"
);
if (fs.existsSync(allowedDevOriginsPath)) {
try {
const data = fs.readFileSync(allowedDevOriginsPath, "utf-8");
allowedDevOrigins = JSON.parse(data);
} catch {}
}
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
reactStrictMode: false, reactStrictMode: false,
reactCompiler: true, eslint: {
transpilePackages: ["@novnc/novnc"], ignoreDuringBuilds: true
output: "standalone", },
allowedDevOrigins, experimental: {
async redirects() { reactCompiler: true
return [ },
{ output: "standalone"
source: "/:orgId/settings/resources/proxy/:path*",
destination: "/:orgId/settings/resources/public/:path*",
permanent: true
},
{
source: "/:orgId/settings/resources/client/:path*",
destination: "/:orgId/settings/resources/private/:path*",
permanent: true
}
];
}
}; };
export default withNextIntl(nextConfig); export default withNextIntl(nextConfig);
+3580 -2358
View File
File diff suppressed because it is too large Load Diff
+58 -64
View File
@@ -32,15 +32,13 @@
"format": "prettier --write ." "format": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@asteasolutions/zod-to-openapi": "8.5.0", "@asteasolutions/zod-to-openapi": "8.4.1",
"@devolutions/iron-remote-desktop": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz", "@aws-sdk/client-s3": "3.1011.0",
"@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.0.tgz", "@faker-js/faker": "10.3.0",
"@aws-sdk/client-s3": "3.1056.0", "@headlessui/react": "2.2.9",
"@headlessui/react": "2.2.10", "@hookform/resolvers": "5.2.2",
"@hookform/resolvers": "5.4.0",
"@monaco-editor/react": "4.7.0", "@monaco-editor/react": "4.7.0",
"@node-rs/argon2": "2.0.2", "@node-rs/argon2": "2.0.2",
"@novnc/novnc": "^1.7.0",
"@oslojs/crypto": "1.0.1", "@oslojs/crypto": "1.0.1",
"@oslojs/encoding": "1.1.0", "@oslojs/encoding": "1.1.0",
"@radix-ui/react-avatar": "1.1.11", "@radix-ui/react-avatar": "1.1.11",
@@ -61,20 +59,16 @@
"@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toast": "1.2.15",
"@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-tooltip": "1.2.8",
"@react-email/body": "0.3.0", "@react-email/components": "1.0.8",
"@react-email/components": "1.0.12", "@react-email/render": "2.0.4",
"@react-email/render": "2.0.8", "@react-email/tailwind": "2.0.5",
"@react-email/tailwind": "2.0.7",
"@simplewebauthn/browser": "13.3.0", "@simplewebauthn/browser": "13.3.0",
"@simplewebauthn/server": "13.3.1", "@simplewebauthn/server": "13.3.0",
"@tailwindcss/forms": "0.5.11", "@tailwindcss/forms": "0.5.11",
"@tanstack/react-query": "5.100.14", "@tanstack/react-query": "5.90.21",
"@tanstack/react-table": "8.21.3", "@tanstack/react-table": "8.21.3",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"arctic": "3.7.0", "arctic": "3.7.0",
"axios": "1.16.1", "axios": "1.15.0",
"better-sqlite3": "11.9.1", "better-sqlite3": "11.9.1",
"canvas-confetti": "1.9.4", "canvas-confetti": "1.9.4",
"class-variance-authority": "0.7.1", "class-variance-authority": "0.7.1",
@@ -84,78 +78,79 @@
"cors": "2.8.6", "cors": "2.8.6",
"crypto-js": "4.2.0", "crypto-js": "4.2.0",
"d3": "7.9.0", "d3": "7.9.0",
"drizzle-orm": "0.45.2", "drizzle-orm": "0.45.1",
"express": "5.2.1", "express": "5.2.1",
"express-rate-limit": "8.5.2", "express-rate-limit": "8.3.0",
"glob": "13.0.6", "glob": "13.0.6",
"helmet": "8.2.0", "helmet": "8.1.0",
"http-errors": "2.0.1", "http-errors": "2.0.1",
"input-otp": "1.4.2", "input-otp": "1.4.2",
"ioredis": "5.11.0", "ioredis": "5.10.0",
"jmespath": "0.16.0", "jmespath": "0.16.0",
"js-yaml": "4.1.1", "js-yaml": "4.1.1",
"jsonwebtoken": "9.0.3", "jsonwebtoken": "9.0.3",
"lucide-react": "1.17.0", "lucide-react": "0.577.0",
"maxmind": "5.0.6", "maxmind": "5.0.5",
"moment": "2.30.1", "moment": "2.30.1",
"next": "16.2.6", "next": "15.5.15",
"next-intl": "4.13.0", "next-intl": "4.9.1",
"next-themes": "0.4.6", "next-themes": "0.4.6",
"nextjs-toploader": "3.9.17", "nextjs-toploader": "3.9.17",
"node-cache": "5.1.2", "node-cache": "5.1.2",
"nodemailer": "8.0.9", "nodemailer": "8.0.5",
"oslo": "1.2.1", "oslo": "1.2.1",
"pg": "8.21.0", "pg": "8.20.0",
"posthog-node": "5.35.6", "posthog-node": "5.28.0",
"qrcode.react": "4.2.0", "qrcode.react": "4.2.0",
"react": "19.2.6", "react": "19.2.4",
"react-day-picker": "9.14.0", "react-day-picker": "9.14.0",
"react-dom": "19.2.6", "react-dom": "19.2.4",
"react-easy-sort": "1.8.0", "react-easy-sort": "1.8.0",
"react-hook-form": "7.76.1", "react-hook-form": "7.71.2",
"react-icons": "5.6.0", "react-icons": "5.6.0",
"recharts": "3.8.1", "recharts": "2.15.4",
"reodotdev": "1.1.0", "reodotdev": "1.1.0",
"semver": "7.8.1", "resend": "6.9.2",
"semver": "7.7.4",
"sshpk": "1.18.0", "sshpk": "1.18.0",
"stripe": "22.2.0", "stripe": "20.4.1",
"swagger-ui-express": "5.0.1", "swagger-ui-express": "5.0.1",
"tailwind-merge": "3.6.0", "tailwind-merge": "3.5.0",
"topojson-client": "3.1.0", "topojson-client": "3.1.0",
"tw-animate-css": "1.4.0", "tw-animate-css": "1.4.0",
"use-debounce": "10.1.1", "use-debounce": "10.1.0",
"uuid": "14.0.0", "uuid": "13.0.0",
"vaul": "1.1.2", "vaul": "1.1.2",
"visionscarto-world-atlas": "1.0.0", "visionscarto-world-atlas": "1.0.0",
"winston": "3.19.0", "winston": "3.19.0",
"winston-daily-rotate-file": "5.0.0", "winston-daily-rotate-file": "5.0.0",
"ws": "8.21.0", "ws": "8.19.0",
"yaml": "2.9.0", "yaml": "2.8.3",
"yargs": "18.0.0", "yargs": "18.0.0",
"zod": "4.4.3", "zod": "4.3.6",
"zod-validation-error": "5.0.0" "zod-validation-error": "5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@dotenvx/dotenvx": "1.69.1", "@dotenvx/dotenvx": "1.54.1",
"@esbuild-plugins/tsconfig-paths": "0.1.2", "@esbuild-plugins/tsconfig-paths": "0.1.2",
"@react-email/ui": "^6.5.0", "@react-email/preview-server": "5.2.10",
"@tailwindcss/postcss": "4.3.0", "@tailwindcss/postcss": "4.2.2",
"@tanstack/react-query-devtools": "5.100.14", "@tanstack/react-query-devtools": "5.91.3",
"@types/better-sqlite3": "7.6.13", "@types/better-sqlite3": "7.6.13",
"@types/cookie-parser": "1.4.10", "@types/cookie-parser": "1.4.10",
"@types/cors": "2.8.19", "@types/cors": "2.8.19",
"@types/crypto-js": "4.2.2", "@types/crypto-js": "4.2.2",
"@types/d3": "7.4.3", "@types/d3": "7.4.3",
"@types/express": "5.0.6", "@types/express": "5.0.6",
"@types/express-session": "1.19.0", "@types/express-session": "1.18.2",
"@types/jmespath": "0.15.2", "@types/jmespath": "0.15.2",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/jsonwebtoken": "9.0.10", "@types/jsonwebtoken": "9.0.10",
"@types/node": "25.9.1", "@types/node": "25.3.5",
"@types/nodemailer": "8.0.0", "@types/nodemailer": "7.0.11",
"@types/nprogress": "0.2.3", "@types/nprogress": "0.2.3",
"@types/pg": "8.20.0", "@types/pg": "8.18.0",
"@types/react": "19.2.15", "@types/react": "19.2.14",
"@types/react-dom": "19.2.3", "@types/react-dom": "19.2.3",
"@types/semver": "7.7.1", "@types/semver": "7.7.1",
"@types/sshpk": "1.17.4", "@types/sshpk": "1.17.4",
@@ -165,22 +160,21 @@
"@types/yargs": "17.0.35", "@types/yargs": "17.0.35",
"babel-plugin-react-compiler": "1.0.0", "babel-plugin-react-compiler": "1.0.0",
"drizzle-kit": "0.31.10", "drizzle-kit": "0.31.10",
"esbuild": "0.28.0", "esbuild": "0.27.4",
"esbuild-node-externals": "1.22.0", "esbuild-node-externals": "1.20.1",
"eslint": "10.4.0", "eslint": "10.0.3",
"eslint-config-next": "16.2.6", "eslint-config-next": "16.1.7",
"postcss": "8.5.15", "postcss": "8.5.8",
"prettier": "3.8.3", "prettier": "3.8.1",
"react-email": "6.5.0", "react-email": "5.2.10",
"tailwindcss": "4.3.0", "tailwindcss": "4.2.2",
"tsc-alias": "1.8.17", "tsc-alias": "1.8.16",
"tsx": "4.22.3", "tsx": "4.21.0",
"typescript": "6.0.3", "typescript": "5.9.3",
"typescript-eslint": "8.60.0" "typescript-eslint": "8.56.1"
}, },
"overrides": { "overrides": {
"esbuild": "0.28.0", "esbuild": "0.27.4",
"dompurify": "3.4.0", "dompurify": "3.3.2"
"postcss": "8.5.15"
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 KiB

After

Width:  |  Height:  |  Size: 588 KiB

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