mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-29 14:20:44 +00:00
Compare commits
255 Commits
1.14.0-rc.
...
msg-delive
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05748bf8ff | ||
|
|
f8c98bf6bf | ||
|
|
a1ea3f74b3 | ||
|
|
06aaa7c680 | ||
|
|
65e8bfc93e | ||
|
|
ff5e12655f | ||
|
|
6d90d734f4 | ||
|
|
6c8757f230 | ||
|
|
40e37b1798 | ||
|
|
bd87585396 | ||
|
|
e9e935d6c4 | ||
|
|
2f2c2b4222 | ||
|
|
9749a272ec | ||
|
|
b76a50238e | ||
|
|
a4f3963a5a | ||
|
|
d52bd65d21 | ||
|
|
fb51f42f35 | ||
|
|
c910a715bd | ||
|
|
9040f9b82a | ||
|
|
fc0ec0d754 | ||
|
|
b3569174b6 | ||
|
|
0cae624995 | ||
|
|
cbf184342b | ||
|
|
ce123a7f1a | ||
|
|
0c5daa7173 | ||
|
|
bc20a34a49 | ||
|
|
d5b6a426a9 | ||
|
|
4c78e93143 | ||
|
|
5f184e9e5e | ||
|
|
2201b0395d | ||
|
|
51818044b1 | ||
|
|
30943010e6 | ||
|
|
dd5ca10226 | ||
|
|
a56b058858 | ||
|
|
eade72e2c6 | ||
|
|
e9bc9747b8 | ||
|
|
eb0cdda0f9 | ||
|
|
552adf3200 | ||
|
|
eba25fcc4d | ||
|
|
673cd0fcd1 | ||
|
|
b941b5571f | ||
|
|
ca026b41c0 | ||
|
|
29a683a815 | ||
|
|
69dbd20ea5 | ||
|
|
427ee026ac | ||
|
|
0a537c6830 | ||
|
|
89682a2ee4 | ||
|
|
78b00a18cc | ||
|
|
192702daf9 | ||
|
|
fcee735578 | ||
|
|
2ba49e84bb | ||
|
|
262376aa75 | ||
|
|
4c8d2266ec | ||
|
|
bb98bf03aa | ||
|
|
19c3efc9e9 | ||
|
|
7164721ee0 | ||
|
|
74b16809ec | ||
|
|
220723d25f | ||
|
|
fdb03c9626 | ||
|
|
a81bbb9192 | ||
|
|
7a4aff8e4b | ||
|
|
2810632f4a | ||
|
|
2d0dd067b8 | ||
|
|
3ab25f5ff1 | ||
|
|
39bebea5f7 | ||
|
|
57681dcd3d | ||
|
|
168ce549f7 | ||
|
|
9ec94441f3 | ||
|
|
53e7b99605 | ||
|
|
abfe476cb9 | ||
|
|
bbca200ceb | ||
|
|
cb21cab117 | ||
|
|
1f80845a7a | ||
|
|
20088ef82b | ||
|
|
1e0b1a3607 | ||
|
|
24e8455c73 | ||
|
|
e42a732e93 | ||
|
|
0f2b94307f | ||
|
|
d333cb5199 | ||
|
|
a6db4f20ad | ||
|
|
9ed9472c01 | ||
|
|
f7fcde8312 | ||
|
|
6660c850f3 | ||
|
|
8a08bdf9f0 | ||
|
|
87807e22e0 | ||
|
|
0eb39abdb4 | ||
|
|
a499ebc158 | ||
|
|
9467e6c032 | ||
|
|
9d849a0ced | ||
|
|
2ca400ab16 | ||
|
|
4183067c77 | ||
|
|
5eb4691973 | ||
|
|
d14dfbf360 | ||
|
|
493a5ad02a | ||
|
|
481beff028 | ||
|
|
f1f7e438b4 | ||
|
|
00f84c9d8e | ||
|
|
f75b9c6c86 | ||
|
|
31bc6d5773 | ||
|
|
51dc1450d3 | ||
|
|
fcbea08c87 | ||
|
|
8d60a87aa1 | ||
|
|
956aa64519 | ||
|
|
fd1cb6ca23 | ||
|
|
37082ae436 | ||
|
|
bb47ca3d2e | ||
|
|
0dd3c84b24 | ||
|
|
848fca7e1b | ||
|
|
2500f99722 | ||
|
|
c7737c444f | ||
|
|
4d1a7ed69b | ||
|
|
626d5df67e | ||
|
|
e4c369deec | ||
|
|
307209e73f | ||
|
|
dc84935ee6 | ||
|
|
998c1f52ca | ||
|
|
2766758c66 | ||
|
|
258d1d82f3 | ||
|
|
46aaadb76a | ||
|
|
ea7a618810 | ||
|
|
c0e503b31f | ||
|
|
55f5a41752 | ||
|
|
b0be82be86 | ||
|
|
96a9bdb700 | ||
|
|
74e6d39c24 | ||
|
|
61dfa00222 | ||
|
|
476281db2b | ||
|
|
f32e31c73d | ||
|
|
ea72279080 | ||
|
|
16ba56af84 | ||
|
|
f13ddde988 | ||
|
|
67dc10dfe9 | ||
|
|
5fd216adc2 | ||
|
|
6f0268f6c0 | ||
|
|
2996dfb33a | ||
|
|
c92f2cd4ba | ||
|
|
8164d5c1ad | ||
|
|
d9d8d85f6e | ||
|
|
d49720703f | ||
|
|
2362a9b4dd | ||
|
|
a8265a5286 | ||
|
|
9ea7431b73 | ||
|
|
37e6f320fe | ||
|
|
c0c0d48edf | ||
|
|
284cccbe17 | ||
|
|
81a9a94264 | ||
|
|
dccf101554 | ||
|
|
a01c06bbc7 | ||
|
|
db43cf1b30 | ||
|
|
2f561b5604 | ||
|
|
5a30f036ff | ||
|
|
768b9ffd09 | ||
|
|
8732e50047 | ||
|
|
d6e0024c96 | ||
|
|
9759e86921 | ||
|
|
982c692c40 | ||
|
|
0c3ce7836c | ||
|
|
7ef86c5707 | ||
|
|
f62b88b930 | ||
|
|
03a326c841 | ||
|
|
4df4cafd70 | ||
|
|
4b9539cc6d | ||
|
|
87135c90bd | ||
|
|
853d416b2f | ||
|
|
bfd14b87bd | ||
|
|
88aba4e169 | ||
|
|
99e2fcb2e8 | ||
|
|
1f138ab68c | ||
|
|
99ded7454e | ||
|
|
f82cacac6d | ||
|
|
a548f61ea6 | ||
|
|
bfae715076 | ||
|
|
358e25b7c2 | ||
|
|
2c3fa54933 | ||
|
|
00cdd5833e | ||
|
|
52b1164e58 | ||
|
|
657bc9cdf0 | ||
|
|
ec6bcd41b0 | ||
|
|
1721cce040 | ||
|
|
e41a5ad6b0 | ||
|
|
ee1eca9e66 | ||
|
|
d049369172 | ||
|
|
6280a68d51 | ||
|
|
32054dc4f6 | ||
|
|
831c631048 | ||
|
|
e23711bcce | ||
|
|
440bff57d0 | ||
|
|
7345cc81c1 | ||
|
|
164ab26069 | ||
|
|
4b6ace80d3 | ||
|
|
653127a0f7 | ||
|
|
bf3a1e20fc | ||
|
|
d7a44e7589 | ||
|
|
6c0d583557 | ||
|
|
13f0fb25da | ||
|
|
818aca9ec8 | ||
|
|
1c7fb476b0 | ||
|
|
93843ed733 | ||
|
|
0973313703 | ||
|
|
bfbfbe8b11 | ||
|
|
8c62d9fe78 | ||
|
|
d5558f55ed | ||
|
|
a96ad6bd07 | ||
|
|
00d9482a99 | ||
|
|
0f90e2a30f | ||
|
|
3eed636404 | ||
|
|
a67f88381f | ||
|
|
808fd856d1 | ||
|
|
5b9b532458 | ||
|
|
9fba9bd6b7 | ||
|
|
c5ece144d0 | ||
|
|
b64e2e11db | ||
|
|
0ccd5714f9 | ||
|
|
e2dfc3eb20 | ||
|
|
40eeb9b7cb | ||
|
|
8fa62a0908 | ||
|
|
446eba8bc9 | ||
|
|
18579c0647 | ||
|
|
2bb94e24eb | ||
|
|
0d37e08638 | ||
|
|
ca89c5feca | ||
|
|
729c2adb3f | ||
|
|
a21f49cb02 | ||
|
|
ef697c4864 | ||
|
|
2652dea09a | ||
|
|
efa9312fca | ||
|
|
074ee70025 | ||
|
|
77117e48e3 | ||
|
|
da112d3417 | ||
|
|
ddaaf34dbd | ||
|
|
373e35324e | ||
|
|
09b2f27749 | ||
|
|
7e9f18bf24 | ||
|
|
ab3be26790 | ||
|
|
5c67a1cb12 | ||
|
|
e28ab19ed4 | ||
|
|
59f8334cfd | ||
|
|
718bec4bbc | ||
|
|
2d731cb24b | ||
|
|
1905936950 | ||
|
|
c362bc673c | ||
|
|
4da0a752ef | ||
|
|
221ee6a1c2 | ||
|
|
2e60ecec87 | ||
|
|
71386d3b05 | ||
|
|
89a7e2e4dc | ||
|
|
27440700a5 | ||
|
|
b5019cef12 | ||
|
|
7e48cbe1aa | ||
|
|
4b2c570e73 | ||
|
|
75b9703793 | ||
|
|
e983e1166a | ||
|
|
322f3bfb1d | ||
|
|
009b86c33b | ||
|
|
a5775a0f4f |
160
.github/workflows/cicd.yml
vendored
160
.github/workflows/cicd.yml
vendored
@@ -99,7 +99,7 @@ jobs:
|
||||
id: check-rc
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
if [[ "$TAG" == *".rc."* ]]; then
|
||||
if [[ "$TAG" == *"-rc."* ]]; then
|
||||
echo "IS_RC=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "IS_RC=false" >> $GITHUB_ENV
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
id: check-rc
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
if [[ "$TAG" == *".rc."* ]]; then
|
||||
if [[ "$TAG" == *"-rc."* ]]; then
|
||||
echo "IS_RC=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "IS_RC=false" >> $GITHUB_ENV
|
||||
@@ -219,7 +219,7 @@ jobs:
|
||||
id: check-rc
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
if [[ "$TAG" == *".rc."* ]]; then
|
||||
if [[ "$TAG" == *"-rc."* ]]; then
|
||||
echo "IS_RC=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "IS_RC=false" >> $GITHUB_ENV
|
||||
@@ -322,22 +322,96 @@ jobs:
|
||||
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
|
||||
- name: Copy tags from Docker Hub to GHCR
|
||||
# Mirror the already-built images (all architectures) to GHCR so we can sign them
|
||||
# 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..."
|
||||
MAJOR_TAG=$(echo $TAG | cut -d. -f1)
|
||||
MINOR_TAG=$(echo $TAG | cut -d. -f1,2)
|
||||
|
||||
echo "Waiting for multi-arch manifests 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
|
||||
|
||||
# Determine if this is an RC release
|
||||
IS_RC="false"
|
||||
if echo "$TAG" | grep -qE "rc[0-9]+$"; then
|
||||
IS_RC="true"
|
||||
fi
|
||||
|
||||
if [ "$IS_RC" = "true" ]; then
|
||||
echo "RC release detected - copying version-specific tags only"
|
||||
|
||||
# SQLite OSS
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:$TAG \
|
||||
docker://$GHCR_IMAGE:$TAG
|
||||
|
||||
# PostgreSQL OSS
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:postgresql-${TAG} -> ${{ env.GHCR_IMAGE }}:postgresql-${TAG}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:postgresql-$TAG \
|
||||
docker://$GHCR_IMAGE:postgresql-$TAG
|
||||
|
||||
# SQLite Enterprise
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-${TAG} -> ${{ env.GHCR_IMAGE }}:ee-${TAG}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:ee-$TAG \
|
||||
docker://$GHCR_IMAGE:ee-$TAG
|
||||
|
||||
# PostgreSQL Enterprise
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-postgresql-${TAG} -> ${{ env.GHCR_IMAGE }}:ee-postgresql-${TAG}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:ee-postgresql-$TAG \
|
||||
docker://$GHCR_IMAGE:ee-postgresql-$TAG
|
||||
else
|
||||
echo "Regular release detected - copying all tags (latest, major, minor, full version)"
|
||||
|
||||
# SQLite OSS - all tags
|
||||
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:${TAG_SUFFIX}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:$TAG_SUFFIX \
|
||||
docker://$GHCR_IMAGE:$TAG_SUFFIX
|
||||
done
|
||||
|
||||
# PostgreSQL OSS - all tags
|
||||
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:postgresql-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:postgresql-${TAG_SUFFIX}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:postgresql-$TAG_SUFFIX \
|
||||
docker://$GHCR_IMAGE:postgresql-$TAG_SUFFIX
|
||||
done
|
||||
|
||||
# SQLite Enterprise - all tags
|
||||
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:ee-${TAG_SUFFIX}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:ee-$TAG_SUFFIX \
|
||||
docker://$GHCR_IMAGE:ee-$TAG_SUFFIX
|
||||
done
|
||||
|
||||
# PostgreSQL Enterprise - all tags
|
||||
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-postgresql-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:ee-postgresql-${TAG_SUFFIX}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:ee-postgresql-$TAG_SUFFIX \
|
||||
docker://$GHCR_IMAGE:ee-postgresql-$TAG_SUFFIX
|
||||
done
|
||||
fi
|
||||
|
||||
echo "All images copied successfully to GHCR!"
|
||||
shell: bash
|
||||
|
||||
- name: Login to GitHub Container Registry (for cosign)
|
||||
@@ -366,28 +440,62 @@ jobs:
|
||||
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}"
|
||||
# Determine if this is an RC release
|
||||
IS_RC="false"
|
||||
if echo "$TAG" | grep -qE "rc[0-9]+$"; then
|
||||
IS_RC="true"
|
||||
fi
|
||||
|
||||
DIGEST="$(skopeo inspect --retry-times 3 docker://${IMAGE}:${TAG} | jq -r '.Digest')"
|
||||
REF="${IMAGE}@${DIGEST}"
|
||||
echo "Resolved digest: ${REF}"
|
||||
# Define image variants to sign
|
||||
if [ "$IS_RC" = "true" ]; then
|
||||
echo "RC release - signing version-specific tags only"
|
||||
IMAGE_TAGS=(
|
||||
"${TAG}"
|
||||
"postgresql-${TAG}"
|
||||
"ee-${TAG}"
|
||||
"ee-postgresql-${TAG}"
|
||||
)
|
||||
else
|
||||
echo "Regular release - signing all tags"
|
||||
MAJOR_TAG=$(echo $TAG | cut -d. -f1)
|
||||
MINOR_TAG=$(echo $TAG | cut -d. -f1,2)
|
||||
IMAGE_TAGS=(
|
||||
"latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"
|
||||
"postgresql-latest" "postgresql-$MAJOR_TAG" "postgresql-$MINOR_TAG" "postgresql-$TAG"
|
||||
"ee-latest" "ee-$MAJOR_TAG" "ee-$MINOR_TAG" "ee-$TAG"
|
||||
"ee-postgresql-latest" "ee-postgresql-$MAJOR_TAG" "ee-postgresql-$MINOR_TAG" "ee-postgresql-$TAG"
|
||||
)
|
||||
fi
|
||||
|
||||
echo "==> cosign sign (keyless) --recursive ${REF}"
|
||||
cosign sign --recursive "${REF}"
|
||||
# Sign each image variant for both registries
|
||||
for BASE_IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
|
||||
for IMAGE_TAG in "${IMAGE_TAGS[@]}"; do
|
||||
echo "Processing ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||
|
||||
echo "==> cosign sign (key) --recursive ${REF}"
|
||||
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
|
||||
DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${IMAGE_TAG} | jq -r '.Digest')"
|
||||
REF="${BASE_IMAGE}@${DIGEST}"
|
||||
echo "Resolved digest: ${REF}"
|
||||
|
||||
echo "==> cosign verify (public key) ${REF}"
|
||||
cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text
|
||||
echo "==> cosign sign (keyless) --recursive ${REF}"
|
||||
cosign sign --recursive "${REF}"
|
||||
|
||||
echo "==> cosign verify (keyless policy) ${REF}"
|
||||
cosign verify \
|
||||
--certificate-oidc-issuer "${issuer}" \
|
||||
--certificate-identity-regexp "${id_regex}" \
|
||||
"${REF}" -o text
|
||||
echo "==> cosign sign (key) --recursive ${REF}"
|
||||
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
|
||||
|
||||
echo "==> cosign verify (public key) ${REF}"
|
||||
cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text
|
||||
|
||||
echo "==> cosign verify (keyless policy) ${REF}"
|
||||
cosign verify \
|
||||
--certificate-oidc-issuer "${issuer}" \
|
||||
--certificate-identity-regexp "${id_regex}" \
|
||||
"${REF}" -o text
|
||||
|
||||
echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||
done
|
||||
done
|
||||
|
||||
echo "All images signed and verified successfully!"
|
||||
shell: bash
|
||||
|
||||
post-run:
|
||||
|
||||
426
.github/workflows/cicd.yml.backup
vendored
Normal file
426
.github/workflows/cicd.yml.backup
vendored
Normal 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
.github/workflows/mirror.yaml
vendored
2
.github/workflows/mirror.yaml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
skopeo list-tags --retry-times 3 docker://"${SOURCE_IMAGE}" \
|
||||
| jq -r '.Tags[]' | sort -u > src-tags.txt
|
||||
| jq -r '.Tags[]' | grep -v -e '-arm64' -e '-amd64' | sort -u > src-tags.txt
|
||||
echo "Found source tags: $(wc -l < src-tags.txt)"
|
||||
head -n 20 src-tags.txt || true
|
||||
|
||||
|
||||
125
.github/workflows/saas.yml
vendored
Normal file
125
.github/workflows/saas.yml
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
name: CI/CD Pipeline
|
||||
|
||||
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
|
||||
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # for GHCR push
|
||||
id-token: write # for Cosign Keyless (OIDC) Signing
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+.[0-9]+-s.[0-9]+"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pre-run:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Verify AWS identity
|
||||
run: aws sts get-caller-identity
|
||||
|
||||
- name: Start EC2 instances
|
||||
run: |
|
||||
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||
echo "EC2 instances started"
|
||||
|
||||
|
||||
release-arm:
|
||||
name: Build and Release (ARM64)
|
||||
runs-on: [self-hosted, linux, arm64, us-east-1]
|
||||
needs: [pre-run]
|
||||
if: >-
|
||||
${{
|
||||
needs.pre-run.result == 'success'
|
||||
}}
|
||||
# Job-level timeout to avoid runaway or stuck runs
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
# Target images
|
||||
AWS_IMAGE: ${{ secrets.aws_account_id }}.dkr.ecr.us-east-1.amazonaws.com/${{ github.event.repository.name }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Monitor storage space
|
||||
run: |
|
||||
THRESHOLD=75
|
||||
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
|
||||
echo "Used space: $USED_SPACE%"
|
||||
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
|
||||
echo "Used space is below the threshold of 75% free. Running Docker system prune."
|
||||
echo y | docker system prune -a
|
||||
else
|
||||
echo "Storage space is above the threshold. No action needed."
|
||||
fi
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@v2
|
||||
|
||||
- name: Extract tag name
|
||||
id: get-tag
|
||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Update version in package.json
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
|
||||
cat server/lib/consts.ts
|
||||
shell: bash
|
||||
|
||||
- name: Build and push Docker images (Docker Hub - ARM64)
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
make build-saas tag=$TAG
|
||||
echo "Built & pushed ARM64 images to: ${{ env.AWS_IMAGE }}:${TAG}"
|
||||
shell: bash
|
||||
|
||||
post-run:
|
||||
needs: [pre-run, release-arm]
|
||||
if: >-
|
||||
${{
|
||||
always() &&
|
||||
needs.pre-run.result == 'success' &&
|
||||
(needs.release-arm.result == 'success' || needs.release-arm.result == 'skipped' || needs.release-arm.result == 'failure')
|
||||
}}
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Verify AWS identity
|
||||
run: aws sts get-caller-identity
|
||||
|
||||
- name: Stop EC2 instances
|
||||
run: |
|
||||
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||
echo "EC2 instances stopped"
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -50,4 +50,5 @@ dynamic/
|
||||
*.mmdb
|
||||
scratch/
|
||||
tsconfig.json
|
||||
hydrateSaas.ts
|
||||
hydrateSaas.ts
|
||||
CLAUDE.md
|
||||
96
:w
96
:w
@@ -1,96 +0,0 @@
|
||||
import { db } from "@server/db/pg/driver";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { __DIRNAME } from "@server/lib/consts";
|
||||
|
||||
const version = "1.14.0";
|
||||
|
||||
export default async function migration() {
|
||||
console.log(`Running setup script ${version}...`);
|
||||
|
||||
try {
|
||||
await db.execute(sql`BEGIN`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE TABLE "loginPageBranding" (
|
||||
"loginPageBrandingId" serial PRIMARY KEY NOT NULL,
|
||||
"logoUrl" text NOT NULL,
|
||||
"logoWidth" integer NOT NULL,
|
||||
"logoHeight" integer NOT NULL,
|
||||
"primaryColor" text,
|
||||
"resourceTitle" text NOT NULL,
|
||||
"resourceSubtitle" text,
|
||||
"orgTitle" text,
|
||||
"orgSubtitle" text
|
||||
);
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE TABLE "loginPageBrandingOrg" (
|
||||
"loginPageBrandingId" integer NOT NULL,
|
||||
"orgId" varchar NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE TABLE "resourceHeaderAuthExtendedCompatibility" (
|
||||
"headerAuthExtendedCompatibilityId" serial PRIMARY KEY NOT NULL,
|
||||
"resourceId" integer NOT NULL,
|
||||
"extendedCompatibilityIsActivated" boolean DEFAULT false NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "resources" ADD COLUMN "maintenanceModeEnabled" boolean DEFAULT false NOT NULL;`
|
||||
);
|
||||
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "resources" ADD COLUMN "maintenanceModeType" text DEFAULT 'forced';`
|
||||
);
|
||||
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "resources" ADD COLUMN "maintenanceTitle" text;`
|
||||
);
|
||||
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "resources" ADD COLUMN "maintenanceMessage" text;`
|
||||
);
|
||||
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "resources" ADD COLUMN "maintenanceEstimatedTime" text;`
|
||||
);
|
||||
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "siteResources" ADD COLUMN "tcpPortRangeString" varchar;`
|
||||
);
|
||||
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "siteResources" ADD COLUMN "udpPortRangeString" varchar;`
|
||||
);
|
||||
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "siteResources" ADD COLUMN "disableIcmp" boolean DEFAULT false NOT NULL;`
|
||||
);
|
||||
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "loginPageBrandingOrg" ADD CONSTRAINT "loginPageBrandingOrg_loginPageBrandingId_loginPageBranding_loginPageBrandingId_fk" FOREIGN KEY ("loginPageBrandingId") REFERENCES "public"."loginPageBranding"("loginPageBrandingId") ON DELETE cascade ON UPDATE no action;`
|
||||
);
|
||||
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "loginPageBrandingOrg" ADD CONSTRAINT "loginPageBrandingOrg_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`
|
||||
);
|
||||
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "resourceHeaderAuthExtendedCompatibility" ADD CONSTRAINT "resourceHeaderAuthExtendedCompatibility_resourceId_resources_resourceId_fk" FOREIGN KEY ("resourceId") REFERENCES "public"."resources"("resourceId") ON DELETE cascade ON UPDATE no action;`
|
||||
);
|
||||
|
||||
await db.execute(sql`COMMIT`);
|
||||
console.log("Migrated database");
|
||||
} catch (e) {
|
||||
await db.execute(sql`ROLLBACK`);
|
||||
console.log("Unable to migrate database");
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
console.log(`${version} migration complete`);
|
||||
}
|
||||
23
Dockerfile
23
Dockerfile
@@ -1,10 +1,20 @@
|
||||
FROM node:24-alpine AS builder
|
||||
|
||||
# OCI Image Labels - Build Args for dynamic values
|
||||
ARG VERSION="dev"
|
||||
ARG REVISION=""
|
||||
ARG CREATED=""
|
||||
ARG LICENSE="AGPL-3.0"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ARG BUILD=oss
|
||||
ARG DATABASE=sqlite
|
||||
|
||||
# Derive title and description based on BUILD type
|
||||
ARG IMAGE_TITLE="Pangolin"
|
||||
ARG IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere"
|
||||
|
||||
RUN apk add --no-cache curl tzdata python3 make g++
|
||||
|
||||
# COPY package.json package-lock.json ./
|
||||
@@ -69,4 +79,17 @@ RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
|
||||
COPY server/db/names.json ./dist/names.json
|
||||
COPY public ./public
|
||||
|
||||
# OCI Image Labels
|
||||
# https://github.com/opencontainers/image-spec/blob/main/annotations.md
|
||||
LABEL org.opencontainers.image.source="https://github.com/fosrl/pangolin" \
|
||||
org.opencontainers.image.url="https://github.com/fosrl/pangolin" \
|
||||
org.opencontainers.image.documentation="https://docs.pangolin.net" \
|
||||
org.opencontainers.image.vendor="Fossorial" \
|
||||
org.opencontainers.image.licenses="${LICENSE}" \
|
||||
org.opencontainers.image.title="${IMAGE_TITLE}" \
|
||||
org.opencontainers.image.description="${IMAGE_DESCRIPTION}" \
|
||||
org.opencontainers.image.version="${VERSION}" \
|
||||
org.opencontainers.image.revision="${REVISION}" \
|
||||
org.opencontainers.image.created="${CREATED}"
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
|
||||
205
Makefile
205
Makefile
@@ -3,6 +3,25 @@
|
||||
major_tag := $(shell echo $(tag) | cut -d. -f1)
|
||||
minor_tag := $(shell echo $(tag) | cut -d. -f1,2)
|
||||
|
||||
# OCI label variables
|
||||
CREATED := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
REVISION := $(shell git rev-parse HEAD 2>/dev/null || echo "unknown")
|
||||
|
||||
# Common OCI build args for OSS builds
|
||||
OCI_ARGS_OSS = --build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$(REVISION) \
|
||||
--build-arg CREATED=$(CREATED) \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere"
|
||||
|
||||
# Common OCI build args for Enterprise builds
|
||||
OCI_ARGS_EE = --build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$(REVISION) \
|
||||
--build-arg CREATED=$(CREATED) \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere"
|
||||
|
||||
.PHONY: build-release build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
|
||||
|
||||
build-release: build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
|
||||
@@ -15,6 +34,7 @@ build-sqlite:
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=sqlite \
|
||||
$(OCI_ARGS_OSS) \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:latest \
|
||||
--tag fosrl/pangolin:$(major_tag) \
|
||||
@@ -30,6 +50,7 @@ build-postgresql:
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
$(OCI_ARGS_OSS) \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:postgresql-latest \
|
||||
--tag fosrl/pangolin:postgresql-$(major_tag) \
|
||||
@@ -45,6 +66,7 @@ build-ee-sqlite:
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
$(OCI_ARGS_EE) \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-latest \
|
||||
--tag fosrl/pangolin:ee-$(major_tag) \
|
||||
@@ -60,6 +82,7 @@ build-ee-postgresql:
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
$(OCI_ARGS_EE) \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-latest \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(major_tag) \
|
||||
@@ -67,6 +90,18 @@ build-ee-postgresql:
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||
--push .
|
||||
|
||||
build-saas:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
docker buildx build \
|
||||
--build-arg BUILD=saas \
|
||||
--build-arg DATABASE=pg \
|
||||
--platform linux/arm64 \
|
||||
--tag $(AWS_IMAGE):$(tag) \
|
||||
--push .
|
||||
|
||||
build-release-arm:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release-arm tag=<tag>"; \
|
||||
@@ -74,9 +109,16 @@ build-release-arm:
|
||||
fi
|
||||
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
||||
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
||||
CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:latest-arm64 \
|
||||
--tag fosrl/pangolin:$$MAJOR_TAG-arm64 \
|
||||
@@ -86,6 +128,11 @@ build-release-arm:
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:postgresql-latest-arm64 \
|
||||
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-arm64 \
|
||||
@@ -95,6 +142,12 @@ build-release-arm:
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:ee-latest-arm64 \
|
||||
--tag fosrl/pangolin:ee-$$MAJOR_TAG-arm64 \
|
||||
@@ -104,6 +157,12 @@ build-release-arm:
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-latest-arm64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-arm64 \
|
||||
@@ -118,9 +177,16 @@ build-release-amd:
|
||||
fi
|
||||
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
||||
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
||||
CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:latest-amd64 \
|
||||
--tag fosrl/pangolin:$$MAJOR_TAG-amd64 \
|
||||
@@ -130,6 +196,11 @@ build-release-amd:
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:postgresql-latest-amd64 \
|
||||
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-amd64 \
|
||||
@@ -139,6 +210,12 @@ build-release-amd:
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-latest-amd64 \
|
||||
--tag fosrl/pangolin:ee-$$MAJOR_TAG-amd64 \
|
||||
@@ -148,6 +225,12 @@ build-release-amd:
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-latest-amd64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-amd64 \
|
||||
@@ -201,27 +284,51 @@ build-rc:
|
||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:$(tag) \
|
||||
--push .
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||
--push .
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-$(tag) \
|
||||
--push .
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||
--push .
|
||||
@@ -231,27 +338,51 @@ build-rc-arm:
|
||||
echo "Error: tag is required. Usage: make build-rc-arm tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:$(tag)-arm64 \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:postgresql-$(tag)-arm64 \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:ee-$(tag)-arm64 \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag)-arm64 \
|
||||
--push .
|
||||
@@ -261,27 +392,51 @@ build-rc-amd:
|
||||
echo "Error: tag is required. Usage: make build-rc-amd tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:$(tag)-amd64 \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:postgresql-$(tag)-amd64 \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-$(tag)-amd64 \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=$(tag) \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg LICENSE="Fossorial Commercial" \
|
||||
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag)-amd64 \
|
||||
--push .
|
||||
@@ -314,16 +469,52 @@ create-manifests-rc:
|
||||
echo "All RC multi-arch manifests created successfully!"
|
||||
|
||||
build-arm:
|
||||
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker buildx build \
|
||||
--build-arg VERSION=dev \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/arm64 \
|
||||
-t fosrl/pangolin:latest .
|
||||
|
||||
build-x86:
|
||||
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
|
||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker buildx build \
|
||||
--build-arg VERSION=dev \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
--platform linux/amd64 \
|
||||
-t fosrl/pangolin:latest .
|
||||
|
||||
dev-build-sqlite:
|
||||
docker build --build-arg DATABASE=sqlite -t fosrl/pangolin:latest .
|
||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker build \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--build-arg VERSION=dev \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
-t fosrl/pangolin:latest .
|
||||
|
||||
dev-build-pg:
|
||||
docker build --build-arg DATABASE=pg -t fosrl/pangolin:postgresql-latest .
|
||||
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker build \
|
||||
--build-arg DATABASE=pg \
|
||||
--build-arg VERSION=dev \
|
||||
--build-arg REVISION=$$REVISION \
|
||||
--build-arg CREATED=$$CREATED \
|
||||
--build-arg IMAGE_TITLE="Pangolin" \
|
||||
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||
-t fosrl/pangolin:postgresql-latest .
|
||||
|
||||
test:
|
||||
docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
http:
|
||||
middlewares:
|
||||
badger:
|
||||
plugin:
|
||||
badger:
|
||||
disableForwardAuth: true
|
||||
redirect-to-https:
|
||||
redirectScheme:
|
||||
scheme: https
|
||||
@@ -13,6 +17,7 @@ http:
|
||||
- web
|
||||
middlewares:
|
||||
- redirect-to-https
|
||||
- badger
|
||||
|
||||
# Next.js router (handles everything except API and WebSocket paths)
|
||||
next-router:
|
||||
@@ -21,6 +26,8 @@ http:
|
||||
priority: 10
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- badger
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -31,6 +38,8 @@ http:
|
||||
priority: 100
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- badger
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
|
||||
@@ -43,9 +43,12 @@ entryPoints:
|
||||
http:
|
||||
tls:
|
||||
certResolver: "letsencrypt"
|
||||
encodedCharacters:
|
||||
allowEncodedSlash: true
|
||||
allowEncodedQuestionMark: true
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
|
||||
ping:
|
||||
entryPoint: "web"
|
||||
entryPoint: "web"
|
||||
|
||||
@@ -340,7 +340,7 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||
// Basic configuration
|
||||
fmt.Println("\n=== Basic Configuration ===")
|
||||
|
||||
config.IsEnterprise = readBoolNoDefault(reader, "Do you want to install the Enterprise version of Pangolin? The EE is free for persoal 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(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"orgPolicyConfig": "Конфигуриране на достъп за организация",
|
||||
"idpUpdatedDescription": "Идентификационният доставчик беше актуализиран успешно",
|
||||
"redirectUrl": "URL за пренасочване",
|
||||
"orgIdpRedirectUrls": "URL адреси за пренасочване",
|
||||
"redirectUrlAbout": "За URL за пренасочване",
|
||||
"redirectUrlAboutDescription": "Това е URL адресът, към който потребителите ще бъдат пренасочени след удостоверяване. Трябва да конфигурирате този URL адрес в настройките на доставчика на идентичност.",
|
||||
"pangolinAuth": "Authent - Pangolin",
|
||||
@@ -1479,7 +1480,7 @@
|
||||
"IAgreeToThe": "Съгласен съм с",
|
||||
"termsOfService": "условията за ползване",
|
||||
"and": "и",
|
||||
"privacyPolicy": "политиката за поверителност"
|
||||
"privacyPolicy": "политика за поверителност."
|
||||
},
|
||||
"signUpMarketing": {
|
||||
"keepMeInTheLoop": "Дръж ме в течение с новини, актуализации и нови функции чрез имейл."
|
||||
@@ -2349,6 +2350,7 @@
|
||||
"enterConfirmation": "Въведете потвърждение.",
|
||||
"blueprintViewDetails": "Подробности.",
|
||||
"defaultIdentityProvider": "По подразбиране доставчик на идентичност.",
|
||||
"defaultIdentityProviderDescription": "Когато е избран основен доставчик на идентичност, потребителят ще бъде автоматично пренасочен към доставчика за удостоверяване.",
|
||||
"editInternalResourceDialogNetworkSettings": "Мрежови настройки.",
|
||||
"editInternalResourceDialogAccessPolicy": "Политика за достъп.",
|
||||
"editInternalResourceDialogAddRoles": "Добавяне на роли.",
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"orgPolicyConfig": "Konfigurace přístupu pro organizaci",
|
||||
"idpUpdatedDescription": "Poskytovatel identity byl úspěšně aktualizován",
|
||||
"redirectUrl": "Přesměrovat URL",
|
||||
"orgIdpRedirectUrls": "Přesměrovat URL",
|
||||
"redirectUrlAbout": "O přesměrování URL",
|
||||
"redirectUrlAboutDescription": "Toto je URL, na kterou budou uživatelé po ověření přesměrováni. Tuto URL je třeba nastavit v nastavení poskytovatele identity.",
|
||||
"pangolinAuth": "Auth - Pangolin",
|
||||
@@ -1479,7 +1480,7 @@
|
||||
"IAgreeToThe": "Souhlasím s",
|
||||
"termsOfService": "podmínky služby",
|
||||
"and": "a",
|
||||
"privacyPolicy": "zásady ochrany osobních údajů"
|
||||
"privacyPolicy": "zásady ochrany osobních údajů."
|
||||
},
|
||||
"signUpMarketing": {
|
||||
"keepMeInTheLoop": "Udržujte mě ve smyčce s novinkami, aktualizacemi a novými funkcemi e-mailem."
|
||||
@@ -2349,6 +2350,7 @@
|
||||
"enterConfirmation": "Zadejte potvrzení",
|
||||
"blueprintViewDetails": "Detaily",
|
||||
"defaultIdentityProvider": "Výchozí poskytovatel identity",
|
||||
"defaultIdentityProviderDescription": "Pokud je vybrán výchozí poskytovatel identity, uživatel bude automaticky přesměrován na poskytovatele pro ověření.",
|
||||
"editInternalResourceDialogNetworkSettings": "Nastavení sítě",
|
||||
"editInternalResourceDialogAccessPolicy": "Přístupová politika",
|
||||
"editInternalResourceDialogAddRoles": "Přidat role",
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"orgPolicyConfig": "Zugriff für eine Organisation konfigurieren",
|
||||
"idpUpdatedDescription": "Identitätsanbieter erfolgreich aktualisiert",
|
||||
"redirectUrl": "Weiterleitungs-URL",
|
||||
"orgIdpRedirectUrls": "Umleitungs-URLs",
|
||||
"redirectUrlAbout": "Über die Weiterleitungs-URL",
|
||||
"redirectUrlAboutDescription": "Dies ist die URL, zu der Benutzer nach der Authentifizierung umgeleitet werden. Sie müssen diese URL in den Einstellungen des Identity Providers konfigurieren.",
|
||||
"pangolinAuth": "Authentifizierung - Pangolin",
|
||||
@@ -1479,7 +1480,7 @@
|
||||
"IAgreeToThe": "Ich stimme den",
|
||||
"termsOfService": "Nutzungsbedingungen zu",
|
||||
"and": "und",
|
||||
"privacyPolicy": "Datenschutzrichtlinie"
|
||||
"privacyPolicy": "datenschutzrichtlinie."
|
||||
},
|
||||
"signUpMarketing": {
|
||||
"keepMeInTheLoop": "Halten Sie mich auf dem Laufenden mit Neuigkeiten, Updates und neuen Funktionen per E-Mail."
|
||||
@@ -2349,6 +2350,7 @@
|
||||
"enterConfirmation": "Bestätigung eingeben",
|
||||
"blueprintViewDetails": "Details",
|
||||
"defaultIdentityProvider": "Standard Identitätsanbieter",
|
||||
"defaultIdentityProviderDescription": "Wenn ein Standard-Identity Provider ausgewählt ist, wird der Benutzer zur Authentifizierung automatisch an den Anbieter weitergeleitet.",
|
||||
"editInternalResourceDialogNetworkSettings": "Netzwerkeinstellungen",
|
||||
"editInternalResourceDialogAccessPolicy": "Zugriffsrichtlinie",
|
||||
"editInternalResourceDialogAddRoles": "Rollen hinzufügen",
|
||||
|
||||
@@ -257,6 +257,8 @@
|
||||
"accessRolesSearch": "Search roles...",
|
||||
"accessRolesAdd": "Add Role",
|
||||
"accessRoleDelete": "Delete Role",
|
||||
"accessApprovalsManage": "Manage Approvals",
|
||||
"accessApprovalsDescription": "Manage approval requests in the organization",
|
||||
"description": "Description",
|
||||
"inviteTitle": "Open Invitations",
|
||||
"inviteDescription": "Manage invitations for other users to join the organization",
|
||||
@@ -450,6 +452,18 @@
|
||||
"selectDuration": "Select duration",
|
||||
"selectResource": "Select Resource",
|
||||
"filterByResource": "Filter By Resource",
|
||||
"selectApprovalState": "Select Approval State",
|
||||
"filterByApprovalState": "Filter By Approval State",
|
||||
"approvalListEmpty": "No approvals",
|
||||
"approvalState": "Approval State",
|
||||
"approve": "Approve",
|
||||
"approved": "Approved",
|
||||
"denied": "Denied",
|
||||
"deniedApproval": "Denied Approval",
|
||||
"all": "All",
|
||||
"deny": "Deny",
|
||||
"viewDetails": "View Details",
|
||||
"requestingNewDeviceApproval": "requested a new device",
|
||||
"resetFilters": "Reset Filters",
|
||||
"totalBlocked": "Requests Blocked By Pangolin",
|
||||
"totalRequests": "Total Requests",
|
||||
@@ -729,16 +743,28 @@
|
||||
"countries": "Countries",
|
||||
"accessRoleCreate": "Create Role",
|
||||
"accessRoleCreateDescription": "Create a new role to group users and manage their permissions.",
|
||||
"accessRoleEdit": "Edit Role",
|
||||
"accessRoleEditDescription": "Edit role information.",
|
||||
"accessRoleCreateSubmit": "Create Role",
|
||||
"accessRoleCreated": "Role created",
|
||||
"accessRoleCreatedDescription": "The role has been successfully created.",
|
||||
"accessRoleErrorCreate": "Failed to create role",
|
||||
"accessRoleErrorCreateDescription": "An error occurred while creating the role.",
|
||||
"accessRoleUpdateSubmit": "Update Role",
|
||||
"accessRoleUpdated": "Role updated",
|
||||
"accessRoleUpdatedDescription": "The role has been successfully updated.",
|
||||
"accessApprovalUpdated": "Approval processed",
|
||||
"accessApprovalApprovedDescription": "Set Approval Request decision to approved.",
|
||||
"accessApprovalDeniedDescription": "Set Approval Request decision to denied.",
|
||||
"accessRoleErrorUpdate": "Failed to update role",
|
||||
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
|
||||
"accessApprovalErrorUpdate": "Failed to process approval",
|
||||
"accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.",
|
||||
"accessRoleErrorNewRequired": "New role is required",
|
||||
"accessRoleErrorRemove": "Failed to remove role",
|
||||
"accessRoleErrorRemoveDescription": "An error occurred while removing the role.",
|
||||
"accessRoleName": "Role Name",
|
||||
"accessRoleQuestionRemove": "You're about to delete the {name} role. You cannot undo this action.",
|
||||
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
|
||||
"accessRoleRemove": "Remove Role",
|
||||
"accessRoleRemoveDescription": "Remove a role from the organization",
|
||||
"accessRoleRemoveSubmit": "Remove Role",
|
||||
@@ -850,6 +876,7 @@
|
||||
"orgPolicyConfig": "Configure access for an organization",
|
||||
"idpUpdatedDescription": "Identity provider updated successfully",
|
||||
"redirectUrl": "Redirect URL",
|
||||
"orgIdpRedirectUrls": "Redirect URLs",
|
||||
"redirectUrlAbout": "About Redirect URL",
|
||||
"redirectUrlAboutDescription": "This is the URL to which users will be redirected after authentication. You need to configure this URL in the identity provider's settings.",
|
||||
"pangolinAuth": "Auth - Pangolin",
|
||||
@@ -873,7 +900,7 @@
|
||||
"inviteAlready": "Looks like you've been invited!",
|
||||
"inviteAlreadyDescription": "To accept the invite, you must log in or create an account.",
|
||||
"signupQuestion": "Already have an account?",
|
||||
"login": "Log in",
|
||||
"login": "Log In",
|
||||
"resourceNotFound": "Resource Not Found",
|
||||
"resourceNotFoundDescription": "The resource you're trying to access does not exist.",
|
||||
"pincodeRequirementsLength": "PIN must be exactly 6 digits",
|
||||
@@ -953,13 +980,13 @@
|
||||
"passwordExpiryDescription": "This organization requires you to change your password every {maxDays} days.",
|
||||
"changePasswordNow": "Change Password Now",
|
||||
"pincodeAuth": "Authenticator Code",
|
||||
"pincodeSubmit2": "Submit Code",
|
||||
"pincodeSubmit2": "Submit code",
|
||||
"passwordResetSubmit": "Request Reset",
|
||||
"passwordResetAlreadyHaveCode": "Enter Code",
|
||||
"passwordResetSmtpRequired": "Please contact your administrator",
|
||||
"passwordResetSmtpRequiredDescription": "A password reset code is required to reset your password. Please contact your administrator for assistance.",
|
||||
"passwordBack": "Back to Password",
|
||||
"loginBack": "Go back to log in",
|
||||
"loginBack": "Go back to main login page",
|
||||
"signup": "Sign up",
|
||||
"loginStart": "Log in to get started",
|
||||
"idpOidcTokenValidating": "Validating OIDC token",
|
||||
@@ -1117,6 +1144,10 @@
|
||||
"actionUpdateIdpOrg": "Update IDP Org",
|
||||
"actionCreateClient": "Create Client",
|
||||
"actionDeleteClient": "Delete Client",
|
||||
"actionArchiveClient": "Archive Client",
|
||||
"actionUnarchiveClient": "Unarchive Client",
|
||||
"actionBlockClient": "Block Client",
|
||||
"actionUnblockClient": "Unblock Client",
|
||||
"actionUpdateClient": "Update Client",
|
||||
"actionListClients": "List Clients",
|
||||
"actionGetClient": "Get Client",
|
||||
@@ -1133,14 +1164,14 @@
|
||||
"searchProgress": "Search...",
|
||||
"create": "Create",
|
||||
"orgs": "Organizations",
|
||||
"loginError": "An error occurred while logging in",
|
||||
"loginRequiredForDevice": "Login is required to authenticate your device.",
|
||||
"loginError": "An unexpected error occurred. Please try again.",
|
||||
"loginRequiredForDevice": "Login is required for your device.",
|
||||
"passwordForgot": "Forgot your password?",
|
||||
"otpAuth": "Two-Factor Authentication",
|
||||
"otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.",
|
||||
"otpAuthSubmit": "Submit Code",
|
||||
"idpContinue": "Or continue with",
|
||||
"otpAuthBack": "Back to Log In",
|
||||
"otpAuthBack": "Back to Password",
|
||||
"navbar": "Navigation Menu",
|
||||
"navbarDescription": "Main navigation menu for the application",
|
||||
"navbarDocsLink": "Documentation",
|
||||
@@ -1188,6 +1219,7 @@
|
||||
"sidebarOverview": "Overview",
|
||||
"sidebarHome": "Home",
|
||||
"sidebarSites": "Sites",
|
||||
"sidebarApprovals": "Approval Requests",
|
||||
"sidebarResources": "Resources",
|
||||
"sidebarProxyResources": "Public",
|
||||
"sidebarClientResources": "Private",
|
||||
@@ -1204,7 +1236,7 @@
|
||||
"sidebarIdentityProviders": "Identity Providers",
|
||||
"sidebarLicense": "License",
|
||||
"sidebarClients": "Clients",
|
||||
"sidebarUserDevices": "Users",
|
||||
"sidebarUserDevices": "User Devices",
|
||||
"sidebarMachineClients": "Machines",
|
||||
"sidebarDomains": "Domains",
|
||||
"sidebarGeneral": "Manage",
|
||||
@@ -1303,6 +1335,7 @@
|
||||
"refreshError": "Failed to refresh data",
|
||||
"verified": "Verified",
|
||||
"pending": "Pending",
|
||||
"pendingApproval": "Pending Approval",
|
||||
"sidebarBilling": "Billing",
|
||||
"billing": "Billing",
|
||||
"orgBillingDescription": "Manage billing information and subscriptions",
|
||||
@@ -1419,7 +1452,7 @@
|
||||
"securityKeyRemoveSuccess": "Security key removed successfully",
|
||||
"securityKeyRemoveError": "Failed to remove security key",
|
||||
"securityKeyLoadError": "Failed to load security keys",
|
||||
"securityKeyLogin": "Continue with security key",
|
||||
"securityKeyLogin": "Use Security Key",
|
||||
"securityKeyAuthError": "Failed to authenticate with security key",
|
||||
"securityKeyRecommendation": "Register a backup security key on another device to ensure you always have access to your account.",
|
||||
"registering": "Registering...",
|
||||
@@ -1479,7 +1512,7 @@
|
||||
"IAgreeToThe": "I agree to the",
|
||||
"termsOfService": "terms of service",
|
||||
"and": "and",
|
||||
"privacyPolicy": "privacy policy"
|
||||
"privacyPolicy": "privacy policy."
|
||||
},
|
||||
"signUpMarketing": {
|
||||
"keepMeInTheLoop": "Keep me in the loop with news, updates, and new features by email."
|
||||
@@ -1546,6 +1579,8 @@
|
||||
"IntervalSeconds": "Healthy Interval",
|
||||
"timeoutSeconds": "Timeout (sec)",
|
||||
"timeIsInSeconds": "Time is in seconds",
|
||||
"requireDeviceApproval": "Require Device Approvals",
|
||||
"requireDeviceApprovalDescription": "Users with this role need their devices approved by an admin before they can access resources",
|
||||
"retryAttempts": "Retry Attempts",
|
||||
"expectedResponseCodes": "Expected Response Codes",
|
||||
"expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.",
|
||||
@@ -2231,6 +2266,8 @@
|
||||
"deviceCodeInvalidFormat": "Code must be 9 characters (e.g., A1AJ-N5JD)",
|
||||
"deviceCodeInvalidOrExpired": "Invalid or expired code",
|
||||
"deviceCodeVerifyFailed": "Failed to verify device code",
|
||||
"deviceCodeValidating": "Validating device code...",
|
||||
"deviceCodeVerifying": "Verifying device authorization...",
|
||||
"signedInAs": "Signed in as",
|
||||
"deviceCodeEnterPrompt": "Enter the code displayed on the device",
|
||||
"continue": "Continue",
|
||||
@@ -2243,7 +2280,7 @@
|
||||
"deviceOrganizationsAccess": "Access to all organizations your account has access to",
|
||||
"deviceAuthorize": "Authorize {applicationName}",
|
||||
"deviceConnected": "Device Connected!",
|
||||
"deviceAuthorizedMessage": "Device is authorized to access your account.",
|
||||
"deviceAuthorizedMessage": "Device is authorized to access your account. Please return to the client application.",
|
||||
"pangolinCloud": "Pangolin Cloud",
|
||||
"viewDevices": "View Devices",
|
||||
"viewDevicesDescription": "Manage your connected devices",
|
||||
@@ -2305,6 +2342,7 @@
|
||||
"identifier": "Identifier",
|
||||
"deviceLoginUseDifferentAccount": "Not you? Use a different account.",
|
||||
"deviceLoginDeviceRequestingAccessToAccount": "A device is requesting access to this account.",
|
||||
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
|
||||
"noData": "No Data",
|
||||
"machineClients": "Machine Clients",
|
||||
"install": "Install",
|
||||
@@ -2349,6 +2387,7 @@
|
||||
"enterConfirmation": "Enter confirmation",
|
||||
"blueprintViewDetails": "Details",
|
||||
"defaultIdentityProvider": "Default Identity Provider",
|
||||
"defaultIdentityProviderDescription": "When a default identity provider is selected, the user will be automatically redirected to the provider for authentication.",
|
||||
"editInternalResourceDialogNetworkSettings": "Network Settings",
|
||||
"editInternalResourceDialogAccessPolicy": "Access Policy",
|
||||
"editInternalResourceDialogAddRoles": "Add Roles",
|
||||
@@ -2392,5 +2431,56 @@
|
||||
"maintenanceScreenTitle": "Service Temporarily Unavailable",
|
||||
"maintenanceScreenMessage": "We are currently experiencing technical difficulties. Please check back soon.",
|
||||
"maintenanceScreenEstimatedCompletion": "Estimated Completion:",
|
||||
"createInternalResourceDialogDestinationRequired": "Destination is required"
|
||||
"createInternalResourceDialogDestinationRequired": "Destination is required",
|
||||
"available": "Available",
|
||||
"archived": "Archived",
|
||||
"noArchivedDevices": "No archived devices found",
|
||||
"deviceArchived": "Device archived",
|
||||
"deviceArchivedDescription": "The device has been successfully archived.",
|
||||
"errorArchivingDevice": "Error archiving device",
|
||||
"failedToArchiveDevice": "Failed to archive device",
|
||||
"deviceQuestionArchive": "Are you sure you want to archive this device?",
|
||||
"deviceMessageArchive": "The device will be archived and removed from your active devices list.",
|
||||
"deviceArchiveConfirm": "Archive Device",
|
||||
"archiveDevice": "Archive Device",
|
||||
"archive": "Archive",
|
||||
"deviceUnarchived": "Device unarchived",
|
||||
"deviceUnarchivedDescription": "The device has been successfully unarchived.",
|
||||
"errorUnarchivingDevice": "Error unarchiving device",
|
||||
"failedToUnarchiveDevice": "Failed to unarchive device",
|
||||
"unarchive": "Unarchive",
|
||||
"archiveClient": "Archive Client",
|
||||
"archiveClientQuestion": "Are you sure you want to archive this client?",
|
||||
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
|
||||
"archiveClientConfirm": "Archive Client",
|
||||
"blockClient": "Block Client",
|
||||
"blockClientQuestion": "Are you sure you want to block this client?",
|
||||
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
|
||||
"blockClientConfirm": "Block Client",
|
||||
"active": "Active",
|
||||
"usernameOrEmail": "Username or Email",
|
||||
"selectYourOrganization": "Select your organization",
|
||||
"signInTo": "Log in in to",
|
||||
"signInWithPassword": "Continue with Password",
|
||||
"noAuthMethodsAvailable": "No authentication methods available for this organization.",
|
||||
"enterPassword": "Enter your password",
|
||||
"enterMfaCode": "Enter the code from your authenticator app",
|
||||
"securityKeyRequired": "Please use your security key to sign in.",
|
||||
"needToUseAnotherAccount": "Need to use a different account?",
|
||||
"loginLegalDisclaimer": "By clicking the buttons below, you acknowledge you have read, understand, and agree to the <termsOfService>Terms of Service</termsOfService> and <privacyPolicy>Privacy Policy</privacyPolicy>.",
|
||||
"termsOfService": "Terms of Service",
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"userNotFoundWithUsername": "No user found with that username.",
|
||||
"verify": "Verify",
|
||||
"signIn": "Sign In",
|
||||
"forgotPassword": "Forgot password?",
|
||||
"orgSignInTip": "If you've logged in before, you can enter your username or email above to authenticate with your organization's identity provider instead. It's easier!",
|
||||
"continueAnyway": "Continue anyway",
|
||||
"dontShowAgain": "Don't show again",
|
||||
"orgSignInNotice": "Did you know?",
|
||||
"signupOrgNotice": "Trying to sign in?",
|
||||
"signupOrgTip": "Are you trying to sign in through your organization's identity provider?",
|
||||
"signupOrgLink": "Sign in or sign up with your organization instead",
|
||||
"verifyEmailLogInWithDifferentAccount": "Use a Different Account",
|
||||
"logIn": "Log In"
|
||||
}
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"orgPolicyConfig": "Configurar acceso para una organización",
|
||||
"idpUpdatedDescription": "Proveedor de identidad actualizado correctamente",
|
||||
"redirectUrl": "URL de redirección",
|
||||
"orgIdpRedirectUrls": "Redirigir URL",
|
||||
"redirectUrlAbout": "Acerca de la URL de redirección",
|
||||
"redirectUrlAboutDescription": "Esta es la URL a la que los usuarios serán redireccionados después de la autenticación. Necesitas configurar esta URL en la configuración del proveedor de identidad.",
|
||||
"pangolinAuth": "Autenticación - Pangolin",
|
||||
@@ -1479,7 +1480,7 @@
|
||||
"IAgreeToThe": "Estoy de acuerdo con los",
|
||||
"termsOfService": "términos del servicio",
|
||||
"and": "y",
|
||||
"privacyPolicy": "política de privacidad"
|
||||
"privacyPolicy": "política de privacidad."
|
||||
},
|
||||
"signUpMarketing": {
|
||||
"keepMeInTheLoop": "Mantenerme en el bucle con noticias, actualizaciones y nuevas características por correo electrónico."
|
||||
@@ -2349,6 +2350,7 @@
|
||||
"enterConfirmation": "Ingresar confirmación",
|
||||
"blueprintViewDetails": "Detalles",
|
||||
"defaultIdentityProvider": "Proveedor de identidad predeterminado",
|
||||
"defaultIdentityProviderDescription": "Cuando se selecciona un proveedor de identidad por defecto, el usuario será redirigido automáticamente al proveedor de autenticación.",
|
||||
"editInternalResourceDialogNetworkSettings": "Configuración de red",
|
||||
"editInternalResourceDialogAccessPolicy": "Política de acceso",
|
||||
"editInternalResourceDialogAddRoles": "Agregar roles",
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"orgPolicyConfig": "Configurer l'accès pour une organisation",
|
||||
"idpUpdatedDescription": "Fournisseur d'identité mis à jour avec succès",
|
||||
"redirectUrl": "URL de redirection",
|
||||
"orgIdpRedirectUrls": "URL de redirection",
|
||||
"redirectUrlAbout": "À propos de l'URL de redirection",
|
||||
"redirectUrlAboutDescription": "C'est l'URL vers laquelle les utilisateurs seront redirigés après l'authentification. Vous devez configurer cette URL dans les paramètres du fournisseur d'identité.",
|
||||
"pangolinAuth": "Auth - Pangolin",
|
||||
@@ -1479,7 +1480,7 @@
|
||||
"IAgreeToThe": "Je suis d'accord avec",
|
||||
"termsOfService": "les conditions d'utilisation",
|
||||
"and": "et",
|
||||
"privacyPolicy": "la politique de confidentialité"
|
||||
"privacyPolicy": "politique de confidentialité."
|
||||
},
|
||||
"signUpMarketing": {
|
||||
"keepMeInTheLoop": "Gardez-moi dans la boucle avec des nouvelles, des mises à jour et de nouvelles fonctionnalités par courriel."
|
||||
@@ -2349,6 +2350,7 @@
|
||||
"enterConfirmation": "Entrez la confirmation",
|
||||
"blueprintViewDetails": "Détails",
|
||||
"defaultIdentityProvider": "Fournisseur d'identité par défaut",
|
||||
"defaultIdentityProviderDescription": "Lorsqu'un fournisseur d'identité par défaut est sélectionné, l'utilisateur sera automatiquement redirigé vers le fournisseur pour authentification.",
|
||||
"editInternalResourceDialogNetworkSettings": "Paramètres réseau",
|
||||
"editInternalResourceDialogAccessPolicy": "Politique d'accès",
|
||||
"editInternalResourceDialogAddRoles": "Ajouter des rôles",
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"orgPolicyConfig": "Configura l'accesso per un'organizzazione",
|
||||
"idpUpdatedDescription": "Provider di identità aggiornato con successo",
|
||||
"redirectUrl": "URL di Reindirizzamento",
|
||||
"orgIdpRedirectUrls": "Reindirizza URL",
|
||||
"redirectUrlAbout": "Informazioni sull'URL di Reindirizzamento",
|
||||
"redirectUrlAboutDescription": "Questo è l'URL a cui gli utenti saranno reindirizzati dopo l'autenticazione. È necessario configurare questo URL nelle impostazioni del provider di identità.",
|
||||
"pangolinAuth": "Autenticazione - Pangolina",
|
||||
@@ -1479,7 +1480,7 @@
|
||||
"IAgreeToThe": "Accetto i",
|
||||
"termsOfService": "termini di servizio",
|
||||
"and": "e",
|
||||
"privacyPolicy": "informativa sulla privacy"
|
||||
"privacyPolicy": "informativa sulla privacy."
|
||||
},
|
||||
"signUpMarketing": {
|
||||
"keepMeInTheLoop": "Tienimi in loop con notizie, aggiornamenti e nuove funzionalità via e-mail."
|
||||
@@ -2349,6 +2350,7 @@
|
||||
"enterConfirmation": "Inserisci conferma",
|
||||
"blueprintViewDetails": "Dettagli",
|
||||
"defaultIdentityProvider": "Provider di Identità Predefinito",
|
||||
"defaultIdentityProviderDescription": "Quando viene selezionato un provider di identità predefinito, l'utente verrà automaticamente reindirizzato al provider per l'autenticazione.",
|
||||
"editInternalResourceDialogNetworkSettings": "Impostazioni di Rete",
|
||||
"editInternalResourceDialogAccessPolicy": "Politica di Accesso",
|
||||
"editInternalResourceDialogAddRoles": "Aggiungi Ruoli",
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"orgPolicyConfig": "조직에 대한 접근을 구성하십시오.",
|
||||
"idpUpdatedDescription": "아이덴티티 제공자가 성공적으로 업데이트되었습니다",
|
||||
"redirectUrl": "리디렉션 URL",
|
||||
"orgIdpRedirectUrls": "리디렉션 URL",
|
||||
"redirectUrlAbout": "리디렉션 URL에 대한 정보",
|
||||
"redirectUrlAboutDescription": "사용자가 인증 후 리디렉션될 URL입니다. 이 URL을 신원 제공자 설정에서 구성해야 합니다.",
|
||||
"pangolinAuth": "인증 - 판골린",
|
||||
@@ -1479,7 +1480,7 @@
|
||||
"IAgreeToThe": "동의합니다",
|
||||
"termsOfService": "서비스 약관",
|
||||
"and": "및",
|
||||
"privacyPolicy": "개인 정보 보호 정책"
|
||||
"privacyPolicy": "개인 정보 보호 정책."
|
||||
},
|
||||
"signUpMarketing": {
|
||||
"keepMeInTheLoop": "이메일을 통해 소식, 업데이트 및 새로운 기능을 받아보세요."
|
||||
@@ -2349,6 +2350,7 @@
|
||||
"enterConfirmation": "확인 입력",
|
||||
"blueprintViewDetails": "세부 정보",
|
||||
"defaultIdentityProvider": "기본 아이덴티티 공급자",
|
||||
"defaultIdentityProviderDescription": "기본 ID 공급자가 선택되면, 사용자는 인증을 위해 자동으로 해당 공급자로 리디렉션됩니다.",
|
||||
"editInternalResourceDialogNetworkSettings": "네트워크 설정",
|
||||
"editInternalResourceDialogAccessPolicy": "액세스 정책",
|
||||
"editInternalResourceDialogAddRoles": "역할 추가",
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"orgPolicyConfig": "Konfigurer tilgang for en organisasjon",
|
||||
"idpUpdatedDescription": "Identitetsleverandør vellykket oppdatert",
|
||||
"redirectUrl": "Omdirigerings-URL",
|
||||
"orgIdpRedirectUrls": "Omadressere URL'er",
|
||||
"redirectUrlAbout": "Om omdirigerings-URL",
|
||||
"redirectUrlAboutDescription": "Dette er URLen som brukere vil bli omdirigert etter autentisering. Du må konfigurere denne URLen i identitetsleverandørens innstillinger.",
|
||||
"pangolinAuth": "Autentisering - Pangolin",
|
||||
@@ -1479,7 +1480,7 @@
|
||||
"IAgreeToThe": "Jeg godtar",
|
||||
"termsOfService": "brukervilkårene",
|
||||
"and": "og",
|
||||
"privacyPolicy": "personvernerklæringen"
|
||||
"privacyPolicy": "retningslinjer for personvern"
|
||||
},
|
||||
"signUpMarketing": {
|
||||
"keepMeInTheLoop": "Hold meg i løken med nyheter, oppdateringer og nye funksjoner via e-post."
|
||||
@@ -2349,6 +2350,7 @@
|
||||
"enterConfirmation": "Skriv inn bekreftelse",
|
||||
"blueprintViewDetails": "Detaljer",
|
||||
"defaultIdentityProvider": "Standard identitetsleverandør",
|
||||
"defaultIdentityProviderDescription": "Når en standard identitetsleverandør er valgt, vil brukeren automatisk bli omdirigert til leverandøren for autentisering.",
|
||||
"editInternalResourceDialogNetworkSettings": "Nettverksinnstillinger",
|
||||
"editInternalResourceDialogAccessPolicy": "Tilgangsregler for tilgang",
|
||||
"editInternalResourceDialogAddRoles": "Legg til roller",
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"orgPolicyConfig": "Toegang voor een organisatie configureren",
|
||||
"idpUpdatedDescription": "Identity provider succesvol bijgewerkt",
|
||||
"redirectUrl": "Omleidings URL",
|
||||
"orgIdpRedirectUrls": "URL's omleiden",
|
||||
"redirectUrlAbout": "Over omleidings-URL",
|
||||
"redirectUrlAboutDescription": "Dit is de URL waarnaar gebruikers worden doorverwezen na verificatie. U moet deze URL configureren in de instellingen van de identiteitsprovider.",
|
||||
"pangolinAuth": "Authenticatie - Pangolin",
|
||||
@@ -1479,7 +1480,7 @@
|
||||
"IAgreeToThe": "Ik ga akkoord met de",
|
||||
"termsOfService": "servicevoorwaarden",
|
||||
"and": "en",
|
||||
"privacyPolicy": "privacybeleid"
|
||||
"privacyPolicy": "privacy beleid"
|
||||
},
|
||||
"signUpMarketing": {
|
||||
"keepMeInTheLoop": "Houd me op de hoogte met nieuws, updates en nieuwe functies per e-mail."
|
||||
@@ -2349,6 +2350,7 @@
|
||||
"enterConfirmation": "Bevestiging invoeren",
|
||||
"blueprintViewDetails": "Details",
|
||||
"defaultIdentityProvider": "Standaard Identiteitsprovider",
|
||||
"defaultIdentityProviderDescription": "Wanneer een standaard identity provider is geselecteerd, zal de gebruiker automatisch worden doorgestuurd naar de provider voor authenticatie.",
|
||||
"editInternalResourceDialogNetworkSettings": "Netwerkinstellingen",
|
||||
"editInternalResourceDialogAccessPolicy": "Toegangsbeleid",
|
||||
"editInternalResourceDialogAddRoles": "Rollen toevoegen",
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"orgPolicyConfig": "Skonfiguruj dostęp dla organizacji",
|
||||
"idpUpdatedDescription": "Dostawca tożsamości został pomyślnie zaktualizowany",
|
||||
"redirectUrl": "URL przekierowania",
|
||||
"orgIdpRedirectUrls": "Przekieruj adresy URL",
|
||||
"redirectUrlAbout": "O URL przekierowania",
|
||||
"redirectUrlAboutDescription": "Jest to adres URL, na który użytkownicy zostaną przekierowani po uwierzytelnieniu. Musisz skonfigurować ten adres URL w ustawieniach dostawcy tożsamości.",
|
||||
"pangolinAuth": "Autoryzacja - Pangolin",
|
||||
@@ -1479,7 +1480,7 @@
|
||||
"IAgreeToThe": "Zgadzam się z",
|
||||
"termsOfService": "warunkami usługi",
|
||||
"and": "oraz",
|
||||
"privacyPolicy": "polityką prywatności"
|
||||
"privacyPolicy": "polityka prywatności."
|
||||
},
|
||||
"signUpMarketing": {
|
||||
"keepMeInTheLoop": "Zachowaj mnie w pętli z wiadomościami, aktualizacjami i nowymi funkcjami przez e-mail."
|
||||
@@ -2349,6 +2350,7 @@
|
||||
"enterConfirmation": "Wprowadź potwierdzenie",
|
||||
"blueprintViewDetails": "Szczegóły",
|
||||
"defaultIdentityProvider": "Domyślny dostawca tożsamości",
|
||||
"defaultIdentityProviderDescription": "Gdy zostanie wybrany domyślny dostawca tożsamości, użytkownik zostanie automatycznie przekierowany do dostawcy w celu uwierzytelnienia.",
|
||||
"editInternalResourceDialogNetworkSettings": "Ustawienia sieci",
|
||||
"editInternalResourceDialogAccessPolicy": "Polityka dostępowa",
|
||||
"editInternalResourceDialogAddRoles": "Dodaj role",
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"orgPolicyConfig": "Configurar acesso para uma organização",
|
||||
"idpUpdatedDescription": "Provedor de identidade atualizado com sucesso",
|
||||
"redirectUrl": "URL de Redirecionamento",
|
||||
"orgIdpRedirectUrls": "Redirecionar URLs",
|
||||
"redirectUrlAbout": "Sobre o URL de Redirecionamento",
|
||||
"redirectUrlAboutDescription": "Essa é a URL para a qual os usuários serão redirecionados após a autenticação. Você precisa configurar esta URL nas configurações do provedor de identidade.",
|
||||
"pangolinAuth": "Autenticação - Pangolin",
|
||||
@@ -1479,7 +1480,7 @@
|
||||
"IAgreeToThe": "Concordo com",
|
||||
"termsOfService": "os termos de serviço",
|
||||
"and": "e",
|
||||
"privacyPolicy": "política de privacidade"
|
||||
"privacyPolicy": "política de privacidade."
|
||||
},
|
||||
"signUpMarketing": {
|
||||
"keepMeInTheLoop": "Mantenha-me à disposição com notícias, atualizações e novos recursos por e-mail."
|
||||
@@ -2349,6 +2350,7 @@
|
||||
"enterConfirmation": "Inserir confirmação",
|
||||
"blueprintViewDetails": "Detalhes",
|
||||
"defaultIdentityProvider": "Provedor de Identidade Padrão",
|
||||
"defaultIdentityProviderDescription": "Quando um provedor de identidade padrão for selecionado, o usuário será automaticamente redirecionado para o provedor de autenticação.",
|
||||
"editInternalResourceDialogNetworkSettings": "Configurações de Rede",
|
||||
"editInternalResourceDialogAccessPolicy": "Política de Acesso",
|
||||
"editInternalResourceDialogAddRoles": "Adicionar Funções",
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"orgPolicyConfig": "Настроить доступ для организации",
|
||||
"idpUpdatedDescription": "Поставщик удостоверений успешно обновлён",
|
||||
"redirectUrl": "URL редиректа",
|
||||
"orgIdpRedirectUrls": "Перенаправление URL",
|
||||
"redirectUrlAbout": "О редиректе URL",
|
||||
"redirectUrlAboutDescription": "Это URL, на который пользователи будут перенаправлены после аутентификации. Вам нужно настроить этот URL в настройках провайдера.",
|
||||
"pangolinAuth": "Аутентификация - Pangolin",
|
||||
@@ -1479,7 +1480,7 @@
|
||||
"IAgreeToThe": "Я согласен с",
|
||||
"termsOfService": "условия использования",
|
||||
"and": "и",
|
||||
"privacyPolicy": "политика конфиденциальности"
|
||||
"privacyPolicy": "политика конфиденциальности."
|
||||
},
|
||||
"signUpMarketing": {
|
||||
"keepMeInTheLoop": "Держите меня в цикле с новостями, обновлениями и новыми функциями по электронной почте."
|
||||
@@ -2349,6 +2350,7 @@
|
||||
"enterConfirmation": "Введите подтверждение",
|
||||
"blueprintViewDetails": "Подробности",
|
||||
"defaultIdentityProvider": "Поставщик удостоверений по умолчанию",
|
||||
"defaultIdentityProviderDescription": "Когда выбран поставщик идентификации по умолчанию, пользователь будет автоматически перенаправлен на провайдер для аутентификации.",
|
||||
"editInternalResourceDialogNetworkSettings": "Настройки сети",
|
||||
"editInternalResourceDialogAccessPolicy": "Политика доступа",
|
||||
"editInternalResourceDialogAddRoles": "Добавить роли",
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"orgPolicyConfig": "Bir kuruluş için erişimi yapılandırın",
|
||||
"idpUpdatedDescription": "Kimlik sağlayıcı başarıyla güncellendi",
|
||||
"redirectUrl": "Yönlendirme URL'si",
|
||||
"orgIdpRedirectUrls": "Yönlendirme URL'leri",
|
||||
"redirectUrlAbout": "Yönlendirme URL'si Hakkında",
|
||||
"redirectUrlAboutDescription": "Bu, kimlik doğrulamasından sonra kullanıcıların yönlendirileceği URL'dir. Bu URL'yi kimlik sağlayıcınızın ayarlarında yapılandırmanız gerekir.",
|
||||
"pangolinAuth": "Yetkilendirme - Pangolin",
|
||||
@@ -1479,7 +1480,7 @@
|
||||
"IAgreeToThe": "Kabul ediyorum",
|
||||
"termsOfService": "hizmet şartları",
|
||||
"and": "ve",
|
||||
"privacyPolicy": "gizlilik politikası"
|
||||
"privacyPolicy": "gizlilik politikası."
|
||||
},
|
||||
"signUpMarketing": {
|
||||
"keepMeInTheLoop": "Bana e-posta yoluyla haberler, güncellemeler ve yeni özellikler hakkında bilgi verin."
|
||||
@@ -2349,6 +2350,7 @@
|
||||
"enterConfirmation": "Onayı girin",
|
||||
"blueprintViewDetails": "Detaylar",
|
||||
"defaultIdentityProvider": "Varsayılan Kimlik Sağlayıcı",
|
||||
"defaultIdentityProviderDescription": "Varsayılan bir kimlik sağlayıcı seçildiğinde, kullanıcı kimlik doğrulaması için otomatik olarak sağlayıcıya yönlendirilecektir.",
|
||||
"editInternalResourceDialogNetworkSettings": "Ağ Ayarları",
|
||||
"editInternalResourceDialogAccessPolicy": "Erişim Politikası",
|
||||
"editInternalResourceDialogAddRoles": "Roller Ekle",
|
||||
|
||||
@@ -850,6 +850,7 @@
|
||||
"orgPolicyConfig": "配置组织访问权限",
|
||||
"idpUpdatedDescription": "身份提供商更新成功",
|
||||
"redirectUrl": "重定向网址",
|
||||
"orgIdpRedirectUrls": "重定向URL",
|
||||
"redirectUrlAbout": "关于重定向网址",
|
||||
"redirectUrlAboutDescription": "这是用户在验证后将被重定向到的URL。您需要在身份提供者的设置中配置此URL。",
|
||||
"pangolinAuth": "认证 - Pangolin",
|
||||
@@ -1479,7 +1480,7 @@
|
||||
"IAgreeToThe": "我同意",
|
||||
"termsOfService": "服务条款",
|
||||
"and": "和",
|
||||
"privacyPolicy": "隐私政策"
|
||||
"privacyPolicy": "隐私政策。"
|
||||
},
|
||||
"signUpMarketing": {
|
||||
"keepMeInTheLoop": "通过电子邮件让我在循环中保持新闻、更新和新功能。"
|
||||
@@ -2349,6 +2350,7 @@
|
||||
"enterConfirmation": "输入确认",
|
||||
"blueprintViewDetails": "详细信息",
|
||||
"defaultIdentityProvider": "默认身份提供商",
|
||||
"defaultIdentityProviderDescription": "当选择默认身份提供商时,用户将自动重定向到提供商进行身份验证。",
|
||||
"editInternalResourceDialogNetworkSettings": "网络设置",
|
||||
"editInternalResourceDialogAccessPolicy": "访问策略",
|
||||
"editInternalResourceDialogAddRoles": "添加角色",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI",
|
||||
"description": "Identity-aware VPN and proxy for remote access to anything, anywhere and Dashboard UI",
|
||||
"homepage": "https://github.com/fosrl/pangolin",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -78,6 +78,10 @@ export enum ActionsEnum {
|
||||
updateSiteResource = "updateSiteResource",
|
||||
createClient = "createClient",
|
||||
deleteClient = "deleteClient",
|
||||
archiveClient = "archiveClient",
|
||||
unarchiveClient = "unarchiveClient",
|
||||
blockClient = "blockClient",
|
||||
unblockClient = "unblockClient",
|
||||
updateClient = "updateClient",
|
||||
listClients = "listClients",
|
||||
getClient = "getClient",
|
||||
@@ -125,7 +129,9 @@ export enum ActionsEnum {
|
||||
getBlueprint = "getBlueprint",
|
||||
applyBlueprint = "applyBlueprint",
|
||||
viewLogs = "viewLogs",
|
||||
exportLogs = "exportLogs"
|
||||
exportLogs = "exportLogs",
|
||||
listApprovals = "listApprovals",
|
||||
updateApprovals = "updateApprovals"
|
||||
}
|
||||
|
||||
export async function checkUserActionPermission(
|
||||
|
||||
@@ -68,7 +68,7 @@ export const MAJOR_ASNS = [
|
||||
code: "AS36351",
|
||||
asn: 36351
|
||||
},
|
||||
|
||||
|
||||
// CDNs
|
||||
{
|
||||
name: "Cloudflare",
|
||||
@@ -90,7 +90,7 @@ export const MAJOR_ASNS = [
|
||||
code: "AS16625",
|
||||
asn: 16625
|
||||
},
|
||||
|
||||
|
||||
// Mobile Carriers - US
|
||||
{
|
||||
name: "T-Mobile USA",
|
||||
@@ -117,7 +117,7 @@ export const MAJOR_ASNS = [
|
||||
code: "AS6430",
|
||||
asn: 6430
|
||||
},
|
||||
|
||||
|
||||
// Mobile Carriers - Europe
|
||||
{
|
||||
name: "Vodafone UK",
|
||||
@@ -144,7 +144,7 @@ export const MAJOR_ASNS = [
|
||||
code: "AS12430",
|
||||
asn: 12430
|
||||
},
|
||||
|
||||
|
||||
// Mobile Carriers - Asia
|
||||
{
|
||||
name: "NTT DoCoMo (Japan)",
|
||||
@@ -176,7 +176,7 @@ export const MAJOR_ASNS = [
|
||||
code: "AS9808",
|
||||
asn: 9808
|
||||
},
|
||||
|
||||
|
||||
// Major US ISPs
|
||||
{
|
||||
name: "AT&T Services",
|
||||
@@ -208,7 +208,7 @@ export const MAJOR_ASNS = [
|
||||
code: "AS209",
|
||||
asn: 209
|
||||
},
|
||||
|
||||
|
||||
// Major European ISPs
|
||||
{
|
||||
name: "Deutsche Telekom",
|
||||
@@ -235,7 +235,7 @@ export const MAJOR_ASNS = [
|
||||
code: "AS12956",
|
||||
asn: 12956
|
||||
},
|
||||
|
||||
|
||||
// Major Asian ISPs
|
||||
{
|
||||
name: "China Telecom",
|
||||
@@ -262,7 +262,7 @@ export const MAJOR_ASNS = [
|
||||
code: "AS55836",
|
||||
asn: 55836
|
||||
},
|
||||
|
||||
|
||||
// VPN/Proxy Providers
|
||||
{
|
||||
name: "Private Internet Access",
|
||||
@@ -279,7 +279,7 @@ export const MAJOR_ASNS = [
|
||||
code: "AS213281",
|
||||
asn: 213281
|
||||
},
|
||||
|
||||
|
||||
// Social Media / Major Tech
|
||||
{
|
||||
name: "Facebook/Meta",
|
||||
@@ -301,7 +301,7 @@ export const MAJOR_ASNS = [
|
||||
code: "AS2906",
|
||||
asn: 2906
|
||||
},
|
||||
|
||||
|
||||
// Academic/Research
|
||||
{
|
||||
name: "MIT",
|
||||
|
||||
150
server/db/ios_models.json
Normal file
150
server/db/ios_models.json
Normal file
@@ -0,0 +1,150 @@
|
||||
{
|
||||
"iPad1,1": "iPad",
|
||||
"iPad2,1": "iPad 2",
|
||||
"iPad2,2": "iPad 2",
|
||||
"iPad2,3": "iPad 2",
|
||||
"iPad2,4": "iPad 2",
|
||||
"iPad3,1": "iPad 3rd Gen",
|
||||
"iPad3,3": "iPad 3rd Gen",
|
||||
"iPad3,2": "iPad 3rd Gen",
|
||||
"iPad3,4": "iPad 4th Gen",
|
||||
"iPad3,5": "iPad 4th Gen",
|
||||
"iPad3,6": "iPad 4th Gen",
|
||||
"iPad6,11": "iPad 9.7 5th Gen",
|
||||
"iPad6,12": "iPad 9.7 5th Gen",
|
||||
"iPad7,5": "iPad 9.7 6th Gen",
|
||||
"iPad7,6": "iPad 9.7 6th Gen",
|
||||
"iPad7,11": "iPad 10.2 7th Gen",
|
||||
"iPad7,12": "iPad 10.2 7th Gen",
|
||||
"iPad11,6": "iPad 10.2 8th Gen",
|
||||
"iPad11,7": "iPad 10.2 8th Gen",
|
||||
"iPad12,1": "iPad 10.2 9th Gen",
|
||||
"iPad12,2": "iPad 10.2 9th Gen",
|
||||
"iPad13,18": "iPad 10.9 10th Gen",
|
||||
"iPad13,19": "iPad 10.9 10th Gen",
|
||||
"iPad4,1": "iPad Air",
|
||||
"iPad4,2": "iPad Air",
|
||||
"iPad4,3": "iPad Air",
|
||||
"iPad5,3": "iPad Air 2",
|
||||
"iPad5,4": "iPad Air 2",
|
||||
"iPad11,3": "iPad Air 3rd Gen",
|
||||
"iPad11,4": "iPad Air 3rd Gen",
|
||||
"iPad13,1": "iPad Air 4th Gen",
|
||||
"iPad13,2": "iPad Air 4th Gen",
|
||||
"iPad13,16": "iPad Air 5th Gen",
|
||||
"iPad13,17": "iPad Air 5th Gen",
|
||||
"iPad14,8": "iPad Air M2 11",
|
||||
"iPad14,9": "iPad Air M2 11",
|
||||
"iPad14,10": "iPad Air M2 13",
|
||||
"iPad14,11": "iPad Air M2 13",
|
||||
"iPad2,5": "iPad mini",
|
||||
"iPad2,6": "iPad mini",
|
||||
"iPad2,7": "iPad mini",
|
||||
"iPad4,4": "iPad mini 2",
|
||||
"iPad4,5": "iPad mini 2",
|
||||
"iPad4,6": "iPad mini 2",
|
||||
"iPad4,7": "iPad mini 3",
|
||||
"iPad4,8": "iPad mini 3",
|
||||
"iPad4,9": "iPad mini 3",
|
||||
"iPad5,1": "iPad mini 4",
|
||||
"iPad5,2": "iPad mini 4",
|
||||
"iPad11,1": "iPad mini 5th Gen",
|
||||
"iPad11,2": "iPad mini 5th Gen",
|
||||
"iPad14,1": "iPad mini 6th Gen",
|
||||
"iPad14,2": "iPad mini 6th Gen",
|
||||
"iPad6,7": "iPad Pro 12.9",
|
||||
"iPad6,8": "iPad Pro 12.9",
|
||||
"iPad6,3": "iPad Pro 9.7",
|
||||
"iPad6,4": "iPad Pro 9.7",
|
||||
"iPad7,3": "iPad Pro 10.5",
|
||||
"iPad7,4": "iPad Pro 10.5",
|
||||
"iPad7,1": "iPad Pro 12.9",
|
||||
"iPad7,2": "iPad Pro 12.9",
|
||||
"iPad8,1": "iPad Pro 11",
|
||||
"iPad8,2": "iPad Pro 11",
|
||||
"iPad8,3": "iPad Pro 11",
|
||||
"iPad8,4": "iPad Pro 11",
|
||||
"iPad8,5": "iPad Pro 12.9",
|
||||
"iPad8,6": "iPad Pro 12.9",
|
||||
"iPad8,7": "iPad Pro 12.9",
|
||||
"iPad8,8": "iPad Pro 12.9",
|
||||
"iPad8,9": "iPad Pro 11",
|
||||
"iPad8,10": "iPad Pro 11",
|
||||
"iPad8,11": "iPad Pro 12.9",
|
||||
"iPad8,12": "iPad Pro 12.9",
|
||||
"iPad13,4": "iPad Pro 11",
|
||||
"iPad13,5": "iPad Pro 11",
|
||||
"iPad13,6": "iPad Pro 11",
|
||||
"iPad13,7": "iPad Pro 11",
|
||||
"iPad13,8": "iPad Pro 12.9",
|
||||
"iPad13,9": "iPad Pro 12.9",
|
||||
"iPad13,10": "iPad Pro 12.9",
|
||||
"iPad13,11": "iPad Pro 12.9",
|
||||
"iPad14,3": "iPad Pro 11",
|
||||
"iPad14,4": "iPad Pro 11",
|
||||
"iPad14,5": "iPad Pro 12.9",
|
||||
"iPad14,6": "iPad Pro 12.9",
|
||||
"iPad16,3": "iPad Pro M4 11",
|
||||
"iPad16,4": "iPad Pro M4 11",
|
||||
"iPad16,5": "iPad Pro M4 13",
|
||||
"iPad16,6": "iPad Pro M4 13",
|
||||
"iPhone1,1": "iPhone",
|
||||
"iPhone1,2": "iPhone 3G",
|
||||
"iPhone2,1": "iPhone 3GS",
|
||||
"iPhone3,1": "iPhone 4",
|
||||
"iPhone3,2": "iPhone 4",
|
||||
"iPhone3,3": "iPhone 4",
|
||||
"iPhone4,1": "iPhone 4S",
|
||||
"iPhone5,1": "iPhone 5",
|
||||
"iPhone5,2": "iPhone 5",
|
||||
"iPhone5,3": "iPhone 5c",
|
||||
"iPhone5,4": "iPhone 5c",
|
||||
"iPhone6,1": "iPhone 5s",
|
||||
"iPhone6,2": "iPhone 5s",
|
||||
"iPhone7,2": "iPhone 6",
|
||||
"iPhone7,1": "iPhone 6 Plus",
|
||||
"iPhone8,1": "iPhone 6s",
|
||||
"iPhone8,2": "iPhone 6s Plus",
|
||||
"iPhone8,4": "iPhone SE",
|
||||
"iPhone9,1": "iPhone 7",
|
||||
"iPhone9,3": "iPhone 7",
|
||||
"iPhone9,2": "iPhone 7 Plus",
|
||||
"iPhone9,4": "iPhone 7 Plus",
|
||||
"iPhone10,1": "iPhone 8",
|
||||
"iPhone10,4": "iPhone 8",
|
||||
"iPhone10,2": "iPhone 8 Plus",
|
||||
"iPhone10,5": "iPhone 8 Plus",
|
||||
"iPhone10,3": "iPhone X",
|
||||
"iPhone10,6": "iPhone X",
|
||||
"iPhone11,2": "iPhone Xs",
|
||||
"iPhone11,6": "iPhone Xs Max",
|
||||
"iPhone11,8": "iPhone XR",
|
||||
"iPhone12,1": "iPhone 11",
|
||||
"iPhone12,3": "iPhone 11 Pro",
|
||||
"iPhone12,5": "iPhone 11 Pro Max",
|
||||
"iPhone12,8": "iPhone SE",
|
||||
"iPhone13,1": "iPhone 12 mini",
|
||||
"iPhone13,2": "iPhone 12",
|
||||
"iPhone13,3": "iPhone 12 Pro",
|
||||
"iPhone13,4": "iPhone 12 Pro Max",
|
||||
"iPhone14,4": "iPhone 13 mini",
|
||||
"iPhone14,5": "iPhone 13",
|
||||
"iPhone14,2": "iPhone 13 Pro",
|
||||
"iPhone14,3": "iPhone 13 Pro Max",
|
||||
"iPhone14,6": "iPhone SE",
|
||||
"iPhone14,7": "iPhone 14",
|
||||
"iPhone14,8": "iPhone 14 Plus",
|
||||
"iPhone15,2": "iPhone 14 Pro",
|
||||
"iPhone15,3": "iPhone 14 Pro Max",
|
||||
"iPhone15,4": "iPhone 15",
|
||||
"iPhone15,5": "iPhone 15 Plus",
|
||||
"iPhone16,1": "iPhone 15 Pro",
|
||||
"iPhone16,2": "iPhone 15 Pro Max",
|
||||
"iPod1,1": "iPod touch Original",
|
||||
"iPod2,1": "iPod touch 2nd",
|
||||
"iPod3,1": "iPod touch 3rd Gen",
|
||||
"iPod4,1": "iPod touch 4th",
|
||||
"iPod5,1": "iPod touch 5th",
|
||||
"iPod7,1": "iPod touch 6th Gen",
|
||||
"iPod9,1": "iPod touch 7th Gen"
|
||||
}
|
||||
201
server/db/mac_models.json
Normal file
201
server/db/mac_models.json
Normal file
@@ -0,0 +1,201 @@
|
||||
{
|
||||
"PowerMac4,4": "eMac",
|
||||
"PowerMac6,4": "eMac",
|
||||
"PowerBook2,1": "iBook",
|
||||
"PowerBook2,2": "iBook",
|
||||
"PowerBook4,1": "iBook",
|
||||
"PowerBook4,2": "iBook",
|
||||
"PowerBook4,3": "iBook",
|
||||
"PowerBook6,3": "iBook",
|
||||
"PowerBook6,5": "iBook",
|
||||
"PowerBook6,7": "iBook",
|
||||
"iMac,1": "iMac",
|
||||
"PowerMac2,1": "iMac",
|
||||
"PowerMac2,2": "iMac",
|
||||
"PowerMac4,1": "iMac",
|
||||
"PowerMac4,2": "iMac",
|
||||
"PowerMac4,5": "iMac",
|
||||
"PowerMac6,1": "iMac",
|
||||
"PowerMac6,3*": "iMac",
|
||||
"PowerMac6,3": "iMac",
|
||||
"PowerMac8,1": "iMac",
|
||||
"PowerMac8,2": "iMac",
|
||||
"PowerMac12,1": "iMac",
|
||||
"iMac4,1": "iMac",
|
||||
"iMac4,2": "iMac",
|
||||
"iMac5,2": "iMac",
|
||||
"iMac5,1": "iMac",
|
||||
"iMac6,1": "iMac",
|
||||
"iMac7,1": "iMac",
|
||||
"iMac8,1": "iMac",
|
||||
"iMac9,1": "iMac",
|
||||
"iMac10,1": "iMac",
|
||||
"iMac11,1": "iMac",
|
||||
"iMac11,2": "iMac",
|
||||
"iMac11,3": "iMac",
|
||||
"iMac12,1": "iMac",
|
||||
"iMac12,2": "iMac",
|
||||
"iMac13,1": "iMac",
|
||||
"iMac13,2": "iMac",
|
||||
"iMac14,1": "iMac",
|
||||
"iMac14,3": "iMac",
|
||||
"iMac14,2": "iMac",
|
||||
"iMac14,4": "iMac",
|
||||
"iMac15,1": "iMac",
|
||||
"iMac16,1": "iMac",
|
||||
"iMac16,2": "iMac",
|
||||
"iMac17,1": "iMac",
|
||||
"iMac18,1": "iMac",
|
||||
"iMac18,2": "iMac",
|
||||
"iMac18,3": "iMac",
|
||||
"iMac19,2": "iMac",
|
||||
"iMac19,1": "iMac",
|
||||
"iMac20,1": "iMac",
|
||||
"iMac20,2": "iMac",
|
||||
"iMac21,2": "iMac",
|
||||
"iMac21,1": "iMac",
|
||||
"iMacPro1,1": "iMac Pro",
|
||||
"PowerMac10,1": "Mac mini",
|
||||
"PowerMac10,2": "Mac mini",
|
||||
"Macmini1,1": "Mac mini",
|
||||
"Macmini2,1": "Mac mini",
|
||||
"Macmini3,1": "Mac mini",
|
||||
"Macmini4,1": "Mac mini",
|
||||
"Macmini5,1": "Mac mini",
|
||||
"Macmini5,2": "Mac mini",
|
||||
"Macmini5,3": "Mac mini",
|
||||
"Macmini6,1": "Mac mini",
|
||||
"Macmini6,2": "Mac mini",
|
||||
"Macmini7,1": "Mac mini",
|
||||
"Macmini8,1": "Mac mini",
|
||||
"ADP3,2": "Mac mini",
|
||||
"Macmini9,1": "Mac mini",
|
||||
"Mac14,3": "Mac mini",
|
||||
"Mac14,12": "Mac mini",
|
||||
"MacPro1,1*": "Mac Pro",
|
||||
"MacPro2,1": "Mac Pro",
|
||||
"MacPro3,1": "Mac Pro",
|
||||
"MacPro4,1": "Mac Pro",
|
||||
"MacPro5,1": "Mac Pro",
|
||||
"MacPro6,1": "Mac Pro",
|
||||
"MacPro7,1": "Mac Pro",
|
||||
"N/A*": "Power Macintosh",
|
||||
"PowerMac1,1": "Power Macintosh",
|
||||
"PowerMac3,1": "Power Macintosh",
|
||||
"PowerMac3,3": "Power Macintosh",
|
||||
"PowerMac3,4": "Power Macintosh",
|
||||
"PowerMac3,5": "Power Macintosh",
|
||||
"PowerMac3,6": "Power Macintosh",
|
||||
"Mac13,1": "Mac Studio",
|
||||
"Mac13,2": "Mac Studio",
|
||||
"MacBook1,1": "MacBook",
|
||||
"MacBook2,1": "MacBook",
|
||||
"MacBook3,1": "MacBook",
|
||||
"MacBook4,1": "MacBook",
|
||||
"MacBook5,1": "MacBook",
|
||||
"MacBook5,2": "MacBook",
|
||||
"MacBook6,1": "MacBook",
|
||||
"MacBook7,1": "MacBook",
|
||||
"MacBook8,1": "MacBook",
|
||||
"MacBook9,1": "MacBook",
|
||||
"MacBook10,1": "MacBook",
|
||||
"MacBookAir1,1": "MacBook Air",
|
||||
"MacBookAir2,1": "MacBook Air",
|
||||
"MacBookAir3,1": "MacBook Air",
|
||||
"MacBookAir3,2": "MacBook Air",
|
||||
"MacBookAir4,1": "MacBook Air",
|
||||
"MacBookAir4,2": "MacBook Air",
|
||||
"MacBookAir5,1": "MacBook Air",
|
||||
"MacBookAir5,2": "MacBook Air",
|
||||
"MacBookAir6,1": "MacBook Air",
|
||||
"MacBookAir6,2": "MacBook Air",
|
||||
"MacBookAir7,1": "MacBook Air",
|
||||
"MacBookAir7,2": "MacBook Air",
|
||||
"MacBookAir8,1": "MacBook Air",
|
||||
"MacBookAir8,2": "MacBook Air",
|
||||
"MacBookAir9,1": "MacBook Air",
|
||||
"MacBookAir10,1": "MacBook Air",
|
||||
"Mac14,2": "MacBook Air",
|
||||
"MacBookPro1,1": "MacBook Pro",
|
||||
"MacBookPro1,2": "MacBook Pro",
|
||||
"MacBookPro2,2": "MacBook Pro",
|
||||
"MacBookPro2,1": "MacBook Pro",
|
||||
"MacBookPro3,1": "MacBook Pro",
|
||||
"MacBookPro4,1": "MacBook Pro",
|
||||
"MacBookPro5,1": "MacBook Pro",
|
||||
"MacBookPro5,2": "MacBook Pro",
|
||||
"MacBookPro5,5": "MacBook Pro",
|
||||
"MacBookPro5,4": "MacBook Pro",
|
||||
"MacBookPro5,3": "MacBook Pro",
|
||||
"MacBookPro7,1": "MacBook Pro",
|
||||
"MacBookPro6,2": "MacBook Pro",
|
||||
"MacBookPro6,1": "MacBook Pro",
|
||||
"MacBookPro8,1": "MacBook Pro",
|
||||
"MacBookPro8,2": "MacBook Pro",
|
||||
"MacBookPro8,3": "MacBook Pro",
|
||||
"MacBookPro9,2": "MacBook Pro",
|
||||
"MacBookPro9,1": "MacBook Pro",
|
||||
"MacBookPro10,1": "MacBook Pro",
|
||||
"MacBookPro10,2": "MacBook Pro",
|
||||
"MacBookPro11,1": "MacBook Pro",
|
||||
"MacBookPro11,2": "MacBook Pro",
|
||||
"MacBookPro11,3": "MacBook Pro",
|
||||
"MacBookPro12,1": "MacBook Pro",
|
||||
"MacBookPro11,4": "MacBook Pro",
|
||||
"MacBookPro11,5": "MacBook Pro",
|
||||
"MacBookPro13,1": "MacBook Pro",
|
||||
"MacBookPro13,2": "MacBook Pro",
|
||||
"MacBookPro13,3": "MacBook Pro",
|
||||
"MacBookPro14,1": "MacBook Pro",
|
||||
"MacBookPro14,2": "MacBook Pro",
|
||||
"MacBookPro14,3": "MacBook Pro",
|
||||
"MacBookPro15,2": "MacBook Pro",
|
||||
"MacBookPro15,1": "MacBook Pro",
|
||||
"MacBookPro15,3": "MacBook Pro",
|
||||
"MacBookPro15,4": "MacBook Pro",
|
||||
"MacBookPro16,1": "MacBook Pro",
|
||||
"MacBookPro16,3": "MacBook Pro",
|
||||
"MacBookPro16,2": "MacBook Pro",
|
||||
"MacBookPro16,4": "MacBook Pro",
|
||||
"MacBookPro17,1": "MacBook Pro",
|
||||
"MacBookPro18,3": "MacBook Pro",
|
||||
"MacBookPro18,4": "MacBook Pro",
|
||||
"MacBookPro18,1": "MacBook Pro",
|
||||
"MacBookPro18,2": "MacBook Pro",
|
||||
"Mac14,7": "MacBook Pro",
|
||||
"Mac14,9": "MacBook Pro",
|
||||
"Mac14,5": "MacBook Pro",
|
||||
"Mac14,10": "MacBook Pro",
|
||||
"Mac14,6": "MacBook Pro",
|
||||
"PowerMac1,2": "Power Macintosh",
|
||||
"PowerMac5,1": "Power Macintosh",
|
||||
"PowerMac7,2": "Power Macintosh",
|
||||
"PowerMac7,3": "Power Macintosh",
|
||||
"PowerMac9,1": "Power Macintosh",
|
||||
"PowerMac11,2": "Power Macintosh",
|
||||
"PowerBook1,1": "PowerBook",
|
||||
"PowerBook3,1": "PowerBook",
|
||||
"PowerBook3,2": "PowerBook",
|
||||
"PowerBook3,3": "PowerBook",
|
||||
"PowerBook3,4": "PowerBook",
|
||||
"PowerBook3,5": "PowerBook",
|
||||
"PowerBook6,1": "PowerBook",
|
||||
"PowerBook5,1": "PowerBook",
|
||||
"PowerBook6,2": "PowerBook",
|
||||
"PowerBook5,2": "PowerBook",
|
||||
"PowerBook5,3": "PowerBook",
|
||||
"PowerBook6,4": "PowerBook",
|
||||
"PowerBook5,4": "PowerBook",
|
||||
"PowerBook5,5": "PowerBook",
|
||||
"PowerBook6,8": "PowerBook",
|
||||
"PowerBook5,6": "PowerBook",
|
||||
"PowerBook5,7": "PowerBook",
|
||||
"PowerBook5,8": "PowerBook",
|
||||
"PowerBook5,9": "PowerBook",
|
||||
"RackMac1,1": "Xserve",
|
||||
"RackMac1,2": "Xserve",
|
||||
"RackMac3,1": "Xserve",
|
||||
"Xserve1,1": "Xserve",
|
||||
"Xserve2,1": "Xserve",
|
||||
"Xserve3,1": "Xserve"
|
||||
}
|
||||
@@ -16,6 +16,24 @@ if (!dev) {
|
||||
}
|
||||
export const names = JSON.parse(readFileSync(file, "utf-8"));
|
||||
|
||||
// Load iOS and Mac model mappings
|
||||
let iosModelsFile: string;
|
||||
let macModelsFile: string;
|
||||
if (!dev) {
|
||||
iosModelsFile = join(__DIRNAME, "ios_models.json");
|
||||
macModelsFile = join(__DIRNAME, "mac_models.json");
|
||||
} else {
|
||||
iosModelsFile = join("server/db/ios_models.json");
|
||||
macModelsFile = join("server/db/mac_models.json");
|
||||
}
|
||||
|
||||
const iosModels: Record<string, string> = JSON.parse(
|
||||
readFileSync(iosModelsFile, "utf-8")
|
||||
);
|
||||
const macModels: Record<string, string> = JSON.parse(
|
||||
readFileSync(macModelsFile, "utf-8")
|
||||
);
|
||||
|
||||
export async function getUniqueClientName(orgId: string): Promise<string> {
|
||||
let loops = 0;
|
||||
while (true) {
|
||||
@@ -159,3 +177,29 @@ export function generateName(): string {
|
||||
// clean out any non-alphanumeric characters except for dashes
|
||||
return name.replace(/[^a-z0-9-]/g, "");
|
||||
}
|
||||
|
||||
export function getMacDeviceName(macIdentifier?: string | null): string | null {
|
||||
if (macIdentifier && macModels[macIdentifier]) {
|
||||
return macModels[macIdentifier];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getIosDeviceName(iosIdentifier?: string | null): string | null {
|
||||
if (iosIdentifier && iosModels[iosIdentifier]) {
|
||||
return iosModels[iosIdentifier];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getUserDeviceName(
|
||||
model: string | null,
|
||||
fallBack: string | null
|
||||
): string {
|
||||
return (
|
||||
getMacDeviceName(model) ||
|
||||
getIosDeviceName(model) ||
|
||||
fallBack ||
|
||||
"Unknown Device"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,15 @@ import {
|
||||
index
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { InferSelectModel } from "drizzle-orm";
|
||||
import { domains, orgs, targets, users, exitNodes, sessions } from "./schema";
|
||||
import {
|
||||
domains,
|
||||
orgs,
|
||||
targets,
|
||||
users,
|
||||
exitNodes,
|
||||
sessions,
|
||||
clients
|
||||
} from "./schema";
|
||||
|
||||
export const certificates = pgTable("certificates", {
|
||||
certId: serial("certId").primaryKey(),
|
||||
@@ -289,6 +297,33 @@ export const accessAuditLog = pgTable(
|
||||
]
|
||||
);
|
||||
|
||||
export const approvals = pgTable("approvals", {
|
||||
approvalId: serial("approvalId").primaryKey(),
|
||||
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
clientId: integer("clientId").references(() => clients.clientId, {
|
||||
onDelete: "cascade"
|
||||
}), // clients reference user devices (in this case)
|
||||
userId: varchar("userId")
|
||||
.references(() => users.userId, {
|
||||
// optionally tied to a user and in this case delete when the user deletes
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
decision: varchar("decision")
|
||||
.$type<"approved" | "denied" | "pending">()
|
||||
.default("pending")
|
||||
.notNull(),
|
||||
type: varchar("type")
|
||||
.$type<"user_device" /*| 'proxy' // for later */>()
|
||||
.notNull()
|
||||
});
|
||||
|
||||
export type Approval = InferSelectModel<typeof approvals>;
|
||||
export type Limit = InferSelectModel<typeof limits>;
|
||||
export type Account = InferSelectModel<typeof account>;
|
||||
export type Certificate = InferSelectModel<typeof certificates>;
|
||||
|
||||
@@ -134,13 +134,15 @@ export const resources = pgTable("resources", {
|
||||
proxyProtocol: boolean("proxyProtocol").notNull().default(false),
|
||||
proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
|
||||
|
||||
maintenanceModeEnabled: boolean("maintenanceModeEnabled").notNull().default(false),
|
||||
maintenanceModeEnabled: boolean("maintenanceModeEnabled")
|
||||
.notNull()
|
||||
.default(false),
|
||||
maintenanceModeType: text("maintenanceModeType", {
|
||||
enum: ["forced", "automatic"]
|
||||
}).default("forced"), // "forced" = always show, "automatic" = only when down
|
||||
maintenanceTitle: text("maintenanceTitle"),
|
||||
maintenanceMessage: text("maintenanceMessage"),
|
||||
maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
|
||||
maintenanceEstimatedTime: text("maintenanceEstimatedTime")
|
||||
});
|
||||
|
||||
export const targets = pgTable("targets", {
|
||||
@@ -223,8 +225,8 @@ export const siteResources = pgTable("siteResources", {
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
alias: varchar("alias"),
|
||||
aliasAddress: varchar("aliasAddress"),
|
||||
tcpPortRangeString: varchar("tcpPortRangeString"),
|
||||
udpPortRangeString: varchar("udpPortRangeString"),
|
||||
tcpPortRangeString: varchar("tcpPortRangeString").notNull().default("*"),
|
||||
udpPortRangeString: varchar("udpPortRangeString").notNull().default("*"),
|
||||
disableIcmp: boolean("disableIcmp").notNull().default(false)
|
||||
});
|
||||
|
||||
@@ -363,7 +365,8 @@ export const roles = pgTable("roles", {
|
||||
.notNull(),
|
||||
isAdmin: boolean("isAdmin"),
|
||||
name: varchar("name").notNull(),
|
||||
description: varchar("description")
|
||||
description: varchar("description"),
|
||||
requireDeviceApproval: boolean("requireDeviceApproval").default(false)
|
||||
});
|
||||
|
||||
export const roleActions = pgTable("roleActions", {
|
||||
@@ -464,13 +467,22 @@ export const resourceHeaderAuth = pgTable("resourceHeaderAuth", {
|
||||
headerAuthHash: varchar("headerAuthHash").notNull()
|
||||
});
|
||||
|
||||
export const resourceHeaderAuthExtendedCompatibility = pgTable("resourceHeaderAuthExtendedCompatibility", {
|
||||
headerAuthExtendedCompatibilityId: serial("headerAuthExtendedCompatibilityId").primaryKey(),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
extendedCompatibilityIsActivated: boolean("extendedCompatibilityIsActivated").notNull().default(true),
|
||||
});
|
||||
export const resourceHeaderAuthExtendedCompatibility = pgTable(
|
||||
"resourceHeaderAuthExtendedCompatibility",
|
||||
{
|
||||
headerAuthExtendedCompatibilityId: serial(
|
||||
"headerAuthExtendedCompatibilityId"
|
||||
).primaryKey(),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
extendedCompatibilityIsActivated: boolean(
|
||||
"extendedCompatibilityIsActivated"
|
||||
)
|
||||
.notNull()
|
||||
.default(true)
|
||||
}
|
||||
);
|
||||
|
||||
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
||||
accessTokenId: varchar("accessTokenId").primaryKey(),
|
||||
@@ -580,7 +592,8 @@ export const idp = pgTable("idp", {
|
||||
type: varchar("type").notNull(),
|
||||
defaultRoleMapping: varchar("defaultRoleMapping"),
|
||||
defaultOrgMapping: varchar("defaultOrgMapping"),
|
||||
autoProvision: boolean("autoProvision").notNull().default(false)
|
||||
autoProvision: boolean("autoProvision").notNull().default(false),
|
||||
tags: text("tags")
|
||||
});
|
||||
|
||||
export const idpOidcConfig = pgTable("idpOidcConfig", {
|
||||
@@ -677,7 +690,12 @@ export const clients = pgTable("clients", {
|
||||
online: boolean("online").notNull().default(false),
|
||||
// endpoint: varchar("endpoint"),
|
||||
lastHolePunch: integer("lastHolePunch"),
|
||||
maxConnections: integer("maxConnections")
|
||||
maxConnections: integer("maxConnections"),
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
blocked: boolean("blocked").notNull().default(false),
|
||||
approvalState: varchar("approvalState").$type<
|
||||
"pending" | "approved" | "denied"
|
||||
>()
|
||||
});
|
||||
|
||||
export const clientSitesAssociationsCache = pgTable(
|
||||
@@ -701,6 +719,49 @@ export const clientSiteResourcesAssociationsCache = pgTable(
|
||||
}
|
||||
);
|
||||
|
||||
export const clientPostureSnapshots = pgTable("clientPostureSnapshots", {
|
||||
snapshotId: serial("snapshotId").primaryKey(),
|
||||
|
||||
clientId: integer("clientId").references(() => clients.clientId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
|
||||
// Platform-agnostic checks
|
||||
|
||||
biometricsEnabled: boolean("biometricsEnabled").notNull().default(false),
|
||||
diskEncrypted: boolean("diskEncrypted").notNull().default(false),
|
||||
firewallEnabled: boolean("firewallEnabled").notNull().default(false),
|
||||
autoUpdatesEnabled: boolean("autoUpdatesEnabled").notNull().default(false),
|
||||
tpmAvailable: boolean("tpmAvailable").notNull().default(false),
|
||||
|
||||
// Windows-specific posture check information
|
||||
|
||||
windowsDefenderEnabled: boolean("windowsDefenderEnabled")
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
// macOS-specific posture check information
|
||||
|
||||
macosSipEnabled: boolean("macosSipEnabled").notNull().default(false),
|
||||
macosGatekeeperEnabled: boolean("macosGatekeeperEnabled")
|
||||
.notNull()
|
||||
.default(false),
|
||||
macosFirewallStealthMode: boolean("macosFirewallStealthMode")
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
// Linux-specific posture check information
|
||||
|
||||
linuxAppArmorEnabled: boolean("linuxAppArmorEnabled")
|
||||
.notNull()
|
||||
.default(false),
|
||||
linuxSELinuxEnabled: boolean("linuxSELinuxEnabled")
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
collectedAt: integer("collectedAt").notNull()
|
||||
});
|
||||
|
||||
export const olms = pgTable("olms", {
|
||||
olmId: varchar("id").primaryKey(),
|
||||
secretHash: varchar("secretHash").notNull(),
|
||||
@@ -715,7 +776,29 @@ export const olms = pgTable("olms", {
|
||||
userId: text("userId").references(() => users.userId, {
|
||||
// optionally tied to a user and in this case delete when the user deletes
|
||||
onDelete: "cascade"
|
||||
})
|
||||
}),
|
||||
archived: boolean("archived").notNull().default(false)
|
||||
});
|
||||
|
||||
export const fingerprints = pgTable("fingerprints", {
|
||||
fingerprintId: serial("id").primaryKey(),
|
||||
|
||||
olmId: text("olmId")
|
||||
.references(() => olms.olmId, { onDelete: "cascade" })
|
||||
.notNull(),
|
||||
|
||||
firstSeen: integer("firstSeen").notNull(),
|
||||
lastSeen: integer("lastSeen").notNull(),
|
||||
|
||||
username: text("username"),
|
||||
hostname: text("hostname"),
|
||||
platform: text("platform"), // macos | windows | linux | ios | android | unknown
|
||||
osVersion: text("osVersion"),
|
||||
kernelVersion: text("kernelVersion"),
|
||||
arch: text("arch"),
|
||||
deviceModel: text("deviceModel"),
|
||||
serialNumber: text("serialNumber"),
|
||||
platformFingerprint: varchar("platformFingerprint")
|
||||
});
|
||||
|
||||
export const olmSessions = pgTable("clientSession", {
|
||||
@@ -872,7 +955,9 @@ export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
||||
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
||||
export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel<typeof resourceHeaderAuthExtendedCompatibility>;
|
||||
export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel<
|
||||
typeof resourceHeaderAuthExtendedCompatibility
|
||||
>;
|
||||
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
||||
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
||||
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import {
|
||||
db, loginPage, LoginPage, loginPageOrg, Org, orgs,
|
||||
} from "@server/db";
|
||||
import { db, loginPage, LoginPage, loginPageOrg, Org, orgs, roles } from "@server/db";
|
||||
import {
|
||||
Resource,
|
||||
ResourcePassword,
|
||||
@@ -27,7 +25,7 @@ export type ResourceWithAuth = {
|
||||
pincode: ResourcePincode | null;
|
||||
password: ResourcePassword | null;
|
||||
headerAuth: ResourceHeaderAuth | null;
|
||||
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null
|
||||
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
||||
org: Org;
|
||||
};
|
||||
|
||||
@@ -59,12 +57,12 @@ export async function getResourceByDomain(
|
||||
)
|
||||
.leftJoin(
|
||||
resourceHeaderAuthExtendedCompatibility,
|
||||
eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId)
|
||||
)
|
||||
.innerJoin(
|
||||
orgs,
|
||||
eq(orgs.orgId, resources.orgId)
|
||||
eq(
|
||||
resourceHeaderAuthExtendedCompatibility.resourceId,
|
||||
resources.resourceId
|
||||
)
|
||||
)
|
||||
.innerJoin(orgs, eq(orgs.orgId, resources.orgId))
|
||||
.where(eq(resources.fullDomain, domain))
|
||||
.limit(1);
|
||||
|
||||
@@ -77,7 +75,8 @@ export async function getResourceByDomain(
|
||||
pincode: result.resourcePincode,
|
||||
password: result.resourcePassword,
|
||||
headerAuth: result.resourceHeaderAuth,
|
||||
headerAuthExtendedCompatibility: result.resourceHeaderAuthExtendedCompatibility,
|
||||
headerAuthExtendedCompatibility:
|
||||
result.resourceHeaderAuthExtendedCompatibility,
|
||||
org: result.orgs
|
||||
};
|
||||
}
|
||||
@@ -109,9 +108,17 @@ export async function getUserSessionWithUser(
|
||||
*/
|
||||
export async function getUserOrgRole(userId: string, orgId: string) {
|
||||
const userOrgRole = await db
|
||||
.select()
|
||||
.select({
|
||||
userId: userOrgs.userId,
|
||||
orgId: userOrgs.orgId,
|
||||
roleId: userOrgs.roleId,
|
||||
isOwner: userOrgs.isOwner,
|
||||
autoProvisioned: userOrgs.autoProvisioned,
|
||||
roleName: roles.name
|
||||
})
|
||||
.from(userOrgs)
|
||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
||||
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
|
||||
.limit(1);
|
||||
|
||||
return userOrgRole.length > 0 ? userOrgRole[0] : null;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
sqliteTable,
|
||||
text
|
||||
} from "drizzle-orm/sqlite-core";
|
||||
import { domains, exitNodes, orgs, sessions, users } from "./schema";
|
||||
import { clients, domains, exitNodes, orgs, sessions, users } from "./schema";
|
||||
|
||||
export const certificates = sqliteTable("certificates", {
|
||||
certId: integer("certId").primaryKey({ autoIncrement: true }),
|
||||
@@ -289,6 +289,31 @@ export const accessAuditLog = sqliteTable(
|
||||
]
|
||||
);
|
||||
|
||||
export const approvals = sqliteTable("approvals", {
|
||||
approvalId: integer("approvalId").primaryKey({ autoIncrement: true }),
|
||||
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
|
||||
orgId: text("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
clientId: integer("clientId").references(() => clients.clientId, {
|
||||
onDelete: "cascade"
|
||||
}), // olms reference user devices clients
|
||||
userId: text("userId").references(() => users.userId, {
|
||||
// optionally tied to a user and in this case delete when the user deletes
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
decision: text("decision")
|
||||
.$type<"approved" | "denied" | "pending">()
|
||||
.default("pending")
|
||||
.notNull(),
|
||||
type: text("type")
|
||||
.$type<"user_device" /*| 'proxy' // for later */>()
|
||||
.notNull()
|
||||
});
|
||||
|
||||
export type Approval = InferSelectModel<typeof approvals>;
|
||||
export type Limit = InferSelectModel<typeof limits>;
|
||||
export type Account = InferSelectModel<typeof account>;
|
||||
export type Certificate = InferSelectModel<typeof certificates>;
|
||||
|
||||
@@ -12,22 +12,22 @@ import { no } from "zod/v4/locales";
|
||||
export const domains = sqliteTable("domains", {
|
||||
domainId: text("domainId").primaryKey(),
|
||||
baseDomain: text("baseDomain").notNull(),
|
||||
configManaged: integer("configManaged", {mode: "boolean"})
|
||||
configManaged: integer("configManaged", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
type: text("type"), // "ns", "cname", "wildcard"
|
||||
verified: integer("verified", {mode: "boolean"}).notNull().default(false),
|
||||
failed: integer("failed", {mode: "boolean"}).notNull().default(false),
|
||||
verified: integer("verified", { mode: "boolean" }).notNull().default(false),
|
||||
failed: integer("failed", { mode: "boolean" }).notNull().default(false),
|
||||
tries: integer("tries").notNull().default(0),
|
||||
certResolver: text("certResolver"),
|
||||
preferWildcardCert: integer("preferWildcardCert", {mode: "boolean"})
|
||||
preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" })
|
||||
});
|
||||
|
||||
export const dnsRecords = sqliteTable("dnsRecords", {
|
||||
id: integer("id").primaryKey({autoIncrement: true}),
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
domainId: text("domainId")
|
||||
.notNull()
|
||||
.references(() => domains.domainId, {onDelete: "cascade"}),
|
||||
.references(() => domains.domainId, { onDelete: "cascade" }),
|
||||
|
||||
recordType: text("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT"
|
||||
baseDomain: text("baseDomain"),
|
||||
@@ -41,7 +41,7 @@ export const orgs = sqliteTable("orgs", {
|
||||
subnet: text("subnet"),
|
||||
utilitySubnet: text("utilitySubnet"), // this is the subnet for utility addresses
|
||||
createdAt: text("createdAt"),
|
||||
requireTwoFactor: integer("requireTwoFactor", {mode: "boolean"}),
|
||||
requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" }),
|
||||
maxSessionLengthHours: integer("maxSessionLengthHours"), // hours
|
||||
passwordExpiryDays: integer("passwordExpiryDays"), // days
|
||||
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||
@@ -58,23 +58,23 @@ export const orgs = sqliteTable("orgs", {
|
||||
export const userDomains = sqliteTable("userDomains", {
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, {onDelete: "cascade"}),
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
domainId: text("domainId")
|
||||
.notNull()
|
||||
.references(() => domains.domainId, {onDelete: "cascade"})
|
||||
.references(() => domains.domainId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const orgDomains = sqliteTable("orgDomains", {
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, {onDelete: "cascade"}),
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
domainId: text("domainId")
|
||||
.notNull()
|
||||
.references(() => domains.domainId, {onDelete: "cascade"})
|
||||
.references(() => domains.domainId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const sites = sqliteTable("sites", {
|
||||
siteId: integer("siteId").primaryKey({autoIncrement: true}),
|
||||
siteId: integer("siteId").primaryKey({ autoIncrement: true }),
|
||||
orgId: text("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
@@ -91,7 +91,7 @@ export const sites = sqliteTable("sites", {
|
||||
megabytesOut: integer("bytesOut").default(0),
|
||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||
type: text("type").notNull(), // "newt" or "wireguard"
|
||||
online: integer("online", {mode: "boolean"}).notNull().default(false),
|
||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||
|
||||
// exit node stuff that is how to connect to the site when it has a wg server
|
||||
address: text("address"), // this is the address of the wireguard interface in newt
|
||||
@@ -99,14 +99,14 @@ export const sites = sqliteTable("sites", {
|
||||
publicKey: text("publicKey"), // TODO: Fix typo in publicKey
|
||||
lastHolePunch: integer("lastHolePunch"),
|
||||
listenPort: integer("listenPort"),
|
||||
dockerSocketEnabled: integer("dockerSocketEnabled", {mode: "boolean"})
|
||||
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(true)
|
||||
});
|
||||
|
||||
export const resources = sqliteTable("resources", {
|
||||
resourceId: integer("resourceId").primaryKey({autoIncrement: true}),
|
||||
resourceGuid: text("resourceGuid", {length: 36})
|
||||
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
|
||||
resourceGuid: text("resourceGuid", { length: 36 })
|
||||
.unique()
|
||||
.notNull()
|
||||
.$defaultFn(() => randomUUID()),
|
||||
@@ -122,35 +122,39 @@ export const resources = sqliteTable("resources", {
|
||||
domainId: text("domainId").references(() => domains.domainId, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
ssl: integer("ssl", {mode: "boolean"}).notNull().default(false),
|
||||
blockAccess: integer("blockAccess", {mode: "boolean"})
|
||||
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
|
||||
blockAccess: integer("blockAccess", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
sso: integer("sso", {mode: "boolean"}).notNull().default(true),
|
||||
http: integer("http", {mode: "boolean"}).notNull().default(true),
|
||||
sso: integer("sso", { mode: "boolean" }).notNull().default(true),
|
||||
http: integer("http", { mode: "boolean" }).notNull().default(true),
|
||||
protocol: text("protocol").notNull(),
|
||||
proxyPort: integer("proxyPort"),
|
||||
emailWhitelistEnabled: integer("emailWhitelistEnabled", {mode: "boolean"})
|
||||
emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
applyRules: integer("applyRules", {mode: "boolean"})
|
||||
applyRules: integer("applyRules", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
enabled: integer("enabled", {mode: "boolean"}).notNull().default(true),
|
||||
stickySession: integer("stickySession", {mode: "boolean"})
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
stickySession: integer("stickySession", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
tlsServerName: text("tlsServerName"),
|
||||
setHostHeader: text("setHostHeader"),
|
||||
enableProxy: integer("enableProxy", {mode: "boolean"}).default(true),
|
||||
enableProxy: integer("enableProxy", { mode: "boolean" }).default(true),
|
||||
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
headers: text("headers"), // comma-separated list of headers to add to the request
|
||||
proxyProtocol: integer("proxyProtocol", { mode: "boolean" }).notNull().default(false),
|
||||
proxyProtocol: integer("proxyProtocol", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
|
||||
|
||||
maintenanceModeEnabled: integer("maintenanceModeEnabled", { mode: "boolean" })
|
||||
maintenanceModeEnabled: integer("maintenanceModeEnabled", {
|
||||
mode: "boolean"
|
||||
})
|
||||
.notNull()
|
||||
.default(false),
|
||||
maintenanceModeType: text("maintenanceModeType", {
|
||||
@@ -158,12 +162,11 @@ export const resources = sqliteTable("resources", {
|
||||
}).default("forced"), // "forced" = always show, "automatic" = only when down
|
||||
maintenanceTitle: text("maintenanceTitle"),
|
||||
maintenanceMessage: text("maintenanceMessage"),
|
||||
maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
|
||||
|
||||
maintenanceEstimatedTime: text("maintenanceEstimatedTime")
|
||||
});
|
||||
|
||||
export const targets = sqliteTable("targets", {
|
||||
targetId: integer("targetId").primaryKey({autoIncrement: true}),
|
||||
targetId: integer("targetId").primaryKey({ autoIncrement: true }),
|
||||
resourceId: integer("resourceId")
|
||||
.references(() => resources.resourceId, {
|
||||
onDelete: "cascade"
|
||||
@@ -178,7 +181,7 @@ export const targets = sqliteTable("targets", {
|
||||
method: text("method"),
|
||||
port: integer("port").notNull(),
|
||||
internalPort: integer("internalPort"),
|
||||
enabled: integer("enabled", {mode: "boolean"}).notNull().default(true),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
path: text("path"),
|
||||
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
||||
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
|
||||
@@ -192,8 +195,8 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
||||
}),
|
||||
targetId: integer("targetId")
|
||||
.notNull()
|
||||
.references(() => targets.targetId, {onDelete: "cascade"}),
|
||||
hcEnabled: integer("hcEnabled", {mode: "boolean"})
|
||||
.references(() => targets.targetId, { onDelete: "cascade" }),
|
||||
hcEnabled: integer("hcEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
hcPath: text("hcPath"),
|
||||
@@ -215,7 +218,7 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
||||
});
|
||||
|
||||
export const exitNodes = sqliteTable("exitNodes", {
|
||||
exitNodeId: integer("exitNodeId").primaryKey({autoIncrement: true}),
|
||||
exitNodeId: integer("exitNodeId").primaryKey({ autoIncrement: true }),
|
||||
name: text("name").notNull(),
|
||||
address: text("address").notNull(), // this is the address of the wireguard interface in gerbil
|
||||
endpoint: text("endpoint").notNull(), // this is how to reach gerbil externally - gets put into the wireguard config
|
||||
@@ -223,7 +226,7 @@ export const exitNodes = sqliteTable("exitNodes", {
|
||||
listenPort: integer("listenPort").notNull(),
|
||||
reachableAt: text("reachableAt"), // this is the internal address of the gerbil http server for command control
|
||||
maxConnections: integer("maxConnections"),
|
||||
online: integer("online", {mode: "boolean"}).notNull().default(false),
|
||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||
lastPing: integer("lastPing"),
|
||||
type: text("type").default("gerbil"), // gerbil, remoteExitNode
|
||||
region: text("region")
|
||||
@@ -236,10 +239,10 @@ export const siteResources = sqliteTable("siteResources", {
|
||||
}),
|
||||
siteId: integer("siteId")
|
||||
.notNull()
|
||||
.references(() => sites.siteId, {onDelete: "cascade"}),
|
||||
.references(() => sites.siteId, { onDelete: "cascade" }),
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, {onDelete: "cascade"}),
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
niceId: text("niceId").notNull(),
|
||||
name: text("name").notNull(),
|
||||
mode: text("mode").notNull(), // "host" | "cidr" | "port"
|
||||
@@ -250,9 +253,11 @@ export const siteResources = sqliteTable("siteResources", {
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
alias: text("alias"),
|
||||
aliasAddress: text("aliasAddress"),
|
||||
tcpPortRangeString: text("tcpPortRangeString"),
|
||||
udpPortRangeString: text("udpPortRangeString"),
|
||||
tcpPortRangeString: text("tcpPortRangeString").notNull().default("*"),
|
||||
udpPortRangeString: text("udpPortRangeString").notNull().default("*"),
|
||||
disableIcmp: integer("disableIcmp", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false)
|
||||
});
|
||||
|
||||
export const clientSiteResources = sqliteTable("clientSiteResources", {
|
||||
@@ -292,20 +297,20 @@ export const users = sqliteTable("user", {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
passwordHash: text("passwordHash"),
|
||||
twoFactorEnabled: integer("twoFactorEnabled", {mode: "boolean"})
|
||||
twoFactorEnabled: integer("twoFactorEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
twoFactorSetupRequested: integer("twoFactorSetupRequested", {
|
||||
mode: "boolean"
|
||||
}).default(false),
|
||||
twoFactorSecret: text("twoFactorSecret"),
|
||||
emailVerified: integer("emailVerified", {mode: "boolean"})
|
||||
emailVerified: integer("emailVerified", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
dateCreated: text("dateCreated").notNull(),
|
||||
termsAcceptedTimestamp: text("termsAcceptedTimestamp"),
|
||||
termsVersion: text("termsVersion"),
|
||||
serverAdmin: integer("serverAdmin", {mode: "boolean"})
|
||||
serverAdmin: integer("serverAdmin", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
lastPasswordChange: integer("lastPasswordChange")
|
||||
@@ -339,7 +344,7 @@ export const webauthnChallenge = sqliteTable("webauthnChallenge", {
|
||||
export const setupTokens = sqliteTable("setupTokens", {
|
||||
tokenId: text("tokenId").primaryKey(),
|
||||
token: text("token").notNull(),
|
||||
used: integer("used", {mode: "boolean"}).notNull().default(false),
|
||||
used: integer("used", { mode: "boolean" }).notNull().default(false),
|
||||
dateCreated: text("dateCreated").notNull(),
|
||||
dateUsed: text("dateUsed")
|
||||
});
|
||||
@@ -378,9 +383,14 @@ export const clients = sqliteTable("clients", {
|
||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||
lastPing: integer("lastPing"),
|
||||
type: text("type").notNull(), // "olm"
|
||||
online: integer("online", {mode: "boolean"}).notNull().default(false),
|
||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||
// endpoint: text("endpoint"),
|
||||
lastHolePunch: integer("lastHolePunch")
|
||||
lastHolePunch: integer("lastHolePunch"),
|
||||
archived: integer("archived", { mode: "boolean" }).notNull().default(false),
|
||||
blocked: integer("blocked", { mode: "boolean" }).notNull().default(false),
|
||||
approvalState: text("approvalState").$type<
|
||||
"pending" | "approved" | "denied"
|
||||
>()
|
||||
});
|
||||
|
||||
export const clientSitesAssociationsCache = sqliteTable(
|
||||
@@ -406,6 +416,69 @@ export const clientSiteResourcesAssociationsCache = sqliteTable(
|
||||
}
|
||||
);
|
||||
|
||||
export const clientPostureSnapshots = sqliteTable("clientPostureSnapshots", {
|
||||
snapshotId: integer("snapshotId").primaryKey({ autoIncrement: true }),
|
||||
|
||||
clientId: integer("clientId").references(() => clients.clientId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
|
||||
// Platform-agnostic checks
|
||||
|
||||
biometricsEnabled: integer("biometricsEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
diskEncrypted: integer("diskEncrypted", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
firewallEnabled: integer("firewallEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
autoUpdatesEnabled: integer("autoUpdatesEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
tpmAvailable: integer("tpmAvailable", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
// Windows-specific posture check information
|
||||
|
||||
windowsDefenderEnabled: integer("windowsDefenderEnabled", {
|
||||
mode: "boolean"
|
||||
})
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
// macOS-specific posture check information
|
||||
|
||||
macosSipEnabled: integer("macosSipEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
macosGatekeeperEnabled: integer("macosGatekeeperEnabled", {
|
||||
mode: "boolean"
|
||||
})
|
||||
.notNull()
|
||||
.default(false),
|
||||
macosFirewallStealthMode: integer("macosFirewallStealthMode", {
|
||||
mode: "boolean"
|
||||
})
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
// Linux-specific posture check information
|
||||
|
||||
linuxAppArmorEnabled: integer("linuxAppArmorEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
linuxSELinuxEnabled: integer("linuxSELinuxEnabled", {
|
||||
mode: "boolean"
|
||||
})
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
collectedAt: integer("collectedAt").notNull()
|
||||
});
|
||||
|
||||
export const olms = sqliteTable("olms", {
|
||||
olmId: text("id").primaryKey(),
|
||||
secretHash: text("secretHash").notNull(),
|
||||
@@ -420,14 +493,36 @@ export const olms = sqliteTable("olms", {
|
||||
userId: text("userId").references(() => users.userId, {
|
||||
// optionally tied to a user and in this case delete when the user deletes
|
||||
onDelete: "cascade"
|
||||
})
|
||||
}),
|
||||
archived: integer("archived", { mode: "boolean" }).notNull().default(false)
|
||||
});
|
||||
|
||||
export const fingerprints = sqliteTable("fingerprints", {
|
||||
fingerprintId: integer("id").primaryKey({ autoIncrement: true }),
|
||||
|
||||
olmId: text("olmId")
|
||||
.references(() => olms.olmId, { onDelete: "cascade" })
|
||||
.notNull(),
|
||||
|
||||
firstSeen: integer("firstSeen").notNull(),
|
||||
lastSeen: integer("lastSeen").notNull(),
|
||||
|
||||
username: text("username"),
|
||||
hostname: text("hostname"),
|
||||
platform: text("platform"), // macos | windows | linux | ios | android | unknown
|
||||
osVersion: text("osVersion"),
|
||||
kernelVersion: text("kernelVersion"),
|
||||
arch: text("arch"),
|
||||
deviceModel: text("deviceModel"),
|
||||
serialNumber: text("serialNumber"),
|
||||
platformFingerprint: text("platformFingerprint")
|
||||
});
|
||||
|
||||
export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", {
|
||||
codeId: integer("id").primaryKey({autoIncrement: true}),
|
||||
codeId: integer("id").primaryKey({ autoIncrement: true }),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, {onDelete: "cascade"}),
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
codeHash: text("codeHash").notNull()
|
||||
});
|
||||
|
||||
@@ -435,7 +530,7 @@ export const sessions = sqliteTable("session", {
|
||||
sessionId: text("id").primaryKey(),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, {onDelete: "cascade"}),
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
expiresAt: integer("expiresAt").notNull(),
|
||||
issuedAt: integer("issuedAt"),
|
||||
deviceAuthUsed: integer("deviceAuthUsed", { mode: "boolean" })
|
||||
@@ -447,7 +542,7 @@ export const newtSessions = sqliteTable("newtSession", {
|
||||
sessionId: text("id").primaryKey(),
|
||||
newtId: text("newtId")
|
||||
.notNull()
|
||||
.references(() => newts.newtId, {onDelete: "cascade"}),
|
||||
.references(() => newts.newtId, { onDelete: "cascade" }),
|
||||
expiresAt: integer("expiresAt").notNull()
|
||||
});
|
||||
|
||||
@@ -455,14 +550,14 @@ export const olmSessions = sqliteTable("clientSession", {
|
||||
sessionId: text("id").primaryKey(),
|
||||
olmId: text("olmId")
|
||||
.notNull()
|
||||
.references(() => olms.olmId, {onDelete: "cascade"}),
|
||||
.references(() => olms.olmId, { onDelete: "cascade" }),
|
||||
expiresAt: integer("expiresAt").notNull()
|
||||
});
|
||||
|
||||
export const userOrgs = sqliteTable("userOrgs", {
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, {onDelete: "cascade"}),
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
orgId: text("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
@@ -471,28 +566,28 @@ export const userOrgs = sqliteTable("userOrgs", {
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId),
|
||||
isOwner: integer("isOwner", {mode: "boolean"}).notNull().default(false),
|
||||
isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false),
|
||||
autoProvisioned: integer("autoProvisioned", {
|
||||
mode: "boolean"
|
||||
}).default(false)
|
||||
});
|
||||
|
||||
export const emailVerificationCodes = sqliteTable("emailVerificationCodes", {
|
||||
codeId: integer("id").primaryKey({autoIncrement: true}),
|
||||
codeId: integer("id").primaryKey({ autoIncrement: true }),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, {onDelete: "cascade"}),
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
email: text("email").notNull(),
|
||||
code: text("code").notNull(),
|
||||
expiresAt: integer("expiresAt").notNull()
|
||||
});
|
||||
|
||||
export const passwordResetTokens = sqliteTable("passwordResetTokens", {
|
||||
tokenId: integer("id").primaryKey({autoIncrement: true}),
|
||||
tokenId: integer("id").primaryKey({ autoIncrement: true }),
|
||||
email: text("email").notNull(),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, {onDelete: "cascade"}),
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
tokenHash: text("tokenHash").notNull(),
|
||||
expiresAt: integer("expiresAt").notNull()
|
||||
});
|
||||
@@ -504,106 +599,109 @@ export const actions = sqliteTable("actions", {
|
||||
});
|
||||
|
||||
export const roles = sqliteTable("roles", {
|
||||
roleId: integer("roleId").primaryKey({autoIncrement: true}),
|
||||
roleId: integer("roleId").primaryKey({ autoIncrement: true }),
|
||||
orgId: text("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
isAdmin: integer("isAdmin", {mode: "boolean"}),
|
||||
isAdmin: integer("isAdmin", { mode: "boolean" }),
|
||||
name: text("name").notNull(),
|
||||
description: text("description")
|
||||
description: text("description"),
|
||||
requireDeviceApproval: integer("requireDeviceApproval", {
|
||||
mode: "boolean"
|
||||
}).default(false)
|
||||
});
|
||||
|
||||
export const roleActions = sqliteTable("roleActions", {
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, {onDelete: "cascade"}),
|
||||
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||
actionId: text("actionId")
|
||||
.notNull()
|
||||
.references(() => actions.actionId, {onDelete: "cascade"}),
|
||||
.references(() => actions.actionId, { onDelete: "cascade" }),
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, {onDelete: "cascade"})
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const userActions = sqliteTable("userActions", {
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, {onDelete: "cascade"}),
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
actionId: text("actionId")
|
||||
.notNull()
|
||||
.references(() => actions.actionId, {onDelete: "cascade"}),
|
||||
.references(() => actions.actionId, { onDelete: "cascade" }),
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, {onDelete: "cascade"})
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const roleSites = sqliteTable("roleSites", {
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, {onDelete: "cascade"}),
|
||||
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||
siteId: integer("siteId")
|
||||
.notNull()
|
||||
.references(() => sites.siteId, {onDelete: "cascade"})
|
||||
.references(() => sites.siteId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const userSites = sqliteTable("userSites", {
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, {onDelete: "cascade"}),
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
siteId: integer("siteId")
|
||||
.notNull()
|
||||
.references(() => sites.siteId, {onDelete: "cascade"})
|
||||
.references(() => sites.siteId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const userClients = sqliteTable("userClients", {
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, {onDelete: "cascade"}),
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
clientId: integer("clientId")
|
||||
.notNull()
|
||||
.references(() => clients.clientId, {onDelete: "cascade"})
|
||||
.references(() => clients.clientId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const roleClients = sqliteTable("roleClients", {
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, {onDelete: "cascade"}),
|
||||
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||
clientId: integer("clientId")
|
||||
.notNull()
|
||||
.references(() => clients.clientId, {onDelete: "cascade"})
|
||||
.references(() => clients.clientId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const roleResources = sqliteTable("roleResources", {
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, {onDelete: "cascade"}),
|
||||
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, {onDelete: "cascade"})
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const userResources = sqliteTable("userResources", {
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, {onDelete: "cascade"}),
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, {onDelete: "cascade"})
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const userInvites = sqliteTable("userInvites", {
|
||||
inviteId: text("inviteId").primaryKey(),
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, {onDelete: "cascade"}),
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
email: text("email").notNull(),
|
||||
expiresAt: integer("expiresAt").notNull(),
|
||||
tokenHash: text("token").notNull(),
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, {onDelete: "cascade"})
|
||||
.references(() => roles.roleId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const resourcePincode = sqliteTable("resourcePincode", {
|
||||
@@ -612,7 +710,7 @@ export const resourcePincode = sqliteTable("resourcePincode", {
|
||||
}),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, {onDelete: "cascade"}),
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
pincodeHash: text("pincodeHash").notNull(),
|
||||
digitLength: integer("digitLength").notNull()
|
||||
});
|
||||
@@ -623,7 +721,7 @@ export const resourcePassword = sqliteTable("resourcePassword", {
|
||||
}),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, {onDelete: "cascade"}),
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
passwordHash: text("passwordHash").notNull()
|
||||
});
|
||||
|
||||
@@ -633,28 +731,38 @@ export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", {
|
||||
}),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, {onDelete: "cascade"}),
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
headerAuthHash: text("headerAuthHash").notNull()
|
||||
});
|
||||
|
||||
export const resourceHeaderAuthExtendedCompatibility = sqliteTable("resourceHeaderAuthExtendedCompatibility", {
|
||||
headerAuthExtendedCompatibilityId: integer("headerAuthExtendedCompatibilityId").primaryKey({
|
||||
autoIncrement: true
|
||||
}),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, {onDelete: "cascade"}),
|
||||
extendedCompatibilityIsActivated: integer("extendedCompatibilityIsActivated", {mode: "boolean"}).notNull().default(true)
|
||||
});
|
||||
export const resourceHeaderAuthExtendedCompatibility = sqliteTable(
|
||||
"resourceHeaderAuthExtendedCompatibility",
|
||||
{
|
||||
headerAuthExtendedCompatibilityId: integer(
|
||||
"headerAuthExtendedCompatibilityId"
|
||||
).primaryKey({
|
||||
autoIncrement: true
|
||||
}),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
extendedCompatibilityIsActivated: integer(
|
||||
"extendedCompatibilityIsActivated",
|
||||
{ mode: "boolean" }
|
||||
)
|
||||
.notNull()
|
||||
.default(true)
|
||||
}
|
||||
);
|
||||
|
||||
export const resourceAccessToken = sqliteTable("resourceAccessToken", {
|
||||
accessTokenId: text("accessTokenId").primaryKey(),
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, {onDelete: "cascade"}),
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, {onDelete: "cascade"}),
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
tokenHash: text("tokenHash").notNull(),
|
||||
sessionLength: integer("sessionLength").notNull(),
|
||||
expiresAt: integer("expiresAt"),
|
||||
@@ -667,13 +775,13 @@ export const resourceSessions = sqliteTable("resourceSessions", {
|
||||
sessionId: text("id").primaryKey(),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, {onDelete: "cascade"}),
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
expiresAt: integer("expiresAt").notNull(),
|
||||
sessionLength: integer("sessionLength").notNull(),
|
||||
doNotExtend: integer("doNotExtend", {mode: "boolean"})
|
||||
doNotExtend: integer("doNotExtend", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
isRequestToken: integer("isRequestToken", {mode: "boolean"}),
|
||||
isRequestToken: integer("isRequestToken", { mode: "boolean" }),
|
||||
userSessionId: text("userSessionId").references(() => sessions.sessionId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
@@ -705,11 +813,11 @@ export const resourceSessions = sqliteTable("resourceSessions", {
|
||||
});
|
||||
|
||||
export const resourceWhitelist = sqliteTable("resourceWhitelist", {
|
||||
whitelistId: integer("id").primaryKey({autoIncrement: true}),
|
||||
whitelistId: integer("id").primaryKey({ autoIncrement: true }),
|
||||
email: text("email").notNull(),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, {onDelete: "cascade"})
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const resourceOtp = sqliteTable("resourceOtp", {
|
||||
@@ -718,7 +826,7 @@ export const resourceOtp = sqliteTable("resourceOtp", {
|
||||
}),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, {onDelete: "cascade"}),
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
email: text("email").notNull(),
|
||||
otpHash: text("otpHash").notNull(),
|
||||
expiresAt: integer("expiresAt").notNull()
|
||||
@@ -730,11 +838,11 @@ export const versionMigrations = sqliteTable("versionMigrations", {
|
||||
});
|
||||
|
||||
export const resourceRules = sqliteTable("resourceRules", {
|
||||
ruleId: integer("ruleId").primaryKey({autoIncrement: true}),
|
||||
ruleId: integer("ruleId").primaryKey({ autoIncrement: true }),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, {onDelete: "cascade"}),
|
||||
enabled: integer("enabled", {mode: "boolean"}).notNull().default(true),
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
priority: integer("priority").notNull(),
|
||||
action: text("action").notNull(), // ACCEPT, DROP, PASS
|
||||
match: text("match").notNull(), // CIDR, PATH, IP
|
||||
@@ -742,17 +850,17 @@ export const resourceRules = sqliteTable("resourceRules", {
|
||||
});
|
||||
|
||||
export const supporterKey = sqliteTable("supporterKey", {
|
||||
keyId: integer("keyId").primaryKey({autoIncrement: true}),
|
||||
keyId: integer("keyId").primaryKey({ autoIncrement: true }),
|
||||
key: text("key").notNull(),
|
||||
githubUsername: text("githubUsername").notNull(),
|
||||
phrase: text("phrase"),
|
||||
tier: text("tier"),
|
||||
valid: integer("valid", {mode: "boolean"}).notNull().default(false)
|
||||
valid: integer("valid", { mode: "boolean" }).notNull().default(false)
|
||||
});
|
||||
|
||||
// Identity Providers
|
||||
export const idp = sqliteTable("idp", {
|
||||
idpId: integer("idpId").primaryKey({autoIncrement: true}),
|
||||
idpId: integer("idpId").primaryKey({ autoIncrement: true }),
|
||||
name: text("name").notNull(),
|
||||
type: text("type").notNull(),
|
||||
defaultRoleMapping: text("defaultRoleMapping"),
|
||||
@@ -761,7 +869,8 @@ export const idp = sqliteTable("idp", {
|
||||
mode: "boolean"
|
||||
})
|
||||
.notNull()
|
||||
.default(false)
|
||||
.default(false),
|
||||
tags: text("tags")
|
||||
});
|
||||
|
||||
// Identity Provider OAuth Configuration
|
||||
@@ -772,7 +881,7 @@ export const idpOidcConfig = sqliteTable("idpOidcConfig", {
|
||||
variant: text("variant").notNull().default("oidc"),
|
||||
idpId: integer("idpId")
|
||||
.notNull()
|
||||
.references(() => idp.idpId, {onDelete: "cascade"}),
|
||||
.references(() => idp.idpId, { onDelete: "cascade" }),
|
||||
clientId: text("clientId").notNull(),
|
||||
clientSecret: text("clientSecret").notNull(),
|
||||
authUrl: text("authUrl").notNull(),
|
||||
@@ -800,22 +909,22 @@ export const apiKeys = sqliteTable("apiKeys", {
|
||||
apiKeyHash: text("apiKeyHash").notNull(),
|
||||
lastChars: text("lastChars").notNull(),
|
||||
createdAt: text("dateCreated").notNull(),
|
||||
isRoot: integer("isRoot", {mode: "boolean"}).notNull().default(false)
|
||||
isRoot: integer("isRoot", { mode: "boolean" }).notNull().default(false)
|
||||
});
|
||||
|
||||
export const apiKeyActions = sqliteTable("apiKeyActions", {
|
||||
apiKeyId: text("apiKeyId")
|
||||
.notNull()
|
||||
.references(() => apiKeys.apiKeyId, {onDelete: "cascade"}),
|
||||
.references(() => apiKeys.apiKeyId, { onDelete: "cascade" }),
|
||||
actionId: text("actionId")
|
||||
.notNull()
|
||||
.references(() => actions.actionId, {onDelete: "cascade"})
|
||||
.references(() => actions.actionId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const apiKeyOrg = sqliteTable("apiKeyOrg", {
|
||||
apiKeyId: text("apiKeyId")
|
||||
.notNull()
|
||||
.references(() => apiKeys.apiKeyId, {onDelete: "cascade"}),
|
||||
.references(() => apiKeys.apiKeyId, { onDelete: "cascade" }),
|
||||
orgId: text("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
@@ -826,10 +935,10 @@ export const apiKeyOrg = sqliteTable("apiKeyOrg", {
|
||||
export const idpOrg = sqliteTable("idpOrg", {
|
||||
idpId: integer("idpId")
|
||||
.notNull()
|
||||
.references(() => idp.idpId, {onDelete: "cascade"}),
|
||||
.references(() => idp.idpId, { onDelete: "cascade" }),
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, {onDelete: "cascade"}),
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
roleMapping: text("roleMapping"),
|
||||
orgMapping: text("orgMapping")
|
||||
});
|
||||
@@ -847,19 +956,19 @@ export const blueprints = sqliteTable("blueprints", {
|
||||
name: text("name").notNull(),
|
||||
source: text("source").notNull(),
|
||||
createdAt: integer("createdAt").notNull(),
|
||||
succeeded: integer("succeeded", {mode: "boolean"}).notNull(),
|
||||
succeeded: integer("succeeded", { mode: "boolean" }).notNull(),
|
||||
contents: text("contents").notNull(),
|
||||
message: text("message")
|
||||
});
|
||||
export const requestAuditLog = sqliteTable(
|
||||
"requestAuditLog",
|
||||
{
|
||||
id: integer("id").primaryKey({autoIncrement: true}),
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
|
||||
orgId: text("orgId").references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
action: integer("action", {mode: "boolean"}).notNull(),
|
||||
action: integer("action", { mode: "boolean" }).notNull(),
|
||||
reason: integer("reason").notNull(),
|
||||
actorType: text("actorType"),
|
||||
actor: text("actor"),
|
||||
@@ -876,7 +985,7 @@ export const requestAuditLog = sqliteTable(
|
||||
host: text("host"),
|
||||
path: text("path"),
|
||||
method: text("method"),
|
||||
tls: integer("tls", {mode: "boolean"})
|
||||
tls: integer("tls", { mode: "boolean" })
|
||||
},
|
||||
(table) => [
|
||||
index("idx_requestAuditLog_timestamp").on(table.timestamp),
|
||||
@@ -932,7 +1041,9 @@ export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
||||
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
||||
export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel<typeof resourceHeaderAuthExtendedCompatibility>;
|
||||
export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel<
|
||||
typeof resourceHeaderAuthExtendedCompatibility
|
||||
>;
|
||||
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
||||
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
||||
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
||||
|
||||
3
server/lib/blueprints/MaintenanceSchema.ts
Normal file
3
server/lib/blueprints/MaintenanceSchema.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const MaintenanceSchema = z.object({});
|
||||
@@ -1,4 +1,14 @@
|
||||
import { db, newts, blueprints, Blueprint, Site, siteResources, roleSiteResources, userSiteResources, clientSiteResources } from "@server/db";
|
||||
import {
|
||||
db,
|
||||
newts,
|
||||
blueprints,
|
||||
Blueprint,
|
||||
Site,
|
||||
siteResources,
|
||||
roleSiteResources,
|
||||
userSiteResources,
|
||||
clientSiteResources
|
||||
} from "@server/db";
|
||||
import { Config, ConfigSchema } from "./types";
|
||||
import { ProxyResourcesResults, updateProxyResources } from "./proxyResources";
|
||||
import { fromError } from "zod-validation-error";
|
||||
@@ -126,7 +136,7 @@ export async function applyBlueprint({
|
||||
)
|
||||
.then((rows) => rows.map((row) => row.roleId));
|
||||
|
||||
const existingUserIds= await trx
|
||||
const existingUserIds = await trx
|
||||
.select()
|
||||
.from(userSiteResources)
|
||||
.where(
|
||||
@@ -134,7 +144,8 @@ export async function applyBlueprint({
|
||||
userSiteResources.siteResourceId,
|
||||
result.oldSiteResource.siteResourceId
|
||||
)
|
||||
).then((rows) => rows.map((row) => row.userId));
|
||||
)
|
||||
.then((rows) => rows.map((row) => row.userId));
|
||||
|
||||
const existingClientIds = await trx
|
||||
.select()
|
||||
@@ -144,13 +155,19 @@ export async function applyBlueprint({
|
||||
clientSiteResources.siteResourceId,
|
||||
result.oldSiteResource.siteResourceId
|
||||
)
|
||||
).then((rows) => rows.map((row) => row.clientId));
|
||||
)
|
||||
.then((rows) => rows.map((row) => row.clientId));
|
||||
|
||||
// delete the existing site resource
|
||||
await trx
|
||||
.delete(siteResources)
|
||||
.where(
|
||||
and(eq(siteResources.siteResourceId, result.oldSiteResource.siteResourceId))
|
||||
and(
|
||||
eq(
|
||||
siteResources.siteResourceId,
|
||||
result.oldSiteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(
|
||||
@@ -161,7 +178,7 @@ export async function applyBlueprint({
|
||||
const [insertedSiteResource] = await trx
|
||||
.insert(siteResources)
|
||||
.values({
|
||||
...result.newSiteResource,
|
||||
...result.newSiteResource
|
||||
})
|
||||
.returning();
|
||||
|
||||
@@ -172,18 +189,20 @@ export async function applyBlueprint({
|
||||
|
||||
if (existingRoleIds.length > 0) {
|
||||
await trx.insert(roleSiteResources).values(
|
||||
existingRoleIds.map((roleId) => ({
|
||||
existingRoleIds.map((roleId) => ({
|
||||
roleId,
|
||||
siteResourceId: insertedSiteResource!.siteResourceId
|
||||
siteResourceId:
|
||||
insertedSiteResource!.siteResourceId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
if (existingUserIds.length > 0) {
|
||||
await trx.insert(userSiteResources).values(
|
||||
existingUserIds.map((userId) => ({
|
||||
existingUserIds.map((userId) => ({
|
||||
userId,
|
||||
siteResourceId: insertedSiteResource!.siteResourceId
|
||||
siteResourceId:
|
||||
insertedSiteResource!.siteResourceId
|
||||
}))
|
||||
);
|
||||
}
|
||||
@@ -192,7 +211,8 @@ export async function applyBlueprint({
|
||||
await trx.insert(clientSiteResources).values(
|
||||
existingClientIds.map((clientId) => ({
|
||||
clientId,
|
||||
siteResourceId: insertedSiteResource!.siteResourceId
|
||||
siteResourceId:
|
||||
insertedSiteResource!.siteResourceId
|
||||
}))
|
||||
);
|
||||
}
|
||||
@@ -201,7 +221,6 @@ export async function applyBlueprint({
|
||||
insertedSiteResource,
|
||||
trx
|
||||
);
|
||||
|
||||
} else {
|
||||
const [newSite] = await trx
|
||||
.select()
|
||||
|
||||
@@ -2,7 +2,8 @@ import {
|
||||
domains,
|
||||
orgDomains,
|
||||
Resource,
|
||||
resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility,
|
||||
resourceHeaderAuth,
|
||||
resourceHeaderAuthExtendedCompatibility,
|
||||
resourcePincode,
|
||||
resourceRules,
|
||||
resourceWhitelist,
|
||||
@@ -16,8 +17,8 @@ import {
|
||||
userResources,
|
||||
users
|
||||
} from "@server/db";
|
||||
import {resources, targets, sites} from "@server/db";
|
||||
import {eq, and, asc, or, ne, count, isNotNull} from "drizzle-orm";
|
||||
import { resources, targets, sites } from "@server/db";
|
||||
import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm";
|
||||
import {
|
||||
Config,
|
||||
ConfigSchema,
|
||||
@@ -25,12 +26,13 @@ import {
|
||||
TargetData
|
||||
} from "./types";
|
||||
import logger from "@server/logger";
|
||||
import {createCertificate} from "#dynamic/routers/certificates/createCertificate";
|
||||
import {pickPort} from "@server/routers/target/helpers";
|
||||
import {resourcePassword} from "@server/db";
|
||||
import {hashPassword} from "@server/auth/password";
|
||||
import {isValidCIDR, isValidIP, isValidUrlGlobPattern} from "../validators";
|
||||
import {get} from "http";
|
||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||
import { pickPort } from "@server/routers/target/helpers";
|
||||
import { resourcePassword } from "@server/db";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||
import { isLicensedOrSubscribed } from "../isLicencedOrSubscribed";
|
||||
import { build } from "@server/build";
|
||||
|
||||
export type ProxyResourcesResults = {
|
||||
proxyResource: Resource;
|
||||
@@ -63,7 +65,7 @@ export async function updateProxyResources(
|
||||
if (targetSiteId) {
|
||||
// Look up site by niceId
|
||||
[site] = await trx
|
||||
.select({siteId: sites.siteId})
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(
|
||||
and(
|
||||
@@ -75,7 +77,7 @@ export async function updateProxyResources(
|
||||
} else if (siteId) {
|
||||
// Use the provided siteId directly, but verify it belongs to the org
|
||||
[site] = await trx
|
||||
.select({siteId: sites.siteId})
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(
|
||||
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
|
||||
@@ -93,7 +95,7 @@ export async function updateProxyResources(
|
||||
|
||||
let internalPortToCreate;
|
||||
if (!targetData["internal-port"]) {
|
||||
const {internalPort, targetIps} = await pickPort(
|
||||
const { internalPort, targetIps } = await pickPort(
|
||||
site.siteId!,
|
||||
trx
|
||||
);
|
||||
@@ -209,6 +211,16 @@ export async function updateProxyResources(
|
||||
resource = existingResource;
|
||||
} else {
|
||||
// Update existing resource
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||
if (build == "enterprise" && !isLicensed) {
|
||||
logger.warn(
|
||||
"Server is not licensed! Clearing set maintenance screen values"
|
||||
);
|
||||
// null the maintenance mode fields if not licensed
|
||||
resourceData.maintenance = undefined;
|
||||
}
|
||||
|
||||
[resource] = await trx
|
||||
.update(resources)
|
||||
.set({
|
||||
@@ -228,12 +240,19 @@ export async function updateProxyResources(
|
||||
tlsServerName: resourceData["tls-server-name"] || null,
|
||||
emailWhitelistEnabled: resourceData.auth?.[
|
||||
"whitelist-users"
|
||||
]
|
||||
]
|
||||
? resourceData.auth["whitelist-users"].length > 0
|
||||
: false,
|
||||
headers: headers || null,
|
||||
applyRules:
|
||||
resourceData.rules && resourceData.rules.length > 0
|
||||
resourceData.rules && resourceData.rules.length > 0,
|
||||
maintenanceModeEnabled:
|
||||
resourceData.maintenance?.enabled,
|
||||
maintenanceModeType: resourceData.maintenance?.type,
|
||||
maintenanceTitle: resourceData.maintenance?.title,
|
||||
maintenanceMessage: resourceData.maintenance?.message,
|
||||
maintenanceEstimatedTime:
|
||||
resourceData.maintenance?.["estimated-time"]
|
||||
})
|
||||
.where(
|
||||
eq(resources.resourceId, existingResource.resourceId)
|
||||
@@ -303,8 +322,13 @@ export async function updateProxyResources(
|
||||
const headerAuthPassword =
|
||||
resourceData.auth?.["basic-auth"]?.password;
|
||||
const headerAuthExtendedCompatibility =
|
||||
resourceData.auth?.["basic-auth"]?.extendedCompatibility;
|
||||
if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) {
|
||||
resourceData.auth?.["basic-auth"]
|
||||
?.extendedCompatibility;
|
||||
if (
|
||||
headerAuthUser &&
|
||||
headerAuthPassword &&
|
||||
headerAuthExtendedCompatibility !== null
|
||||
) {
|
||||
const headerAuthHash = await hashPassword(
|
||||
Buffer.from(
|
||||
`${headerAuthUser}:${headerAuthPassword}`
|
||||
@@ -315,10 +339,13 @@ export async function updateProxyResources(
|
||||
resourceId: existingResource.resourceId,
|
||||
headerAuthHash
|
||||
}),
|
||||
trx.insert(resourceHeaderAuthExtendedCompatibility).values({
|
||||
resourceId: existingResource.resourceId,
|
||||
extendedCompatibilityIsActivated: headerAuthExtendedCompatibility
|
||||
})
|
||||
trx
|
||||
.insert(resourceHeaderAuthExtendedCompatibility)
|
||||
.values({
|
||||
resourceId: existingResource.resourceId,
|
||||
extendedCompatibilityIsActivated:
|
||||
headerAuthExtendedCompatibility
|
||||
})
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -380,7 +407,7 @@ export async function updateProxyResources(
|
||||
if (targetSiteId) {
|
||||
// Look up site by niceId
|
||||
[site] = await trx
|
||||
.select({siteId: sites.siteId})
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(
|
||||
and(
|
||||
@@ -392,7 +419,7 @@ export async function updateProxyResources(
|
||||
} else if (siteId) {
|
||||
// Use the provided siteId directly, but verify it belongs to the org
|
||||
[site] = await trx
|
||||
.select({siteId: sites.siteId})
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(
|
||||
and(
|
||||
@@ -437,7 +464,7 @@ export async function updateProxyResources(
|
||||
if (checkIfTargetChanged(existingTarget, updatedTarget)) {
|
||||
let internalPortToUpdate;
|
||||
if (!targetData["internal-port"]) {
|
||||
const {internalPort, targetIps} = await pickPort(
|
||||
const { internalPort, targetIps } = await pickPort(
|
||||
site.siteId!,
|
||||
trx
|
||||
);
|
||||
@@ -622,6 +649,15 @@ export async function updateProxyResources(
|
||||
);
|
||||
}
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||
if (build == "enterprise" && !isLicensed) {
|
||||
logger.warn(
|
||||
"Server is not licensed! Clearing set maintenance screen values"
|
||||
);
|
||||
// null the maintenance mode fields if not licensed
|
||||
resourceData.maintenance = undefined;
|
||||
}
|
||||
|
||||
// Create new resource
|
||||
const [newResource] = await trx
|
||||
.insert(resources)
|
||||
@@ -643,7 +679,13 @@ export async function updateProxyResources(
|
||||
ssl: resourceSsl,
|
||||
headers: headers || null,
|
||||
applyRules:
|
||||
resourceData.rules && resourceData.rules.length > 0
|
||||
resourceData.rules && resourceData.rules.length > 0,
|
||||
maintenanceModeEnabled: resourceData.maintenance?.enabled,
|
||||
maintenanceModeType: resourceData.maintenance?.type,
|
||||
maintenanceTitle: resourceData.maintenance?.title,
|
||||
maintenanceMessage: resourceData.maintenance?.message,
|
||||
maintenanceEstimatedTime:
|
||||
resourceData.maintenance?.["estimated-time"]
|
||||
})
|
||||
.returning();
|
||||
|
||||
@@ -674,9 +716,14 @@ export async function updateProxyResources(
|
||||
const headerAuthUser = resourceData.auth?.["basic-auth"]?.user;
|
||||
const headerAuthPassword =
|
||||
resourceData.auth?.["basic-auth"]?.password;
|
||||
const headerAuthExtendedCompatibility = resourceData.auth?.["basic-auth"]?.extendedCompatibility;
|
||||
const headerAuthExtendedCompatibility =
|
||||
resourceData.auth?.["basic-auth"]?.extendedCompatibility;
|
||||
|
||||
if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) {
|
||||
if (
|
||||
headerAuthUser &&
|
||||
headerAuthPassword &&
|
||||
headerAuthExtendedCompatibility !== null
|
||||
) {
|
||||
const headerAuthHash = await hashPassword(
|
||||
Buffer.from(
|
||||
`${headerAuthUser}:${headerAuthPassword}`
|
||||
@@ -688,10 +735,13 @@ export async function updateProxyResources(
|
||||
resourceId: newResource.resourceId,
|
||||
headerAuthHash
|
||||
}),
|
||||
trx.insert(resourceHeaderAuthExtendedCompatibility).values({
|
||||
resourceId: newResource.resourceId,
|
||||
extendedCompatibilityIsActivated: headerAuthExtendedCompatibility
|
||||
}),
|
||||
trx
|
||||
.insert(resourceHeaderAuthExtendedCompatibility)
|
||||
.values({
|
||||
resourceId: newResource.resourceId,
|
||||
extendedCompatibilityIsActivated:
|
||||
headerAuthExtendedCompatibility
|
||||
})
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1043,7 +1093,7 @@ async function getDomain(
|
||||
trx: Transaction
|
||||
) {
|
||||
const [fullDomainExists] = await trx
|
||||
.select({resourceId: resources.resourceId})
|
||||
.select({ resourceId: resources.resourceId })
|
||||
.from(resources)
|
||||
.where(
|
||||
and(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { portRangeStringSchema } from "@server/lib/ip";
|
||||
import { MaintenanceSchema } from "#dynamic/lib/blueprints/MaintenanceSchema";
|
||||
|
||||
export const SiteSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
@@ -53,11 +54,13 @@ export const AuthSchema = z.object({
|
||||
// pincode has to have 6 digits
|
||||
pincode: z.number().min(100000).max(999999).optional(),
|
||||
password: z.string().min(1).optional(),
|
||||
"basic-auth": z.object({
|
||||
user: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
extendedCompatibility: z.boolean().default(true)
|
||||
}).optional(),
|
||||
"basic-auth": z
|
||||
.object({
|
||||
user: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
extendedCompatibility: z.boolean().default(true)
|
||||
})
|
||||
.optional(),
|
||||
"sso-enabled": z.boolean().optional().default(false),
|
||||
"sso-roles": z
|
||||
.array(z.string())
|
||||
@@ -108,32 +111,30 @@ export const RuleSchema = z
|
||||
.refine(
|
||||
(rule) => {
|
||||
if (rule.match === "country") {
|
||||
// Check if it's a valid 2-letter country code
|
||||
return /^[A-Z]{2}$/.test(rule.value);
|
||||
// Check if it's a valid 2-letter country code or "ALL"
|
||||
return /^[A-Z]{2}$/.test(rule.value) || rule.value === "ALL";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
path: ["value"],
|
||||
message:
|
||||
"Value must be a 2-letter country code when match is 'country'"
|
||||
"Value must be a 2-letter country code or 'ALL' when match is 'country'"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(rule) => {
|
||||
if (rule.match === "asn") {
|
||||
// Check if it's either AS<number> format or just a number
|
||||
// Check if it's either AS<number> format or "ALL"
|
||||
const asNumberPattern = /^AS\d+$/i;
|
||||
const isASFormat = asNumberPattern.test(rule.value);
|
||||
const isNumeric = /^\d+$/.test(rule.value);
|
||||
return isASFormat || isNumeric;
|
||||
return asNumberPattern.test(rule.value) || rule.value === "ALL";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
path: ["value"],
|
||||
message:
|
||||
"Value must be either 'AS<number>' format or a number when match is 'asn'"
|
||||
"Value must be 'AS<number>' format or 'ALL' when match is 'asn'"
|
||||
}
|
||||
);
|
||||
|
||||
@@ -156,7 +157,8 @@ export const ResourceSchema = z
|
||||
"host-header": z.string().optional(),
|
||||
"tls-server-name": z.string().optional(),
|
||||
headers: z.array(HeaderSchema).optional(),
|
||||
rules: z.array(RuleSchema).optional()
|
||||
rules: z.array(RuleSchema).optional(),
|
||||
maintenance: MaintenanceSchema.optional()
|
||||
})
|
||||
.refine(
|
||||
(resource) => {
|
||||
@@ -288,8 +290,8 @@ export const ClientResourceSchema = z
|
||||
alias: z
|
||||
.string()
|
||||
.regex(
|
||||
/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/,
|
||||
"Alias must be a fully qualified domain name (e.g., example.com)"
|
||||
/^(?:[a-zA-Z0-9*?](?:[a-zA-Z0-9*?-]{0,61}[a-zA-Z0-9*?])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/,
|
||||
"Alias must be a fully qualified domain name with optional wildcards (e.g., example.com, *.example.com, host-0?.example.internal)"
|
||||
)
|
||||
.optional(),
|
||||
roles: z
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
||||
import { build } from "@server/build";
|
||||
import {
|
||||
approvals,
|
||||
clients,
|
||||
db,
|
||||
olms,
|
||||
orgs,
|
||||
roleClients,
|
||||
roles,
|
||||
Transaction,
|
||||
userClients,
|
||||
userOrgs,
|
||||
Transaction
|
||||
userOrgs
|
||||
} from "@server/db";
|
||||
import { eq, and, notInArray } from "drizzle-orm";
|
||||
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
||||
import { getNextAvailableClientSubnet } from "@server/lib/ip";
|
||||
import logger from "@server/logger";
|
||||
import { rebuildClientAssociationsFromClient } from "./rebuildClientAssociations";
|
||||
import { sendTerminateClient } from "@server/routers/client/terminate";
|
||||
import { getUniqueClientName } from "@server/db/names";
|
||||
import { getNextAvailableClientSubnet } from "@server/lib/ip";
|
||||
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
|
||||
import logger from "@server/logger";
|
||||
import { sendTerminateClient } from "@server/routers/client/terminate";
|
||||
import { and, eq, notInArray, type InferInsertModel } from "drizzle-orm";
|
||||
import { rebuildClientAssociationsFromClient } from "./rebuildClientAssociations";
|
||||
|
||||
export async function calculateUserClientsForOrgs(
|
||||
userId: string,
|
||||
@@ -38,13 +41,15 @@ export async function calculateUserClientsForOrgs(
|
||||
const allUserOrgs = await transaction
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.innerJoin(roles, eq(roles.roleId, userOrgs.roleId))
|
||||
.where(eq(userOrgs.userId, userId));
|
||||
|
||||
const userOrgIds = allUserOrgs.map((uo) => uo.orgId);
|
||||
const userOrgIds = allUserOrgs.map(({ userOrgs: uo }) => uo.orgId);
|
||||
|
||||
// For each OLM, ensure there's a client in each org the user is in
|
||||
for (const olm of userOlms) {
|
||||
for (const userOrg of allUserOrgs) {
|
||||
for (const userRoleOrg of allUserOrgs) {
|
||||
const { userOrgs: userOrg, roles: role } = userRoleOrg;
|
||||
const orgId = userOrg.orgId;
|
||||
|
||||
const [org] = await transaction
|
||||
@@ -182,21 +187,46 @@ export async function calculateUserClientsForOrgs(
|
||||
|
||||
const niceId = await getUniqueClientName(orgId);
|
||||
|
||||
const isOrgLicensed = await isLicensedOrSubscribed(
|
||||
userOrg.orgId
|
||||
);
|
||||
const requireApproval =
|
||||
build !== "oss" &&
|
||||
isOrgLicensed &&
|
||||
role.requireDeviceApproval;
|
||||
|
||||
const newClientData: InferInsertModel<typeof clients> = {
|
||||
userId,
|
||||
orgId: userOrg.orgId,
|
||||
exitNodeId: randomExitNode.exitNodeId,
|
||||
name: olm.name || "User Client",
|
||||
subnet: updatedSubnet,
|
||||
olmId: olm.olmId,
|
||||
type: "olm",
|
||||
niceId,
|
||||
approvalState: requireApproval ? "pending" : null
|
||||
};
|
||||
|
||||
// Create the client
|
||||
const [newClient] = await transaction
|
||||
.insert(clients)
|
||||
.values({
|
||||
userId,
|
||||
orgId: userOrg.orgId,
|
||||
exitNodeId: randomExitNode.exitNodeId,
|
||||
name: olm.name || "User Client",
|
||||
subnet: updatedSubnet,
|
||||
olmId: olm.olmId,
|
||||
type: "olm",
|
||||
niceId
|
||||
})
|
||||
.values(newClientData)
|
||||
.returning();
|
||||
|
||||
// create approval request
|
||||
if (requireApproval) {
|
||||
await transaction
|
||||
.insert(approvals)
|
||||
.values({
|
||||
timestamp: Math.floor(new Date().getTime() / 1000),
|
||||
orgId: userOrg.orgId,
|
||||
clientId: newClient.clientId,
|
||||
userId,
|
||||
type: "user_device"
|
||||
})
|
||||
.returning();
|
||||
}
|
||||
|
||||
await rebuildClientAssociationsFromClient(
|
||||
newClient,
|
||||
transaction
|
||||
|
||||
@@ -26,6 +26,7 @@ export function initLogCleanupInterval() {
|
||||
)
|
||||
);
|
||||
|
||||
// TODO: handle when there are multiple nodes doing this clearing using redis
|
||||
for (const org of orgsToClean) {
|
||||
const {
|
||||
orgId,
|
||||
|
||||
@@ -84,6 +84,10 @@ export class Config {
|
||||
?.disable_basic_wireguard_sites
|
||||
? "true"
|
||||
: "false";
|
||||
process.env.FLAGS_DISABLE_PRODUCT_HELP_BANNERS = parsedConfig.flags
|
||||
?.disable_product_help_banners
|
||||
? "true"
|
||||
: "false";
|
||||
|
||||
process.env.PRODUCT_UPDATES_NOTIFICATION_ENABLED = parsedConfig.app
|
||||
.notifications.product_updates
|
||||
|
||||
@@ -4,6 +4,7 @@ import { and, eq, isNotNull } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
import z from "zod";
|
||||
import logger from "@server/logger";
|
||||
import semver from "semver";
|
||||
|
||||
interface IPRange {
|
||||
start: bigint;
|
||||
@@ -318,10 +319,7 @@ export function doCidrsOverlap(cidr1: string, cidr2: string): boolean {
|
||||
const range2 = cidrToRange(cidr2);
|
||||
|
||||
// Overlap if the ranges intersect
|
||||
return (
|
||||
range1.start <= range2.end &&
|
||||
range2.start <= range1.end
|
||||
);
|
||||
return range1.start <= range2.end && range2.start <= range1.end;
|
||||
}
|
||||
|
||||
export async function getNextAvailableClientSubnet(
|
||||
@@ -686,3 +684,35 @@ export function parsePortRangeString(
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function stripPortFromHost(ip: string, badgerVersion?: string): string {
|
||||
const isNewerBadger =
|
||||
badgerVersion &&
|
||||
semver.valid(badgerVersion) &&
|
||||
semver.gte(badgerVersion, "1.3.1");
|
||||
|
||||
if (isNewerBadger) {
|
||||
return ip;
|
||||
}
|
||||
|
||||
if (ip.startsWith("[") && ip.includes("]")) {
|
||||
// if brackets are found, extract the IPv6 address from between the brackets
|
||||
const ipv6Match = ip.match(/\[(.*?)\]/);
|
||||
if (ipv6Match) {
|
||||
return ipv6Match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it looks like IPv4 (contains dots and matches IPv4 pattern)
|
||||
// IPv4 format: x.x.x.x where x is 0-255
|
||||
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}/;
|
||||
if (ipv4Pattern.test(ip)) {
|
||||
const lastColonIndex = ip.lastIndexOf(":");
|
||||
if (lastColonIndex !== -1) {
|
||||
return ip.substring(0, lastColonIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Return as is
|
||||
return ip;
|
||||
}
|
||||
|
||||
@@ -216,7 +216,10 @@ export const configSchema = z
|
||||
.default(["newt", "wireguard", "local"]),
|
||||
allow_raw_resources: z.boolean().optional().default(true),
|
||||
file_mode: z.boolean().optional().default(false),
|
||||
pp_transport_prefix: z.string().optional().default("pp-transport-v")
|
||||
pp_transport_prefix: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("pp-transport-v")
|
||||
})
|
||||
.optional()
|
||||
.prefault({}),
|
||||
@@ -327,7 +330,8 @@ export const configSchema = z
|
||||
enable_integration_api: z.boolean().optional(),
|
||||
disable_local_sites: z.boolean().optional(),
|
||||
disable_basic_wireguard_sites: z.boolean().optional(),
|
||||
disable_config_managed_domains: z.boolean().optional()
|
||||
disable_config_managed_domains: z.boolean().optional(),
|
||||
disable_product_help_banners: z.boolean().optional()
|
||||
})
|
||||
.optional(),
|
||||
dns: z
|
||||
|
||||
@@ -41,9 +41,10 @@ type TargetWithSite = Target & {
|
||||
export async function getTraefikConfig(
|
||||
exitNodeId: number,
|
||||
siteTypes: string[],
|
||||
filterOutNamespaceDomains = false,
|
||||
generateLoginPageRouters = false,
|
||||
allowRawResources = true
|
||||
filterOutNamespaceDomains = false, // UNUSED BUT USED IN PRIVATE
|
||||
generateLoginPageRouters = false, // UNUSED BUT USED IN PRIVATE
|
||||
allowRawResources = true,
|
||||
allowMaintenancePage = true, // UNUSED BUT USED IN PRIVATE
|
||||
): Promise<any> {
|
||||
// Get resources with their targets and sites in a single optimized query
|
||||
// Start from sites on this exit node, then join to targets and resources
|
||||
@@ -294,12 +295,12 @@ export async function getTraefikConfig(
|
||||
certResolver: resolverName,
|
||||
...(preferWildcard
|
||||
? {
|
||||
domains: [
|
||||
{
|
||||
main: wildCard
|
||||
}
|
||||
]
|
||||
}
|
||||
domains: [
|
||||
{
|
||||
main: wildCard
|
||||
}
|
||||
]
|
||||
}
|
||||
: {})
|
||||
};
|
||||
|
||||
@@ -475,9 +476,9 @@ export async function getTraefikConfig(
|
||||
// RECEIVE BANDWIDTH ENDPOINT.
|
||||
|
||||
// TODO: HOW TO HANDLE ^^^^^^ BETTER
|
||||
const anySitesOnline = (
|
||||
targets
|
||||
).some((target) => target.site.online);
|
||||
const anySitesOnline = targets.some(
|
||||
(target) => target.site.online
|
||||
);
|
||||
|
||||
return (
|
||||
targets
|
||||
@@ -544,14 +545,14 @@ export async function getTraefikConfig(
|
||||
})(),
|
||||
...(resource.stickySession
|
||||
? {
|
||||
sticky: {
|
||||
cookie: {
|
||||
name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
|
||||
secure: resource.ssl,
|
||||
httpOnly: true
|
||||
}
|
||||
}
|
||||
}
|
||||
sticky: {
|
||||
cookie: {
|
||||
name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
|
||||
secure: resource.ssl,
|
||||
httpOnly: true
|
||||
}
|
||||
}
|
||||
}
|
||||
: {})
|
||||
}
|
||||
};
|
||||
@@ -603,9 +604,9 @@ export async function getTraefikConfig(
|
||||
loadBalancer: {
|
||||
servers: (() => {
|
||||
// Check if any sites are online
|
||||
const anySitesOnline = (
|
||||
targets
|
||||
).some((target) => target.site.online);
|
||||
const anySitesOnline = targets.some(
|
||||
(target) => target.site.online
|
||||
);
|
||||
|
||||
return targets
|
||||
.filter((target) => {
|
||||
@@ -654,18 +655,18 @@ export async function getTraefikConfig(
|
||||
})(),
|
||||
...(resource.proxyProtocol && protocol == "tcp"
|
||||
? {
|
||||
serversTransport: `${ppPrefix}${resource.proxyProtocolVersion || 1}@file` // TODO: does @file here cause issues?
|
||||
}
|
||||
serversTransport: `${ppPrefix}${resource.proxyProtocolVersion || 1}@file` // TODO: does @file here cause issues?
|
||||
}
|
||||
: {}),
|
||||
...(resource.stickySession
|
||||
? {
|
||||
sticky: {
|
||||
ipStrategy: {
|
||||
depth: 0,
|
||||
sourcePort: true
|
||||
}
|
||||
}
|
||||
}
|
||||
sticky: {
|
||||
ipStrategy: {
|
||||
depth: 0,
|
||||
sourcePort: true
|
||||
}
|
||||
}
|
||||
}
|
||||
: {})
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13,3 +13,4 @@ export * from "./verifyApiKeyIsRoot";
|
||||
export * from "./verifyApiKeyApiKeyAccess";
|
||||
export * from "./verifyApiKeyClientAccess";
|
||||
export * from "./verifyApiKeySiteResourceAccess";
|
||||
export * from "./verifyApiKeyIdpAccess";
|
||||
|
||||
88
server/middlewares/integration/verifyApiKeyIdpAccess.ts
Normal file
88
server/middlewares/integration/verifyApiKeyIdpAccess.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { idp, idpOrg, apiKeyOrg } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
export async function verifyApiKeyIdpAccess(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const apiKey = req.apiKey;
|
||||
const idpId = req.params.idpId || req.body.idpId || req.query.idpId;
|
||||
const orgId = req.params.orgId;
|
||||
|
||||
if (!apiKey) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||
);
|
||||
}
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
if (!idpId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid IDP ID")
|
||||
);
|
||||
}
|
||||
|
||||
if (apiKey.isRoot) {
|
||||
// Root keys can access any IDP in any org
|
||||
return next();
|
||||
}
|
||||
|
||||
const [idpRes] = await db
|
||||
.select()
|
||||
.from(idp)
|
||||
.innerJoin(idpOrg, eq(idp.idpId, idpOrg.idpId))
|
||||
.where(and(eq(idp.idpId, idpId), eq(idpOrg.orgId, orgId)))
|
||||
.limit(1);
|
||||
|
||||
if (!idpRes || !idpRes.idp || !idpRes.idpOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`IdP with ID ${idpId} not found for organization ${orgId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.apiKeyOrg) {
|
||||
const apiKeyOrgRes = await db
|
||||
.select()
|
||||
.from(apiKeyOrg)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||
eq(apiKeyOrg.orgId, idpRes.idpOrg.orgId)
|
||||
)
|
||||
);
|
||||
req.apiKeyOrg = apiKeyOrgRes[0];
|
||||
}
|
||||
|
||||
if (!req.apiKeyOrg) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Key does not have access to this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error verifying IDP access"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
22
server/private/lib/blueprints/MaintenanceSchema.ts
Normal file
22
server/private/lib/blueprints/MaintenanceSchema.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
export const MaintenanceSchema = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
type: z.enum(["forced", "automatic"]).optional(),
|
||||
title: z.string().max(255).nullable().optional(),
|
||||
message: z.string().max(2000).nullable().optional(),
|
||||
"estimated-time": z.string().max(100).nullable().optional()
|
||||
});
|
||||
@@ -23,10 +23,10 @@ import {
|
||||
} from "@server/lib/checkOrgAccessPolicy";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
|
||||
export async function enforceResourceSessionLength(
|
||||
export function enforceResourceSessionLength(
|
||||
resourceSession: ResourceSession,
|
||||
org: Org
|
||||
): Promise<{ valid: boolean; error?: string }> {
|
||||
): { valid: boolean; error?: string } {
|
||||
if (org.maxSessionLengthHours) {
|
||||
const sessionIssuedAt = resourceSession.issuedAt; // may be null
|
||||
const maxSessionLengthHours = org.maxSessionLengthHours;
|
||||
|
||||
@@ -139,6 +139,10 @@ export class PrivateConfig {
|
||||
process.env.USE_PANGOLIN_DNS =
|
||||
this.rawPrivateConfig.flags.use_pangolin_dns.toString();
|
||||
}
|
||||
if (this.rawPrivateConfig.flags.use_org_only_idp) {
|
||||
process.env.USE_ORG_ONLY_IDP =
|
||||
this.rawPrivateConfig.flags.use_org_only_idp.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public getRawPrivateConfig() {
|
||||
|
||||
@@ -50,10 +50,14 @@ export async function sendToExitNode(
|
||||
);
|
||||
}
|
||||
|
||||
return sendToClient(remoteExitNode.remoteExitNodeId, {
|
||||
type: request.remoteType,
|
||||
data: request.data
|
||||
});
|
||||
return sendToClient(
|
||||
remoteExitNode.remoteExitNodeId,
|
||||
{
|
||||
type: request.remoteType,
|
||||
data: request.data
|
||||
},
|
||||
{ incrementConfigVersion: true }
|
||||
);
|
||||
} else {
|
||||
let hostname = exitNode.reachableAt;
|
||||
|
||||
|
||||
@@ -288,7 +288,7 @@ export function selectBestExitNode(
|
||||
const validNodes = pingResults.filter((n) => !n.error && n.weight > 0);
|
||||
|
||||
if (validNodes.length === 0) {
|
||||
logger.error("No valid exit nodes available");
|
||||
logger.debug("No valid exit nodes available");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@ export class LockManager {
|
||||
*/
|
||||
async acquireLock(
|
||||
lockKey: string,
|
||||
ttlMs: number = 30000
|
||||
ttlMs: number = 30000,
|
||||
maxRetries: number = 3,
|
||||
retryDelayMs: number = 100
|
||||
): Promise<boolean> {
|
||||
if (!redis || !redis.status || redis.status !== "ready") {
|
||||
return true;
|
||||
@@ -35,49 +37,67 @@ export class LockManager {
|
||||
}:${Date.now()}`;
|
||||
const redisKey = `lock:${lockKey}`;
|
||||
|
||||
try {
|
||||
// Use SET with NX (only set if not exists) and PX (expire in milliseconds)
|
||||
// This is atomic and handles both setting and expiration
|
||||
const result = await redis.set(
|
||||
redisKey,
|
||||
lockValue,
|
||||
"PX",
|
||||
ttlMs,
|
||||
"NX"
|
||||
);
|
||||
|
||||
if (result === "OK") {
|
||||
logger.debug(
|
||||
`Lock acquired: ${lockKey} by ${
|
||||
config.getRawConfig().gerbil.exit_node_name
|
||||
}`
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
// Use SET with NX (only set if not exists) and PX (expire in milliseconds)
|
||||
// This is atomic and handles both setting and expiration
|
||||
const result = await redis.set(
|
||||
redisKey,
|
||||
lockValue,
|
||||
"PX",
|
||||
ttlMs,
|
||||
"NX"
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the existing lock is from this worker (reentrant behavior)
|
||||
const existingValue = await redis.get(redisKey);
|
||||
if (
|
||||
existingValue &&
|
||||
existingValue.startsWith(
|
||||
`${config.getRawConfig().gerbil.exit_node_name}:`
|
||||
)
|
||||
) {
|
||||
// Extend the lock TTL since it's the same worker
|
||||
await redis.pexpire(redisKey, ttlMs);
|
||||
logger.debug(
|
||||
`Lock extended: ${lockKey} by ${
|
||||
config.getRawConfig().gerbil.exit_node_name
|
||||
}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (result === "OK") {
|
||||
logger.debug(
|
||||
`Lock acquired: ${lockKey} by ${
|
||||
config.getRawConfig().gerbil.exit_node_name
|
||||
}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to acquire lock ${lockKey}:`, error);
|
||||
return false;
|
||||
// Check if the existing lock is from this worker (reentrant behavior)
|
||||
const existingValue = await redis.get(redisKey);
|
||||
if (
|
||||
existingValue &&
|
||||
existingValue.startsWith(
|
||||
`${config.getRawConfig().gerbil.exit_node_name}:`
|
||||
)
|
||||
) {
|
||||
// Extend the lock TTL since it's the same worker
|
||||
await redis.pexpire(redisKey, ttlMs);
|
||||
logger.debug(
|
||||
`Lock extended: ${lockKey} by ${
|
||||
config.getRawConfig().gerbil.exit_node_name
|
||||
}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If this isn't our last attempt, wait before retrying with exponential backoff
|
||||
if (attempt < maxRetries - 1) {
|
||||
const delay = retryDelayMs * Math.pow(2, attempt);
|
||||
logger.debug(
|
||||
`Lock ${lockKey} not available, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to acquire lock ${lockKey} (attempt ${attempt + 1}/${maxRetries}):`, error);
|
||||
// On error, still retry if we have attempts left
|
||||
if (attempt < maxRetries - 1) {
|
||||
const delay = retryDelayMs * Math.pow(2, attempt);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Failed to acquire lock ${lockKey} after ${maxRetries} attempts`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,7 @@ import logger from "@server/logger";
|
||||
import { and, eq, lt } from "drizzle-orm";
|
||||
import cache from "@server/lib/cache";
|
||||
import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs";
|
||||
import { stripPortFromHost } from "@server/lib/ip";
|
||||
|
||||
async function getAccessDays(orgId: string): Promise<number> {
|
||||
// check cache first
|
||||
@@ -116,19 +117,7 @@ export async function logAccessAudit(data: {
|
||||
}
|
||||
|
||||
const clientIp = data.requestIp
|
||||
? (() => {
|
||||
if (
|
||||
data.requestIp.startsWith("[") &&
|
||||
data.requestIp.includes("]")
|
||||
) {
|
||||
// if brackets are found, extract the IPv6 address from between the brackets
|
||||
const ipv6Match = data.requestIp.match(/\[(.*?)\]/);
|
||||
if (ipv6Match) {
|
||||
return ipv6Match[1];
|
||||
}
|
||||
}
|
||||
return data.requestIp;
|
||||
})()
|
||||
? stripPortFromHost(data.requestIp)
|
||||
: undefined;
|
||||
|
||||
const countryCode = data.requestIp
|
||||
|
||||
@@ -83,7 +83,8 @@ export const privateConfigSchema = z.object({
|
||||
flags: z
|
||||
.object({
|
||||
enable_redis: z.boolean().optional().default(false),
|
||||
use_pangolin_dns: z.boolean().optional().default(false)
|
||||
use_pangolin_dns: z.boolean().optional().default(false),
|
||||
use_org_only_idp: z.boolean().optional().default(false)
|
||||
})
|
||||
.optional()
|
||||
.prefault({}),
|
||||
|
||||
@@ -573,6 +573,20 @@ class RedisManager {
|
||||
}
|
||||
}
|
||||
|
||||
public async incr(key: string): Promise<number> {
|
||||
if (!this.isRedisEnabled() || !this.writeClient) return 0;
|
||||
|
||||
try {
|
||||
return await this.executeWithRetry(
|
||||
() => this.writeClient!.incr(key),
|
||||
"Redis INCR"
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Redis INCR error:", error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async sadd(key: string, member: string): Promise<boolean> {
|
||||
if (!this.isRedisEnabled() || !this.writeClient) return false;
|
||||
|
||||
|
||||
@@ -71,9 +71,9 @@ export async function getTraefikConfig(
|
||||
siteTypes: string[],
|
||||
filterOutNamespaceDomains = false,
|
||||
generateLoginPageRouters = false,
|
||||
allowRawResources = true
|
||||
allowRawResources = true,
|
||||
allowMaintenancePage = true
|
||||
): Promise<any> {
|
||||
|
||||
// Get resources with their targets and sites in a single optimized query
|
||||
// Start from sites on this exit node, then join to targets and resources
|
||||
const resourcesWithTargetsAndSites = await db
|
||||
@@ -358,18 +358,6 @@ export async function getTraefikConfig(
|
||||
}
|
||||
}
|
||||
|
||||
if (resource.ssl) {
|
||||
config_output.http.routers![routerName + "-redirect"] = {
|
||||
entryPoints: [
|
||||
config.getRawConfig().traefik.http_entrypoint
|
||||
],
|
||||
middlewares: [redirectHttpsMiddlewareName],
|
||||
service: serviceName,
|
||||
rule: rule,
|
||||
priority: priority
|
||||
};
|
||||
}
|
||||
|
||||
let tls = {};
|
||||
if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
|
||||
const domainParts = fullDomain.split(".");
|
||||
@@ -435,17 +423,27 @@ export async function getTraefikConfig(
|
||||
}
|
||||
}
|
||||
|
||||
const availableServers = targets.filter(
|
||||
(target) => {
|
||||
if (!target.enabled) return false;
|
||||
if (resource.ssl) {
|
||||
config_output.http.routers![routerName + "-redirect"] = {
|
||||
entryPoints: [
|
||||
config.getRawConfig().traefik.http_entrypoint
|
||||
],
|
||||
middlewares: [redirectHttpsMiddlewareName],
|
||||
service: serviceName,
|
||||
rule: rule,
|
||||
priority: priority
|
||||
};
|
||||
}
|
||||
|
||||
if (!target.site.online) return false;
|
||||
const availableServers = targets.filter((target) => {
|
||||
if (!target.enabled) return false;
|
||||
|
||||
if (target.health == "unhealthy") return false;
|
||||
if (!target.site.online) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
if (target.health == "unhealthy") return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const hasHealthyServers = availableServers.length > 0;
|
||||
|
||||
@@ -458,15 +456,15 @@ export async function getTraefikConfig(
|
||||
// );
|
||||
} else if (resource.maintenanceModeType === "automatic") {
|
||||
showMaintenancePage = !hasHealthyServers;
|
||||
if (showMaintenancePage) {
|
||||
logger.warn(
|
||||
`Resource ${resource.name} (${fullDomain}) has no healthy servers - showing maintenance page (AUTOMATIC mode)`
|
||||
);
|
||||
}
|
||||
// if (showMaintenancePage) {
|
||||
// logger.warn(
|
||||
// `Resource ${resource.name} (${fullDomain}) has no healthy servers - showing maintenance page (AUTOMATIC mode)`
|
||||
// );
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
if (showMaintenancePage) {
|
||||
if (showMaintenancePage && allowMaintenancePage) {
|
||||
const maintenanceServiceName = `${key}-maintenance-service`;
|
||||
const maintenanceRouterName = `${key}-maintenance-router`;
|
||||
const rewriteMiddlewareName = `${key}-maintenance-rewrite`;
|
||||
@@ -794,9 +792,9 @@ export async function getTraefikConfig(
|
||||
loadBalancer: {
|
||||
servers: (() => {
|
||||
// Check if any sites are online
|
||||
const anySitesOnline = (
|
||||
targets
|
||||
).some((target) => target.site.online);
|
||||
const anySitesOnline = targets.some(
|
||||
(target) => target.site.online
|
||||
);
|
||||
|
||||
return targets
|
||||
.filter((target) => {
|
||||
|
||||
@@ -27,7 +27,18 @@ export async function verifyValidSubscription(
|
||||
return next();
|
||||
}
|
||||
|
||||
const tier = await getOrgTierData(req.params.orgId);
|
||||
const orgId = req.params.orgId || req.body.orgId || req.query.orgId || req.userOrgId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Organization ID is required to verify subscription"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const tier = await getOrgTierData(orgId);
|
||||
|
||||
if (!tier.active) {
|
||||
return next(
|
||||
|
||||
15
server/private/routers/approvals/index.ts
Normal file
15
server/private/routers/approvals/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
export * from "./listApprovals";
|
||||
export * from "./processPendingApproval";
|
||||
188
server/private/routers/approvals/listApprovals.ts
Normal file
188
server/private/routers/approvals/listApprovals.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import { build } from "@server/build";
|
||||
import { getOrgTierData } from "@server/lib/billing";
|
||||
import { TierId } from "@server/lib/billing/tiers";
|
||||
import { approvals, clients, db, users, type Approval } from "@server/db";
|
||||
import { eq, isNull, sql, not, and, desc } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const querySchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("1000")
|
||||
.transform(Number)
|
||||
.pipe(z.int().nonnegative()),
|
||||
offset: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("0")
|
||||
.transform(Number)
|
||||
.pipe(z.int().nonnegative()),
|
||||
approvalState: z
|
||||
.enum(["pending", "approved", "denied", "all"])
|
||||
.optional()
|
||||
.default("all")
|
||||
.catch("all")
|
||||
});
|
||||
|
||||
async function queryApprovals(
|
||||
orgId: string,
|
||||
limit: number,
|
||||
offset: number,
|
||||
approvalState: z.infer<typeof querySchema>["approvalState"]
|
||||
) {
|
||||
let state: Array<Approval["decision"]> = [];
|
||||
switch (approvalState) {
|
||||
case "pending":
|
||||
state = ["pending"];
|
||||
break;
|
||||
case "approved":
|
||||
state = ["approved"];
|
||||
break;
|
||||
case "denied":
|
||||
state = ["denied"];
|
||||
break;
|
||||
default:
|
||||
state = ["approved", "denied", "pending"];
|
||||
}
|
||||
|
||||
const res = await db
|
||||
.select({
|
||||
approvalId: approvals.approvalId,
|
||||
orgId: approvals.orgId,
|
||||
clientId: approvals.clientId,
|
||||
decision: approvals.decision,
|
||||
type: approvals.type,
|
||||
user: {
|
||||
name: users.name,
|
||||
userId: users.userId,
|
||||
username: users.username
|
||||
}
|
||||
})
|
||||
.from(approvals)
|
||||
.innerJoin(users, and(eq(approvals.userId, users.userId)))
|
||||
.leftJoin(
|
||||
clients,
|
||||
and(
|
||||
eq(approvals.clientId, clients.clientId),
|
||||
not(isNull(clients.userId)) // only user devices
|
||||
)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(approvals.orgId, orgId),
|
||||
sql`${approvals.decision} in ${state}`
|
||||
)
|
||||
)
|
||||
.orderBy(
|
||||
sql`CASE ${approvals.decision} WHEN 'pending' THEN 0 ELSE 1 END`,
|
||||
desc(approvals.timestamp)
|
||||
)
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
return res;
|
||||
}
|
||||
|
||||
export type ListApprovalsResponse = {
|
||||
approvals: NonNullable<Awaited<ReturnType<typeof queryApprovals>>>;
|
||||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
export async function listApprovals(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedQuery = querySchema.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedQuery.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
const { limit, offset, approvalState } = parsedQuery.data;
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
if (build === "saas") {
|
||||
const { tier } = await getOrgTierData(orgId);
|
||||
const subscribed = tier === TierId.STANDARD;
|
||||
if (!subscribed) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"This organization's current plan does not support this feature."
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const approvalsList = await queryApprovals(
|
||||
orgId.toString(),
|
||||
limit,
|
||||
offset,
|
||||
approvalState
|
||||
);
|
||||
|
||||
const [{ count }] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(approvals);
|
||||
|
||||
return response<ListApprovalsResponse>(res, {
|
||||
data: {
|
||||
approvals: approvalsList,
|
||||
pagination: {
|
||||
total: count,
|
||||
limit,
|
||||
offset
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Approvals retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
142
server/private/routers/approvals/processPendingApproval.ts
Normal file
142
server/private/routers/approvals/processPendingApproval.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
import { build } from "@server/build";
|
||||
import { approvals, clients, db, orgs, type Approval } from "@server/db";
|
||||
import { getOrgTierData } from "@server/lib/billing";
|
||||
import { TierId } from "@server/lib/billing/tiers";
|
||||
import response from "@server/lib/response";
|
||||
import { and, eq, type InferInsertModel } from "drizzle-orm";
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string(),
|
||||
approvalId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
const bodySchema = z.strictObject({
|
||||
decision: z.enum(["approved", "denied"])
|
||||
});
|
||||
|
||||
export type ProcessApprovalResponse = Approval;
|
||||
|
||||
export async function processPendingApproval(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = bodySchema.safeParse(req.body);
|
||||
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId, approvalId } = parsedParams.data;
|
||||
|
||||
if (build === "saas") {
|
||||
const { tier } = await getOrgTierData(orgId);
|
||||
const subscribed = tier === TierId.STANDARD;
|
||||
if (!subscribed) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"This organization's current plan does not support this feature."
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const updateData = parsedBody.data;
|
||||
|
||||
const approval = await db
|
||||
.select()
|
||||
.from(approvals)
|
||||
.where(
|
||||
and(
|
||||
eq(approvals.approvalId, approvalId),
|
||||
eq(approvals.decision, "pending")
|
||||
)
|
||||
)
|
||||
.innerJoin(orgs, eq(approvals.orgId, approvals.orgId))
|
||||
.limit(1);
|
||||
|
||||
if (approval.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Pending Approval with ID ${approvalId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [updatedApproval] = await db
|
||||
.update(approvals)
|
||||
.set(updateData)
|
||||
.where(eq(approvals.approvalId, approvalId))
|
||||
.returning();
|
||||
|
||||
// Update user device approval state too
|
||||
if (
|
||||
updatedApproval.type === "user_device" &&
|
||||
updatedApproval.clientId
|
||||
) {
|
||||
const updateDataBody: Partial<InferInsertModel<typeof clients>> = {
|
||||
approvalState: updateData.decision
|
||||
};
|
||||
|
||||
if (updateData.decision === "denied") {
|
||||
updateDataBody.blocked = true;
|
||||
}
|
||||
|
||||
await db
|
||||
.update(clients)
|
||||
.set(updateDataBody)
|
||||
.where(eq(clients.clientId, updatedApproval.clientId));
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: updatedApproval,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Approval updated successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import * as generateLicense from "./generatedLicense";
|
||||
import * as logs from "#private/routers/auditLogs";
|
||||
import * as misc from "#private/routers/misc";
|
||||
import * as reKey from "#private/routers/re-key";
|
||||
import * as approval from "#private/routers/approvals";
|
||||
|
||||
import {
|
||||
verifyOrgAccess,
|
||||
@@ -311,6 +312,24 @@ authenticated.get(
|
||||
loginPage.getLoginPage
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/approvals",
|
||||
verifyValidLicense,
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.listApprovals),
|
||||
logActionAudit(ActionsEnum.listApprovals),
|
||||
approval.listApprovals
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/approvals/:approvalId",
|
||||
verifyValidLicense,
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateApprovals),
|
||||
logActionAudit(ActionsEnum.updateApprovals),
|
||||
approval.processPendingApproval
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/login-page-branding",
|
||||
verifyValidLicense,
|
||||
@@ -436,18 +455,18 @@ authenticated.get(
|
||||
|
||||
authenticated.post(
|
||||
"/re-key/:clientId/regenerate-client-secret",
|
||||
verifyClientAccess, // this is first to set the org id
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyClientAccess,
|
||||
verifyUserHasAction(ActionsEnum.reGenerateSecret),
|
||||
reKey.reGenerateClientSecret
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/re-key/:siteId/regenerate-site-secret",
|
||||
verifySiteAccess, // this is first to set the org id
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.reGenerateSecret),
|
||||
reKey.reGenerateSiteSecret
|
||||
);
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
ResourceHeaderAuthExtendedCompatibility,
|
||||
orgs,
|
||||
requestAuditLog,
|
||||
Org
|
||||
} from "@server/db";
|
||||
import {
|
||||
resources,
|
||||
@@ -79,6 +80,7 @@ import { maxmindLookup } from "@server/db/maxmind";
|
||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||
import semver from "semver";
|
||||
import { maxmindAsnLookup } from "@server/db/maxmindAsn";
|
||||
import { checkOrgAccessPolicy } from "@server/lib/checkOrgAccessPolicy";
|
||||
|
||||
// Zod schemas for request validation
|
||||
const getResourceByDomainParamsSchema = z.strictObject({
|
||||
@@ -94,6 +96,12 @@ const getUserOrgRoleParamsSchema = z.strictObject({
|
||||
orgId: z.string().min(1, "Organization ID is required")
|
||||
});
|
||||
|
||||
const getUserOrgSessionVerifySchema = z.strictObject({
|
||||
userId: z.string().min(1, "User ID is required"),
|
||||
orgId: z.string().min(1, "Organization ID is required"),
|
||||
sessionId: z.string().min(1, "Session ID is required")
|
||||
});
|
||||
|
||||
const getRoleResourceAccessParamsSchema = z.strictObject({
|
||||
roleId: z
|
||||
.string()
|
||||
@@ -178,6 +186,7 @@ export type ResourceWithAuth = {
|
||||
password: ResourcePassword | null;
|
||||
headerAuth: ResourceHeaderAuth | null;
|
||||
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
||||
org: Org
|
||||
};
|
||||
|
||||
export type UserSessionWithUser = {
|
||||
@@ -238,7 +247,8 @@ hybridRouter.get(
|
||||
["newt", "local", "wireguard"], // Allow them to use all the site types
|
||||
true, // But don't allow domain namespace resources
|
||||
false, // Dont include login pages,
|
||||
true // allow raw resources
|
||||
true, // allow raw resources
|
||||
false // dont generate maintenance page
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
@@ -503,8 +513,12 @@ hybridRouter.get(
|
||||
)
|
||||
.leftJoin(
|
||||
resourceHeaderAuthExtendedCompatibility,
|
||||
eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId)
|
||||
eq(
|
||||
resourceHeaderAuthExtendedCompatibility.resourceId,
|
||||
resources.resourceId
|
||||
)
|
||||
)
|
||||
.innerJoin(orgs, eq(orgs.orgId, resources.orgId))
|
||||
.where(eq(resources.fullDomain, domain))
|
||||
.limit(1);
|
||||
|
||||
@@ -538,7 +552,9 @@ hybridRouter.get(
|
||||
pincode: result.resourcePincode,
|
||||
password: result.resourcePassword,
|
||||
headerAuth: result.resourceHeaderAuth,
|
||||
headerAuthExtendedCompatibility: result.resourceHeaderAuthExtendedCompatibility
|
||||
headerAuthExtendedCompatibility:
|
||||
result.resourceHeaderAuthExtendedCompatibility,
|
||||
org: result.orgs
|
||||
};
|
||||
|
||||
return response<ResourceWithAuth>(res, {
|
||||
@@ -602,6 +618,16 @@ hybridRouter.get(
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!result) {
|
||||
return response<LoginPage | null>(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Login page not found",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
await checkExitNodeOrg(
|
||||
remoteExitNode.exitNodeId,
|
||||
@@ -617,16 +643,6 @@ hybridRouter.get(
|
||||
);
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return response<LoginPage | null>(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Login page not found",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
}
|
||||
|
||||
return response<LoginPage>(res, {
|
||||
data: result.loginPage,
|
||||
success: true,
|
||||
@@ -818,6 +834,69 @@ hybridRouter.get(
|
||||
}
|
||||
);
|
||||
|
||||
// Get user organization role
|
||||
hybridRouter.get(
|
||||
"/user/:userId/org/:orgId/session/:sessionId/verify",
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const parsedParams = getUserOrgSessionVerifySchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { userId, orgId, sessionId } = parsedParams.data;
|
||||
const remoteExitNode = req.remoteExitNode;
|
||||
|
||||
if (!remoteExitNode || !remoteExitNode.exitNodeId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Remote exit node not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (await checkExitNodeOrg(remoteExitNode.exitNodeId, orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.UNAUTHORIZED,
|
||||
"User is not authorized to access this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const accessPolicy = await checkOrgAccessPolicy({
|
||||
orgId,
|
||||
userId,
|
||||
sessionId
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: accessPolicy,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "User org access policy retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to get user org role"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Check if role has access to resource
|
||||
hybridRouter.get(
|
||||
"/role/:roleId/resource/:resourceId/access",
|
||||
|
||||
@@ -18,7 +18,8 @@ import * as logs from "#private/routers/auditLogs";
|
||||
import {
|
||||
verifyApiKeyHasAction,
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyOrgAccess
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyIdpAccess
|
||||
} from "@server/middlewares";
|
||||
import {
|
||||
verifyValidSubscription,
|
||||
@@ -31,6 +32,8 @@ import {
|
||||
authenticated as a
|
||||
} from "@server/routers/integration";
|
||||
import { logActionAudit } from "#private/middlewares";
|
||||
import config from "#private/lib/config";
|
||||
import { build } from "@server/build";
|
||||
|
||||
export const unauthenticated = ua;
|
||||
export const authenticated = a;
|
||||
@@ -88,3 +91,49 @@ authenticated.get(
|
||||
logActionAudit(ActionsEnum.exportLogs),
|
||||
logs.exportAccessAuditLogs
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/idp/oidc",
|
||||
verifyValidLicense,
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createIdp),
|
||||
logActionAudit(ActionsEnum.createIdp),
|
||||
orgIdp.createOrgOidcIdp
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/idp/:idpId/oidc",
|
||||
verifyValidLicense,
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyIdpAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateIdp),
|
||||
logActionAudit(ActionsEnum.updateIdp),
|
||||
orgIdp.updateOrgOidcIdp
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/org/:orgId/idp/:idpId",
|
||||
verifyValidLicense,
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyIdpAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteIdp),
|
||||
logActionAudit(ActionsEnum.deleteIdp),
|
||||
orgIdp.deleteOrgIdp
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/idp/:idpId",
|
||||
verifyValidLicense,
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyIdpAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getIdp),
|
||||
orgIdp.getOrgIdp
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/idp",
|
||||
verifyValidLicense,
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.listIdps),
|
||||
orgIdp.listOrgIdps
|
||||
);
|
||||
|
||||
@@ -39,4 +39,4 @@ internalRouter.post(
|
||||
|
||||
internalRouter.get(`/license/status`, license.getLicenseStatus);
|
||||
|
||||
internalRouter.get("/maintenance/info", resource.getMaintenanceInfo);
|
||||
internalRouter.get("/maintenance/info", resource.getMaintenanceInfo);
|
||||
|
||||
@@ -29,11 +29,9 @@ import { getOrgTierData } from "#private/lib/billing";
|
||||
import { TierId } from "@server/lib/billing/tiers";
|
||||
import { build } from "@server/build";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
orgId: z.string()
|
||||
})
|
||||
.strict();
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
export async function getLoginPageBranding(
|
||||
req: Request,
|
||||
|
||||
@@ -40,6 +40,11 @@ async function query(orgId: string | undefined, fullDomain: string) {
|
||||
eq(loginPage.loginPageId, loginPageOrg.loginPageId)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...res.loginPage,
|
||||
orgId: res.loginPageOrg.orgId
|
||||
@@ -65,6 +70,11 @@ async function query(orgId: string | undefined, fullDomain: string) {
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...res,
|
||||
orgId: orgLink.orgId
|
||||
|
||||
@@ -48,6 +48,11 @@ async function query(orgId: string) {
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...res,
|
||||
orgId: orgLink.orgs.orgId,
|
||||
|
||||
@@ -28,6 +28,7 @@ import { eq, InferInsertModel } from "drizzle-orm";
|
||||
import { getOrgTierData } from "#private/lib/billing";
|
||||
import { TierId } from "@server/lib/billing/tiers";
|
||||
import { build } from "@server/build";
|
||||
import config from "@server/private/lib/config";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
@@ -94,8 +95,10 @@ export async function upsertLoginPageBranding(
|
||||
typeof loginPageBranding
|
||||
>;
|
||||
|
||||
if (build !== "saas") {
|
||||
// org branding settings are only considered in the saas build
|
||||
if (
|
||||
build !== "saas" &&
|
||||
!config.getRawPrivateConfig().flags.use_org_only_idp
|
||||
) {
|
||||
const { orgTitle, orgSubtitle, ...rest } = updateData;
|
||||
updateData = rest;
|
||||
}
|
||||
|
||||
@@ -43,25 +43,27 @@ const bodySchema = z.strictObject({
|
||||
scopes: z.string().nonempty(),
|
||||
autoProvision: z.boolean().optional(),
|
||||
variant: z.enum(["oidc", "google", "azure"]).optional().default("oidc"),
|
||||
roleMapping: z.string().optional()
|
||||
roleMapping: z.string().optional(),
|
||||
tags: z.string().optional()
|
||||
});
|
||||
|
||||
// registry.registerPath({
|
||||
// method: "put",
|
||||
// path: "/idp/oidc",
|
||||
// description: "Create an OIDC IdP.",
|
||||
// tags: [OpenAPITags.Idp],
|
||||
// request: {
|
||||
// body: {
|
||||
// content: {
|
||||
// "application/json": {
|
||||
// schema: bodySchema
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// responses: {}
|
||||
// });
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/idp/oidc",
|
||||
description: "Create an OIDC IdP for a specific organization.",
|
||||
tags: [OpenAPITags.Idp, OpenAPITags.Org],
|
||||
request: {
|
||||
params: paramsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: bodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function createOrgOidcIdp(
|
||||
req: Request,
|
||||
@@ -103,7 +105,8 @@ export async function createOrgOidcIdp(
|
||||
name,
|
||||
autoProvision,
|
||||
variant,
|
||||
roleMapping
|
||||
roleMapping,
|
||||
tags
|
||||
} = parsedBody.data;
|
||||
|
||||
if (build === "saas") {
|
||||
@@ -131,7 +134,8 @@ export async function createOrgOidcIdp(
|
||||
.values({
|
||||
name,
|
||||
autoProvision,
|
||||
type: "oidc"
|
||||
type: "oidc",
|
||||
tags
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ const paramsSchema = z
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/idp/{idpId}",
|
||||
description: "Delete IDP.",
|
||||
tags: [OpenAPITags.Idp],
|
||||
path: "/org/{orgId}/idp/{idpId}",
|
||||
description: "Delete IDP for a specific organization.",
|
||||
tags: [OpenAPITags.Idp, OpenAPITags.Org],
|
||||
request: {
|
||||
params: paramsSchema
|
||||
},
|
||||
|
||||
@@ -48,16 +48,16 @@ async function query(idpId: number, orgId: string) {
|
||||
return res;
|
||||
}
|
||||
|
||||
// registry.registerPath({
|
||||
// method: "get",
|
||||
// path: "/idp/{idpId}",
|
||||
// description: "Get an IDP by its IDP ID.",
|
||||
// tags: [OpenAPITags.Idp],
|
||||
// request: {
|
||||
// params: paramsSchema
|
||||
// },
|
||||
// responses: {}
|
||||
// });
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/:orgId/idp/:idpId",
|
||||
description: "Get an IDP by its IDP ID for a specific organization.",
|
||||
tags: [OpenAPITags.Idp, OpenAPITags.Org],
|
||||
request: {
|
||||
params: paramsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function getOrgIdp(
|
||||
req: Request,
|
||||
|
||||
@@ -50,7 +50,8 @@ async function query(orgId: string, limit: number, offset: number) {
|
||||
orgId: idpOrg.orgId,
|
||||
name: idp.name,
|
||||
type: idp.type,
|
||||
variant: idpOidcConfig.variant
|
||||
variant: idpOidcConfig.variant,
|
||||
tags: idp.tags
|
||||
})
|
||||
.from(idpOrg)
|
||||
.where(eq(idpOrg.orgId, orgId))
|
||||
@@ -62,16 +63,17 @@ async function query(orgId: string, limit: number, offset: number) {
|
||||
return res;
|
||||
}
|
||||
|
||||
// registry.registerPath({
|
||||
// method: "get",
|
||||
// path: "/idp",
|
||||
// description: "List all IDP in the system.",
|
||||
// tags: [OpenAPITags.Idp],
|
||||
// request: {
|
||||
// query: querySchema
|
||||
// },
|
||||
// responses: {}
|
||||
// });
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/idp",
|
||||
description: "List all IDP for a specific organization.",
|
||||
tags: [OpenAPITags.Idp, OpenAPITags.Org],
|
||||
request: {
|
||||
query: querySchema,
|
||||
params: paramsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function listOrgIdps(
|
||||
req: Request,
|
||||
|
||||
@@ -46,30 +46,31 @@ const bodySchema = z.strictObject({
|
||||
namePath: z.string().optional(),
|
||||
scopes: z.string().optional(),
|
||||
autoProvision: z.boolean().optional(),
|
||||
roleMapping: z.string().optional()
|
||||
roleMapping: z.string().optional(),
|
||||
tags: z.string().optional()
|
||||
});
|
||||
|
||||
export type UpdateOrgIdpResponse = {
|
||||
idpId: number;
|
||||
};
|
||||
|
||||
// registry.registerPath({
|
||||
// method: "post",
|
||||
// path: "/idp/{idpId}/oidc",
|
||||
// description: "Update an OIDC IdP.",
|
||||
// tags: [OpenAPITags.Idp],
|
||||
// request: {
|
||||
// params: paramsSchema,
|
||||
// body: {
|
||||
// content: {
|
||||
// "application/json": {
|
||||
// schema: bodySchema
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// responses: {}
|
||||
// });
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/org/{orgId}/idp/{idpId}/oidc",
|
||||
description: "Update an OIDC IdP for a specific organization.",
|
||||
tags: [OpenAPITags.Idp, OpenAPITags.Org],
|
||||
request: {
|
||||
params: paramsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: bodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function updateOrgOidcIdp(
|
||||
req: Request,
|
||||
@@ -109,7 +110,8 @@ export async function updateOrgOidcIdp(
|
||||
namePath,
|
||||
name,
|
||||
autoProvision,
|
||||
roleMapping
|
||||
roleMapping,
|
||||
tags
|
||||
} = parsedBody.data;
|
||||
|
||||
if (build === "saas") {
|
||||
@@ -167,7 +169,8 @@ export async function updateOrgOidcIdp(
|
||||
await db.transaction(async (trx) => {
|
||||
const idpData = {
|
||||
name,
|
||||
autoProvision
|
||||
autoProvision,
|
||||
tags
|
||||
};
|
||||
|
||||
// only update if at least one key is not undefined
|
||||
|
||||
@@ -11,4 +11,4 @@
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
export * from "./getMaintenanceInfo";
|
||||
export * from "./getMaintenanceInfo";
|
||||
|
||||
@@ -43,7 +43,8 @@ import {
|
||||
WSMessage,
|
||||
TokenPayload,
|
||||
WebSocketRequest,
|
||||
RedisMessage
|
||||
RedisMessage,
|
||||
SendMessageOptions
|
||||
} from "@server/routers/ws";
|
||||
import { validateSessionToken } from "@server/auth/sessions/app";
|
||||
|
||||
@@ -118,12 +119,21 @@ const processMessage = async (
|
||||
if (response.broadcast) {
|
||||
await broadcastToAllExcept(
|
||||
response.message,
|
||||
response.excludeSender ? clientId : undefined
|
||||
response.excludeSender ? clientId : undefined,
|
||||
response.options
|
||||
);
|
||||
} else if (response.targetClientId) {
|
||||
await sendToClient(response.targetClientId, response.message);
|
||||
await sendToClient(
|
||||
response.targetClientId,
|
||||
response.message,
|
||||
response.options
|
||||
);
|
||||
} else {
|
||||
ws.send(JSON.stringify(response.message));
|
||||
await sendToClient(
|
||||
clientId,
|
||||
response.message,
|
||||
response.options
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -172,6 +182,9 @@ const REDIS_CHANNEL = "websocket_messages";
|
||||
// Client tracking map (local to this node)
|
||||
const connectedClients: Map<string, AuthenticatedWebSocket[]> = new Map();
|
||||
|
||||
// Config version tracking map (local to this node, resets on server restart)
|
||||
const clientConfigVersions: Map<string, number> = new Map();
|
||||
|
||||
// Recovery tracking
|
||||
let isRedisRecoveryInProgress = false;
|
||||
|
||||
@@ -182,6 +195,8 @@ const getClientMapKey = (clientId: string) => clientId;
|
||||
const getConnectionsKey = (clientId: string) => `ws:connections:${clientId}`;
|
||||
const getNodeConnectionsKey = (nodeId: string, clientId: string) =>
|
||||
`ws:node:${nodeId}:${clientId}`;
|
||||
const getConfigVersionKey = (clientId: string) =>
|
||||
`ws:configVersion:${clientId}`;
|
||||
|
||||
// Initialize Redis subscription for cross-node messaging
|
||||
const initializeRedisSubscription = async (): Promise<void> => {
|
||||
@@ -304,6 +319,45 @@ const addClient = async (
|
||||
existingClients.push(ws);
|
||||
connectedClients.set(mapKey, existingClients);
|
||||
|
||||
// Get or initialize config version
|
||||
let configVersion = 0;
|
||||
|
||||
// Check Redis first if enabled
|
||||
if (redisManager.isRedisEnabled()) {
|
||||
try {
|
||||
const redisVersion = await redisManager.get(getConfigVersionKey(clientId));
|
||||
if (redisVersion !== null) {
|
||||
configVersion = parseInt(redisVersion, 10);
|
||||
// Sync to local cache
|
||||
clientConfigVersions.set(clientId, configVersion);
|
||||
} else if (!clientConfigVersions.has(clientId)) {
|
||||
// No version in Redis or local cache, initialize to 0
|
||||
await redisManager.set(getConfigVersionKey(clientId), "0");
|
||||
clientConfigVersions.set(clientId, 0);
|
||||
} else {
|
||||
// Use local cache version and sync to Redis
|
||||
configVersion = clientConfigVersions.get(clientId) || 0;
|
||||
await redisManager.set(getConfigVersionKey(clientId), configVersion.toString());
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to get/set config version in Redis:", error);
|
||||
// Fall back to local cache
|
||||
if (!clientConfigVersions.has(clientId)) {
|
||||
clientConfigVersions.set(clientId, 0);
|
||||
}
|
||||
configVersion = clientConfigVersions.get(clientId) || 0;
|
||||
}
|
||||
} else {
|
||||
// Redis not enabled, use local cache only
|
||||
if (!clientConfigVersions.has(clientId)) {
|
||||
clientConfigVersions.set(clientId, 0);
|
||||
}
|
||||
configVersion = clientConfigVersions.get(clientId) || 0;
|
||||
}
|
||||
|
||||
// Set config version on websocket
|
||||
ws.configVersion = configVersion;
|
||||
|
||||
// Add to Redis tracking if enabled
|
||||
if (redisManager.isRedisEnabled()) {
|
||||
try {
|
||||
@@ -322,7 +376,7 @@ const addClient = async (
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Client added to tracking - ${clientType.toUpperCase()} ID: ${clientId}, Connection ID: ${connectionId}, Total connections: ${existingClients.length}`
|
||||
`Client added to tracking - ${clientType.toUpperCase()} ID: ${clientId}, Connection ID: ${connectionId}, Total connections: ${existingClients.length}, Config version: ${configVersion}`
|
||||
);
|
||||
};
|
||||
|
||||
@@ -377,53 +431,133 @@ const removeClient = async (
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to get the current config version for a client
|
||||
const getClientConfigVersion = async (clientId: string): Promise<number | undefined> => {
|
||||
// Try Redis first if available
|
||||
if (redisManager.isRedisEnabled()) {
|
||||
try {
|
||||
const redisVersion = await redisManager.get(
|
||||
getConfigVersionKey(clientId)
|
||||
);
|
||||
if (redisVersion !== null) {
|
||||
const version = parseInt(redisVersion, 10);
|
||||
// Sync local cache with Redis
|
||||
clientConfigVersions.set(clientId, version);
|
||||
return version;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to get config version from Redis:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to local cache
|
||||
return clientConfigVersions.get(clientId);
|
||||
};
|
||||
|
||||
// Helper to increment and get the new config version for a client
|
||||
const incrementClientConfigVersion = async (
|
||||
clientId: string
|
||||
): Promise<number> => {
|
||||
let newVersion: number;
|
||||
|
||||
if (redisManager.isRedisEnabled()) {
|
||||
try {
|
||||
// Use Redis INCR for atomic increment across nodes
|
||||
newVersion = await redisManager.incr(getConfigVersionKey(clientId));
|
||||
// Sync local cache
|
||||
clientConfigVersions.set(clientId, newVersion);
|
||||
return newVersion;
|
||||
} catch (error) {
|
||||
logger.error("Failed to increment config version in Redis:", error);
|
||||
// Fall through to local increment
|
||||
}
|
||||
}
|
||||
|
||||
// Local increment
|
||||
const currentVersion = clientConfigVersions.get(clientId) || 0;
|
||||
newVersion = currentVersion + 1;
|
||||
clientConfigVersions.set(clientId, newVersion);
|
||||
return newVersion;
|
||||
};
|
||||
|
||||
// Local message sending (within this node)
|
||||
const sendToClientLocal = async (
|
||||
clientId: string,
|
||||
message: WSMessage
|
||||
message: WSMessage,
|
||||
options: SendMessageOptions = {}
|
||||
): Promise<boolean> => {
|
||||
const mapKey = getClientMapKey(clientId);
|
||||
const clients = connectedClients.get(mapKey);
|
||||
if (!clients || clients.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const messageString = JSON.stringify(message);
|
||||
|
||||
// Handle config version
|
||||
let configVersion = await getClientConfigVersion(clientId);
|
||||
|
||||
// Add config version to message
|
||||
const messageWithVersion = {
|
||||
...message,
|
||||
configVersion
|
||||
};
|
||||
|
||||
const messageString = JSON.stringify(messageWithVersion);
|
||||
clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(messageString);
|
||||
}
|
||||
});
|
||||
|
||||
logger.debug(
|
||||
`sendToClient: Message type ${message.type} sent to clientId ${clientId}`
|
||||
);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const broadcastToAllExceptLocal = async (
|
||||
message: WSMessage,
|
||||
excludeClientId?: string
|
||||
excludeClientId?: string,
|
||||
options: SendMessageOptions = {}
|
||||
): Promise<void> => {
|
||||
connectedClients.forEach((clients, mapKey) => {
|
||||
for (const [mapKey, clients] of connectedClients.entries()) {
|
||||
const [type, id] = mapKey.split(":");
|
||||
if (!(excludeClientId && id === excludeClientId)) {
|
||||
const clientId = mapKey; // mapKey is the clientId
|
||||
if (!(excludeClientId && clientId === excludeClientId)) {
|
||||
// Handle config version per client
|
||||
let configVersion = await getClientConfigVersion(clientId);
|
||||
if (options.incrementConfigVersion) {
|
||||
configVersion = await incrementClientConfigVersion(clientId);
|
||||
}
|
||||
|
||||
// Add config version to message
|
||||
const messageWithVersion = {
|
||||
...message,
|
||||
configVersion
|
||||
};
|
||||
|
||||
clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify(message));
|
||||
client.send(JSON.stringify(messageWithVersion));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Cross-node message sending (via Redis)
|
||||
const sendToClient = async (
|
||||
clientId: string,
|
||||
message: WSMessage
|
||||
message: WSMessage,
|
||||
options: SendMessageOptions = {}
|
||||
): Promise<boolean> => {
|
||||
let configVersion = await getClientConfigVersion(clientId);
|
||||
if (options.incrementConfigVersion) {
|
||||
configVersion = await incrementClientConfigVersion(clientId);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`sendToClient: Message type ${message.type} sent to clientId ${clientId} (new configVersion: ${configVersion})`
|
||||
);
|
||||
|
||||
// Try to send locally first
|
||||
const localSent = await sendToClientLocal(clientId, message);
|
||||
const localSent = await sendToClientLocal(clientId, message, options);
|
||||
|
||||
// Only send via Redis if the client is not connected locally and Redis is enabled
|
||||
if (!localSent && redisManager.isRedisEnabled()) {
|
||||
@@ -431,7 +565,10 @@ const sendToClient = async (
|
||||
const redisMessage: RedisMessage = {
|
||||
type: "direct",
|
||||
targetClientId: clientId,
|
||||
message,
|
||||
message: {
|
||||
...message,
|
||||
configVersion
|
||||
},
|
||||
fromNodeId: NODE_ID
|
||||
};
|
||||
|
||||
@@ -458,19 +595,22 @@ const sendToClient = async (
|
||||
|
||||
const broadcastToAllExcept = async (
|
||||
message: WSMessage,
|
||||
excludeClientId?: string
|
||||
excludeClientId?: string,
|
||||
options: SendMessageOptions = {}
|
||||
): Promise<void> => {
|
||||
// Broadcast locally
|
||||
await broadcastToAllExceptLocal(message, excludeClientId);
|
||||
await broadcastToAllExceptLocal(message, excludeClientId, options);
|
||||
|
||||
// If Redis is enabled, also broadcast via Redis pub/sub to other nodes
|
||||
// Note: For broadcasts, we include the options so remote nodes can handle versioning
|
||||
if (redisManager.isRedisEnabled()) {
|
||||
try {
|
||||
const redisMessage: RedisMessage = {
|
||||
type: "broadcast",
|
||||
excludeClientId,
|
||||
message,
|
||||
fromNodeId: NODE_ID
|
||||
fromNodeId: NODE_ID,
|
||||
options
|
||||
};
|
||||
|
||||
await redisManager.publish(
|
||||
@@ -936,5 +1076,6 @@ export {
|
||||
getActiveNodes,
|
||||
disconnectClient,
|
||||
NODE_ID,
|
||||
cleanup
|
||||
cleanup,
|
||||
getClientConfigVersion
|
||||
};
|
||||
|
||||
@@ -99,12 +99,13 @@ async function query(query: Q) {
|
||||
.where(and(baseConditions, not(isNull(requestAuditLog.location))))
|
||||
.groupBy(requestAuditLog.location)
|
||||
.orderBy(desc(totalQ))
|
||||
.limit(DISTINCT_LIMIT+1);
|
||||
.limit(DISTINCT_LIMIT + 1);
|
||||
|
||||
if (requestsPerCountry.length > DISTINCT_LIMIT) {
|
||||
// throw an error
|
||||
throw createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
// todo: is this even possible?
|
||||
`Too many distinct countries. Please narrow your query.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -189,22 +189,22 @@ async function queryUniqueFilterAttributes(
|
||||
.selectDistinct({ actor: requestAuditLog.actor })
|
||||
.from(requestAuditLog)
|
||||
.where(baseConditions)
|
||||
.limit(DISTINCT_LIMIT+1),
|
||||
.limit(DISTINCT_LIMIT + 1),
|
||||
primaryDb
|
||||
.selectDistinct({ locations: requestAuditLog.location })
|
||||
.from(requestAuditLog)
|
||||
.where(baseConditions)
|
||||
.limit(DISTINCT_LIMIT+1),
|
||||
.limit(DISTINCT_LIMIT + 1),
|
||||
primaryDb
|
||||
.selectDistinct({ hosts: requestAuditLog.host })
|
||||
.from(requestAuditLog)
|
||||
.where(baseConditions)
|
||||
.limit(DISTINCT_LIMIT+1),
|
||||
.limit(DISTINCT_LIMIT + 1),
|
||||
primaryDb
|
||||
.selectDistinct({ paths: requestAuditLog.path })
|
||||
.from(requestAuditLog)
|
||||
.where(baseConditions)
|
||||
.limit(DISTINCT_LIMIT+1),
|
||||
.limit(DISTINCT_LIMIT + 1),
|
||||
primaryDb
|
||||
.selectDistinct({
|
||||
id: requestAuditLog.resourceId,
|
||||
@@ -216,18 +216,20 @@ async function queryUniqueFilterAttributes(
|
||||
eq(requestAuditLog.resourceId, resources.resourceId)
|
||||
)
|
||||
.where(baseConditions)
|
||||
.limit(DISTINCT_LIMIT+1)
|
||||
.limit(DISTINCT_LIMIT + 1)
|
||||
]);
|
||||
|
||||
if (
|
||||
uniqueActors.length > DISTINCT_LIMIT ||
|
||||
uniqueLocations.length > DISTINCT_LIMIT ||
|
||||
uniqueHosts.length > DISTINCT_LIMIT ||
|
||||
uniquePaths.length > DISTINCT_LIMIT ||
|
||||
uniqueResources.length > DISTINCT_LIMIT
|
||||
) {
|
||||
throw new Error("Too many distinct filter attributes to retrieve. Please refine your time range.");
|
||||
}
|
||||
// TODO: for stuff like the paths this is too restrictive so lets just show some of the paths and the user needs to
|
||||
// refine the time range to see what they need to see
|
||||
// if (
|
||||
// uniqueActors.length > DISTINCT_LIMIT ||
|
||||
// uniqueLocations.length > DISTINCT_LIMIT ||
|
||||
// uniqueHosts.length > DISTINCT_LIMIT ||
|
||||
// uniquePaths.length > DISTINCT_LIMIT ||
|
||||
// uniqueResources.length > DISTINCT_LIMIT
|
||||
// ) {
|
||||
// throw new Error("Too many distinct filter attributes to retrieve. Please refine your time range.");
|
||||
// }
|
||||
|
||||
return {
|
||||
actors: uniqueActors
|
||||
@@ -307,10 +309,12 @@ export async function queryRequestAuditLogs(
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
// if the message is "Too many distinct filter attributes to retrieve. Please refine your time range.", return a 400 and the message
|
||||
if (error instanceof Error && error.message === "Too many distinct filter attributes to retrieve. Please refine your time range.") {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, error.message)
|
||||
);
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message ===
|
||||
"Too many distinct filter attributes to retrieve. Please refine your time range."
|
||||
) {
|
||||
return next(createHttpError(HttpCode.BAD_REQUEST, error.message));
|
||||
}
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
|
||||
@@ -17,3 +17,4 @@ export * from "./securityKey";
|
||||
export * from "./startDeviceWebAuth";
|
||||
export * from "./verifyDeviceWebAuth";
|
||||
export * from "./pollDeviceWebAuth";
|
||||
export * from "./lookupUser";
|
||||
224
server/routers/auth/lookupUser.ts
Normal file
224
server/routers/auth/lookupUser.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import {
|
||||
users,
|
||||
userOrgs,
|
||||
orgs,
|
||||
idpOrg,
|
||||
idp,
|
||||
idpOidcConfig
|
||||
} from "@server/db";
|
||||
import { eq, or, sql, and, isNotNull, inArray } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
|
||||
const lookupBodySchema = z.strictObject({
|
||||
identifier: z.string().min(1).toLowerCase()
|
||||
});
|
||||
|
||||
export type LookupUserResponse = {
|
||||
found: boolean;
|
||||
identifier: string;
|
||||
accounts: Array<{
|
||||
userId: string;
|
||||
email: string | null;
|
||||
username: string;
|
||||
hasInternalAuth: boolean;
|
||||
orgs: Array<{
|
||||
orgId: string;
|
||||
orgName: string;
|
||||
idps: Array<{
|
||||
idpId: number;
|
||||
name: string;
|
||||
variant: string | null;
|
||||
}>;
|
||||
hasInternalAuth: boolean;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
|
||||
// registry.registerPath({
|
||||
// method: "post",
|
||||
// path: "/auth/lookup-user",
|
||||
// description: "Lookup user accounts by username or email and return available authentication methods.",
|
||||
// tags: [OpenAPITags.Auth],
|
||||
// request: {
|
||||
// body: lookupBodySchema
|
||||
// },
|
||||
// responses: {}
|
||||
// });
|
||||
|
||||
export async function lookupUser(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedBody = lookupBodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { identifier } = parsedBody.data;
|
||||
|
||||
// Query users matching identifier (case-insensitive)
|
||||
// Match by username OR email
|
||||
const matchingUsers = await db
|
||||
.select({
|
||||
userId: users.userId,
|
||||
email: users.email,
|
||||
username: users.username,
|
||||
type: users.type,
|
||||
passwordHash: users.passwordHash,
|
||||
idpId: users.idpId
|
||||
})
|
||||
.from(users)
|
||||
.where(
|
||||
or(
|
||||
sql`LOWER(${users.username}) = ${identifier}`,
|
||||
sql`LOWER(${users.email}) = ${identifier}`
|
||||
)
|
||||
);
|
||||
|
||||
if (!matchingUsers || matchingUsers.length === 0) {
|
||||
return response<LookupUserResponse>(res, {
|
||||
data: {
|
||||
found: false,
|
||||
identifier,
|
||||
accounts: []
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "No accounts found",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
}
|
||||
|
||||
// Get unique user IDs
|
||||
const userIds = [...new Set(matchingUsers.map((u) => u.userId))];
|
||||
|
||||
// Get all org memberships for these users
|
||||
const orgMemberships = await db
|
||||
.select({
|
||||
userId: userOrgs.userId,
|
||||
orgId: userOrgs.orgId,
|
||||
orgName: orgs.name
|
||||
})
|
||||
.from(userOrgs)
|
||||
.innerJoin(orgs, eq(orgs.orgId, userOrgs.orgId))
|
||||
.where(inArray(userOrgs.userId, userIds));
|
||||
|
||||
// Get unique org IDs
|
||||
const orgIds = [...new Set(orgMemberships.map((m) => m.orgId))];
|
||||
|
||||
// Get all IdPs for these orgs
|
||||
const orgIdps =
|
||||
orgIds.length > 0
|
||||
? await db
|
||||
.select({
|
||||
orgId: idpOrg.orgId,
|
||||
idpId: idp.idpId,
|
||||
idpName: idp.name,
|
||||
variant: idpOidcConfig.variant
|
||||
})
|
||||
.from(idpOrg)
|
||||
.innerJoin(idp, eq(idp.idpId, idpOrg.idpId))
|
||||
.innerJoin(
|
||||
idpOidcConfig,
|
||||
eq(idpOidcConfig.idpId, idp.idpId)
|
||||
)
|
||||
.where(inArray(idpOrg.orgId, orgIds))
|
||||
: [];
|
||||
|
||||
// Build response structure
|
||||
const accounts: LookupUserResponse["accounts"] = [];
|
||||
|
||||
for (const user of matchingUsers) {
|
||||
const hasInternalAuth =
|
||||
user.type === UserType.Internal && user.passwordHash !== null;
|
||||
|
||||
// Get orgs for this user
|
||||
const userOrgMemberships = orgMemberships.filter(
|
||||
(m) => m.userId === user.userId
|
||||
);
|
||||
|
||||
// Deduplicate orgs (user might have multiple memberships in same org)
|
||||
const uniqueOrgs = new Map<string, typeof userOrgMemberships[0]>();
|
||||
for (const membership of userOrgMemberships) {
|
||||
if (!uniqueOrgs.has(membership.orgId)) {
|
||||
uniqueOrgs.set(membership.orgId, membership);
|
||||
}
|
||||
}
|
||||
|
||||
const orgsData = Array.from(uniqueOrgs.values()).map((membership) => {
|
||||
// Get IdPs for this org where the user (with the exact identifier) is authenticated via that IdP
|
||||
// Only show IdPs where the user's idpId matches
|
||||
// Internal users don't have an idpId, so they won't see any IdPs
|
||||
const orgIdpsList = orgIdps
|
||||
.filter((idp) => {
|
||||
if (idp.orgId !== membership.orgId) {
|
||||
return false;
|
||||
}
|
||||
// Only show IdPs where the user (with exact identifier) is authenticated via that IdP
|
||||
// This means user.idpId must match idp.idpId
|
||||
if (user.idpId !== null && user.idpId === idp.idpId) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map((idp) => ({
|
||||
idpId: idp.idpId,
|
||||
name: idp.idpName,
|
||||
variant: idp.variant
|
||||
}));
|
||||
|
||||
// Check if user has internal auth for this org
|
||||
// User has internal auth if they have an internal account type
|
||||
const orgHasInternalAuth = hasInternalAuth;
|
||||
|
||||
return {
|
||||
orgId: membership.orgId,
|
||||
orgName: membership.orgName,
|
||||
idps: orgIdpsList,
|
||||
hasInternalAuth: orgHasInternalAuth
|
||||
};
|
||||
});
|
||||
|
||||
accounts.push({
|
||||
userId: user.userId,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
hasInternalAuth,
|
||||
orgs: orgsData
|
||||
});
|
||||
}
|
||||
|
||||
return response<LookupUserResponse>(res, {
|
||||
data: {
|
||||
found: true,
|
||||
identifier,
|
||||
accounts
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "User lookup completed",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { eq, and, gt } from "drizzle-orm";
|
||||
import { createSession, generateSessionToken } from "@server/auth/sessions/app";
|
||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { stripPortFromHost } from "@server/lib/ip";
|
||||
|
||||
const paramsSchema = z.object({
|
||||
code: z.string().min(1, "Code is required")
|
||||
@@ -27,30 +28,6 @@ export type PollDeviceWebAuthResponse = {
|
||||
token?: string;
|
||||
};
|
||||
|
||||
// Helper function to extract IP from request (same as in startDeviceWebAuth)
|
||||
function extractIpFromRequest(req: Request): string | undefined {
|
||||
const ip = req.ip || req.socket.remoteAddress;
|
||||
if (!ip) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Handle IPv6 format [::1] or IPv4 format
|
||||
if (ip.startsWith("[") && ip.includes("]")) {
|
||||
const ipv6Match = ip.match(/\[(.*?)\]/);
|
||||
if (ipv6Match) {
|
||||
return ipv6Match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Handle IPv4 with port (split at last colon)
|
||||
const lastColonIndex = ip.lastIndexOf(":");
|
||||
if (lastColonIndex !== -1) {
|
||||
return ip.substring(0, lastColonIndex);
|
||||
}
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
export async function pollDeviceWebAuth(
|
||||
req: Request,
|
||||
res: Response,
|
||||
@@ -70,7 +47,7 @@ export async function pollDeviceWebAuth(
|
||||
try {
|
||||
const { code } = parsedParams.data;
|
||||
const now = Date.now();
|
||||
const requestIp = extractIpFromRequest(req);
|
||||
const requestIp = req.ip ? stripPortFromHost(req.ip) : undefined;
|
||||
|
||||
// Hash the code before querying
|
||||
const hashedCode = hashDeviceCode(code);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { TimeSpan } from "oslo";
|
||||
import { maxmindLookup } from "@server/db/maxmind";
|
||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { stripPortFromHost } from "@server/lib/ip";
|
||||
|
||||
const bodySchema = z
|
||||
.object({
|
||||
@@ -39,30 +40,6 @@ function hashDeviceCode(code: string): string {
|
||||
return encodeHexLowerCase(sha256(new TextEncoder().encode(code)));
|
||||
}
|
||||
|
||||
// Helper function to extract IP from request
|
||||
function extractIpFromRequest(req: Request): string | undefined {
|
||||
const ip = req.ip;
|
||||
if (!ip) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Handle IPv6 format [::1] or IPv4 format
|
||||
if (ip.startsWith("[") && ip.includes("]")) {
|
||||
const ipv6Match = ip.match(/\[(.*?)\]/);
|
||||
if (ipv6Match) {
|
||||
return ipv6Match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Handle IPv4 with port (split at last colon)
|
||||
const lastColonIndex = ip.lastIndexOf(":");
|
||||
if (lastColonIndex !== -1) {
|
||||
return ip.substring(0, lastColonIndex);
|
||||
}
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
// Helper function to get city from IP (if available)
|
||||
async function getCityFromIp(ip: string): Promise<string | undefined> {
|
||||
try {
|
||||
@@ -112,7 +89,7 @@ export async function startDeviceWebAuth(
|
||||
const hashedCode = hashDeviceCode(code);
|
||||
|
||||
// Extract IP from request
|
||||
const ip = extractIpFromRequest(req);
|
||||
const ip = req.ip ? stripPortFromHost(req.ip) : undefined;
|
||||
|
||||
// Get city (optional, may return undefined)
|
||||
const city = ip ? await getCityFromIp(ip) : undefined;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { eq, and, gt } from "drizzle-orm";
|
||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { unauthorized } from "@server/auth/unauthorizedResponse";
|
||||
import { getIosDeviceName, getMacDeviceName } from "@server/db/names";
|
||||
|
||||
const bodySchema = z
|
||||
.object({
|
||||
@@ -120,6 +121,11 @@ export async function verifyDeviceWebAuth(
|
||||
);
|
||||
}
|
||||
|
||||
const deviceName =
|
||||
getMacDeviceName(deviceCode.deviceName) ||
|
||||
getIosDeviceName(deviceCode.deviceName) ||
|
||||
deviceCode.deviceName;
|
||||
|
||||
// If verify is false, just return metadata without verifying
|
||||
if (!verify) {
|
||||
return response<VerifyDeviceWebAuthResponse>(res, {
|
||||
@@ -129,7 +135,7 @@ export async function verifyDeviceWebAuth(
|
||||
metadata: {
|
||||
ip: deviceCode.ip,
|
||||
city: deviceCode.city,
|
||||
deviceName: deviceCode.deviceName,
|
||||
deviceName: deviceName,
|
||||
applicationName: deviceCode.applicationName,
|
||||
createdAt: deviceCode.createdAt
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { SESSION_COOKIE_EXPIRES as RESOURCE_SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/resource";
|
||||
import config from "@server/lib/config";
|
||||
import { response } from "@server/lib/response";
|
||||
import { stripPortFromHost } from "@server/lib/ip";
|
||||
|
||||
const exchangeSessionBodySchema = z.object({
|
||||
requestToken: z.string(),
|
||||
@@ -62,7 +63,7 @@ export async function exchangeSession(
|
||||
cleanHost = cleanHost.slice(0, -1 * matched.length);
|
||||
}
|
||||
|
||||
const clientIp = requestIp?.split(":")[0];
|
||||
const clientIp = requestIp ? stripPortFromHost(requestIp) : undefined;
|
||||
|
||||
const [resource] = await db
|
||||
.select()
|
||||
|
||||
@@ -3,6 +3,7 @@ import logger from "@server/logger";
|
||||
import { and, eq, lt } from "drizzle-orm";
|
||||
import cache from "@server/lib/cache";
|
||||
import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs";
|
||||
import { stripPortFromHost } from "@server/lib/ip";
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,27 +49,43 @@ const auditLogBuffer: Array<{
|
||||
|
||||
const BATCH_SIZE = 100; // Write to DB every 100 logs
|
||||
const BATCH_INTERVAL_MS = 5000; // Or every 5 seconds, whichever comes first
|
||||
const MAX_BUFFER_SIZE = 10000; // Prevent unbounded memory growth
|
||||
let flushTimer: NodeJS.Timeout | null = null;
|
||||
let isFlushInProgress = false;
|
||||
|
||||
/**
|
||||
* Flush buffered logs to database
|
||||
*/
|
||||
async function flushAuditLogs() {
|
||||
if (auditLogBuffer.length === 0) {
|
||||
if (auditLogBuffer.length === 0 || isFlushInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
isFlushInProgress = true;
|
||||
|
||||
// Take all current logs and clear buffer
|
||||
const logsToWrite = auditLogBuffer.splice(0, auditLogBuffer.length);
|
||||
|
||||
try {
|
||||
// Batch insert all logs at once
|
||||
await db.insert(requestAuditLog).values(logsToWrite);
|
||||
// Batch insert logs in groups of 25 to avoid overwhelming the database
|
||||
const BATCH_DB_SIZE = 25;
|
||||
for (let i = 0; i < logsToWrite.length; i += BATCH_DB_SIZE) {
|
||||
const batch = logsToWrite.slice(i, i + BATCH_DB_SIZE);
|
||||
await db.insert(requestAuditLog).values(batch);
|
||||
}
|
||||
logger.debug(`Flushed ${logsToWrite.length} audit logs to database`);
|
||||
} catch (error) {
|
||||
logger.error("Error flushing audit logs:", error);
|
||||
// On error, we lose these logs - consider a fallback strategy if needed
|
||||
// (e.g., write to file, or put back in buffer with retry limit)
|
||||
} finally {
|
||||
isFlushInProgress = false;
|
||||
// If buffer filled up while we were flushing, flush again
|
||||
if (auditLogBuffer.length >= BATCH_SIZE) {
|
||||
flushAuditLogs().catch((err) =>
|
||||
logger.error("Error in follow-up flush:", err)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +111,10 @@ export async function shutdownAuditLogger() {
|
||||
clearTimeout(flushTimer);
|
||||
flushTimer = null;
|
||||
}
|
||||
// Force flush even if one is in progress by waiting and retrying
|
||||
while (isFlushInProgress) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
await flushAuditLogs();
|
||||
}
|
||||
|
||||
@@ -208,28 +229,17 @@ export async function logRequestAudit(
|
||||
}
|
||||
|
||||
const clientIp = body.requestIp
|
||||
? (() => {
|
||||
if (
|
||||
body.requestIp.startsWith("[") &&
|
||||
body.requestIp.includes("]")
|
||||
) {
|
||||
// if brackets are found, extract the IPv6 address from between the brackets
|
||||
const ipv6Match = body.requestIp.match(/\[(.*?)\]/);
|
||||
if (ipv6Match) {
|
||||
return ipv6Match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// ivp4
|
||||
// split at last colon
|
||||
const lastColonIndex = body.requestIp.lastIndexOf(":");
|
||||
if (lastColonIndex !== -1) {
|
||||
return body.requestIp.substring(0, lastColonIndex);
|
||||
}
|
||||
return body.requestIp;
|
||||
})()
|
||||
? stripPortFromHost(body.requestIp)
|
||||
: undefined;
|
||||
|
||||
// Prevent unbounded buffer growth - drop oldest entries if buffer is too large
|
||||
if (auditLogBuffer.length >= MAX_BUFFER_SIZE) {
|
||||
const dropped = auditLogBuffer.splice(0, BATCH_SIZE);
|
||||
logger.warn(
|
||||
`Audit log buffer exceeded max size (${MAX_BUFFER_SIZE}), dropped ${dropped.length} oldest entries`
|
||||
);
|
||||
}
|
||||
|
||||
// Add to buffer instead of writing directly to DB
|
||||
auditLogBuffer.push({
|
||||
timestamp,
|
||||
|
||||
@@ -13,14 +13,15 @@ import {
|
||||
LoginPage,
|
||||
Org,
|
||||
Resource,
|
||||
ResourceHeaderAuth, ResourceHeaderAuthExtendedCompatibility,
|
||||
ResourceHeaderAuth,
|
||||
ResourceHeaderAuthExtendedCompatibility,
|
||||
ResourcePassword,
|
||||
ResourcePincode,
|
||||
ResourceRule,
|
||||
resourceSessions
|
||||
} from "@server/db";
|
||||
import config from "@server/lib/config";
|
||||
import { isIpInCidr } from "@server/lib/ip";
|
||||
import { isIpInCidr, stripPortFromHost } from "@server/lib/ip";
|
||||
import { response } from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -39,6 +40,8 @@ import {
|
||||
} from "#dynamic/lib/checkOrgAccessPolicy";
|
||||
import { logRequestAudit } from "./logRequestAudit";
|
||||
import cache from "@server/lib/cache";
|
||||
import semver from "semver";
|
||||
import { APP_VERSION } from "@server/lib/consts";
|
||||
|
||||
const verifyResourceSessionSchema = z.object({
|
||||
sessions: z.record(z.string(), z.string()).optional(),
|
||||
@@ -50,7 +53,8 @@ const verifyResourceSessionSchema = z.object({
|
||||
path: z.string(),
|
||||
method: z.string(),
|
||||
tls: z.boolean(),
|
||||
requestIp: z.string().optional()
|
||||
requestIp: z.string().optional(),
|
||||
badgerVersion: z.string().optional()
|
||||
});
|
||||
|
||||
export type VerifyResourceSessionSchema = z.infer<
|
||||
@@ -69,6 +73,7 @@ export type VerifyUserResponse = {
|
||||
headerAuthChallenged?: boolean;
|
||||
redirectUrl?: string;
|
||||
userData?: BasicUserData;
|
||||
pangolinVersion?: string;
|
||||
};
|
||||
|
||||
export async function verifyResourceSession(
|
||||
@@ -97,31 +102,15 @@ export async function verifyResourceSession(
|
||||
requestIp,
|
||||
path,
|
||||
headers,
|
||||
query
|
||||
query,
|
||||
badgerVersion
|
||||
} = parsedBody.data;
|
||||
|
||||
// Extract HTTP Basic Auth credentials if present
|
||||
const clientHeaderAuth = extractBasicAuth(headers);
|
||||
|
||||
const clientIp = requestIp
|
||||
? (() => {
|
||||
logger.debug("Request IP:", { requestIp });
|
||||
if (requestIp.startsWith("[") && requestIp.includes("]")) {
|
||||
// if brackets are found, extract the IPv6 address from between the brackets
|
||||
const ipv6Match = requestIp.match(/\[(.*?)\]/);
|
||||
if (ipv6Match) {
|
||||
return ipv6Match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// ivp4
|
||||
// split at last colon
|
||||
const lastColonIndex = requestIp.lastIndexOf(":");
|
||||
if (lastColonIndex !== -1) {
|
||||
return requestIp.substring(0, lastColonIndex);
|
||||
}
|
||||
return requestIp;
|
||||
})()
|
||||
? stripPortFromHost(requestIp, badgerVersion)
|
||||
: undefined;
|
||||
|
||||
logger.debug("Client IP:", { clientIp });
|
||||
@@ -130,9 +119,7 @@ export async function verifyResourceSession(
|
||||
? await getCountryCodeFromIp(clientIp)
|
||||
: undefined;
|
||||
|
||||
const ipAsn = clientIp
|
||||
? await getAsnFromIp(clientIp)
|
||||
: undefined;
|
||||
const ipAsn = clientIp ? await getAsnFromIp(clientIp) : undefined;
|
||||
|
||||
let cleanHost = host;
|
||||
// if the host ends with :port, strip it
|
||||
@@ -178,7 +165,13 @@ export async function verifyResourceSession(
|
||||
cache.set(resourceCacheKey, resourceData, 5);
|
||||
}
|
||||
|
||||
const { resource, pincode, password, headerAuth, headerAuthExtendedCompatibility } = resourceData;
|
||||
const {
|
||||
resource,
|
||||
pincode,
|
||||
password,
|
||||
headerAuth,
|
||||
headerAuthExtendedCompatibility
|
||||
} = resourceData;
|
||||
|
||||
if (!resource) {
|
||||
logger.debug(`Resource not found ${cleanHost}`);
|
||||
@@ -474,8 +467,7 @@ export async function verifyResourceSession(
|
||||
|
||||
return notAllowed(res);
|
||||
}
|
||||
}
|
||||
else if (headerAuth) {
|
||||
} else if (headerAuth) {
|
||||
// if there are no other auth methods we need to return unauthorized if nothing is provided
|
||||
if (
|
||||
!sso &&
|
||||
@@ -713,7 +705,11 @@ export async function verifyResourceSession(
|
||||
}
|
||||
|
||||
// If headerAuthExtendedCompatibility is activated but no clientHeaderAuth provided, force client to challenge
|
||||
if (headerAuthExtendedCompatibility && headerAuthExtendedCompatibility.extendedCompatibilityIsActivated && !clientHeaderAuth){
|
||||
if (
|
||||
headerAuthExtendedCompatibility &&
|
||||
headerAuthExtendedCompatibility.extendedCompatibilityIsActivated &&
|
||||
!clientHeaderAuth
|
||||
) {
|
||||
return headerAuthChallenged(res, redirectPath, resource.orgId);
|
||||
}
|
||||
|
||||
@@ -825,7 +821,7 @@ async function notAllowed(
|
||||
}
|
||||
|
||||
const data = {
|
||||
data: { valid: false, redirectUrl },
|
||||
data: { valid: false, redirectUrl, pangolinVersion: APP_VERSION },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Access denied",
|
||||
@@ -839,8 +835,8 @@ function allowed(res: Response, userData?: BasicUserData) {
|
||||
const data = {
|
||||
data:
|
||||
userData !== undefined && userData !== null
|
||||
? { valid: true, ...userData }
|
||||
: { valid: true },
|
||||
? { valid: true, ...userData, pangolinVersion: APP_VERSION }
|
||||
: { valid: true, pangolinVersion: APP_VERSION },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Access allowed",
|
||||
@@ -879,7 +875,12 @@ async function headerAuthChallenged(
|
||||
}
|
||||
|
||||
const data = {
|
||||
data: { headerAuthChallenged: true, valid: false, redirectUrl },
|
||||
data: {
|
||||
headerAuthChallenged: true,
|
||||
valid: false,
|
||||
redirectUrl,
|
||||
pangolinVersion: APP_VERSION
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Access denied",
|
||||
@@ -941,7 +942,7 @@ async function isUserAllowedToAccessResource(
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role
|
||||
role: userOrgRole.roleName
|
||||
};
|
||||
}
|
||||
|
||||
@@ -955,7 +956,7 @@ async function isUserAllowedToAccessResource(
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role
|
||||
role: userOrgRole.roleName
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1034,14 +1035,25 @@ export function isPathAllowed(pattern: string, path: string): boolean {
|
||||
logger.debug(`Normalized pattern parts: [${patternParts.join(", ")}]`);
|
||||
logger.debug(`Normalized path parts: [${pathParts.join(", ")}]`);
|
||||
|
||||
// Maximum recursion depth to prevent stack overflow and memory issues
|
||||
const MAX_RECURSION_DEPTH = 100;
|
||||
|
||||
// Recursive function to try different wildcard matches
|
||||
function matchSegments(patternIndex: number, pathIndex: number): boolean {
|
||||
const indent = " ".repeat(pathIndex); // Indent based on recursion depth
|
||||
function matchSegments(patternIndex: number, pathIndex: number, depth: number = 0): boolean {
|
||||
// Check recursion depth limit
|
||||
if (depth > MAX_RECURSION_DEPTH) {
|
||||
logger.warn(
|
||||
`Path matching exceeded maximum recursion depth (${MAX_RECURSION_DEPTH}) for pattern "${pattern}" and path "${path}"`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const indent = " ".repeat(depth); // Indent based on recursion depth
|
||||
const currentPatternPart = patternParts[patternIndex];
|
||||
const currentPathPart = pathParts[pathIndex];
|
||||
|
||||
logger.debug(
|
||||
`${indent}Checking patternIndex=${patternIndex} (${currentPatternPart || "END"}) vs pathIndex=${pathIndex} (${currentPathPart || "END"})`
|
||||
`${indent}Checking patternIndex=${patternIndex} (${currentPatternPart || "END"}) vs pathIndex=${pathIndex} (${currentPathPart || "END"}) [depth=${depth}]`
|
||||
);
|
||||
|
||||
// If we've consumed all pattern parts, we should have consumed all path parts
|
||||
@@ -1074,7 +1086,7 @@ export function isPathAllowed(pattern: string, path: string): boolean {
|
||||
logger.debug(
|
||||
`${indent}Trying to skip wildcard (consume 0 segments)`
|
||||
);
|
||||
if (matchSegments(patternIndex + 1, pathIndex)) {
|
||||
if (matchSegments(patternIndex + 1, pathIndex, depth + 1)) {
|
||||
logger.debug(
|
||||
`${indent}Successfully matched by skipping wildcard`
|
||||
);
|
||||
@@ -1085,7 +1097,7 @@ export function isPathAllowed(pattern: string, path: string): boolean {
|
||||
logger.debug(
|
||||
`${indent}Trying to consume segment "${currentPathPart}" for wildcard`
|
||||
);
|
||||
if (matchSegments(patternIndex, pathIndex + 1)) {
|
||||
if (matchSegments(patternIndex, pathIndex + 1, depth + 1)) {
|
||||
logger.debug(
|
||||
`${indent}Successfully matched by consuming segment for wildcard`
|
||||
);
|
||||
@@ -1113,7 +1125,7 @@ export function isPathAllowed(pattern: string, path: string): boolean {
|
||||
logger.debug(
|
||||
`${indent}Segment with wildcard matches: "${currentPatternPart}" matches "${currentPathPart}"`
|
||||
);
|
||||
return matchSegments(patternIndex + 1, pathIndex + 1);
|
||||
return matchSegments(patternIndex + 1, pathIndex + 1, depth + 1);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
@@ -1134,10 +1146,10 @@ export function isPathAllowed(pattern: string, path: string): boolean {
|
||||
`${indent}Segments match: "${currentPatternPart}" = "${currentPathPart}"`
|
||||
);
|
||||
// Move to next segments in both pattern and path
|
||||
return matchSegments(patternIndex + 1, pathIndex + 1);
|
||||
return matchSegments(patternIndex + 1, pathIndex + 1, depth + 1);
|
||||
}
|
||||
|
||||
const result = matchSegments(0, 0);
|
||||
const result = matchSegments(0, 0, 0);
|
||||
logger.debug(`Final result: ${result}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
105
server/routers/client/archiveClient.ts
Normal file
105
server/routers/client/archiveClient.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { clients } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||
import { sendTerminateClient } from "./terminate";
|
||||
|
||||
const archiveClientSchema = z.strictObject({
|
||||
clientId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/client/{clientId}/archive",
|
||||
description: "Archive a client by its client ID.",
|
||||
tags: [OpenAPITags.Client],
|
||||
request: {
|
||||
params: archiveClientSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function archiveClient(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = archiveClientSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { clientId } = parsedParams.data;
|
||||
|
||||
// Check if client exists
|
||||
const [client] = await db
|
||||
.select()
|
||||
.from(clients)
|
||||
.where(eq(clients.clientId, clientId))
|
||||
.limit(1);
|
||||
|
||||
if (!client) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Client with ID ${clientId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (client.archived) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
`Client with ID ${clientId} is already archived`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
// Archive the client
|
||||
await trx
|
||||
.update(clients)
|
||||
.set({ archived: true })
|
||||
.where(eq(clients.clientId, clientId));
|
||||
|
||||
// Rebuild associations to clean up related data
|
||||
await rebuildClientAssociationsFromClient(client, trx);
|
||||
|
||||
// Send terminate signal if there's an associated OLM
|
||||
if (client.olmId) {
|
||||
await sendTerminateClient(client.clientId, client.olmId);
|
||||
}
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Client archived successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to archive client"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
101
server/routers/client/blockClient.ts
Normal file
101
server/routers/client/blockClient.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { clients } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { sendTerminateClient } from "./terminate";
|
||||
|
||||
const blockClientSchema = z.strictObject({
|
||||
clientId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/client/{clientId}/block",
|
||||
description: "Block a client by its client ID.",
|
||||
tags: [OpenAPITags.Client],
|
||||
request: {
|
||||
params: blockClientSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function blockClient(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = blockClientSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { clientId } = parsedParams.data;
|
||||
|
||||
// Check if client exists
|
||||
const [client] = await db
|
||||
.select()
|
||||
.from(clients)
|
||||
.where(eq(clients.clientId, clientId))
|
||||
.limit(1);
|
||||
|
||||
if (!client) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Client with ID ${clientId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (client.blocked) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
`Client with ID ${clientId} is already blocked`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
// Block the client
|
||||
await trx
|
||||
.update(clients)
|
||||
.set({ blocked: true, approvalState: "denied" })
|
||||
.where(eq(clients.clientId, clientId));
|
||||
|
||||
// Send terminate signal if there's an associated OLM and it's connected
|
||||
if (client.olmId && client.online) {
|
||||
await sendTerminateClient(client.clientId, client.olmId);
|
||||
}
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Client blocked successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to block client"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -60,11 +60,12 @@ export async function deleteClient(
|
||||
);
|
||||
}
|
||||
|
||||
// Only allow deletion of machine clients (clients without userId)
|
||||
if (client.userId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
`Cannot delete a user client with this endpoint`
|
||||
`Cannot delete a user client. User clients must be archived instead.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, olms } from "@server/db";
|
||||
import { clients } from "@server/db";
|
||||
import { clients, fingerprints } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -10,6 +10,7 @@ import logger from "@server/logger";
|
||||
import stoi from "@server/lib/stoi";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { getUserDeviceName } from "@server/db/names";
|
||||
|
||||
const getClientSchema = z.strictObject({
|
||||
clientId: z
|
||||
@@ -29,6 +30,7 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
|
||||
.from(clients)
|
||||
.where(eq(clients.clientId, clientId))
|
||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
|
||||
.limit(1);
|
||||
return res;
|
||||
} else if (niceId && orgId) {
|
||||
@@ -36,7 +38,8 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
|
||||
.select()
|
||||
.from(clients)
|
||||
.where(and(eq(clients.niceId, niceId), eq(clients.orgId, orgId)))
|
||||
.leftJoin(olms, eq(olms.clientId, olms.clientId))
|
||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
|
||||
.limit(1);
|
||||
return res;
|
||||
}
|
||||
@@ -105,8 +108,16 @@ export async function getClient(
|
||||
);
|
||||
}
|
||||
|
||||
// Replace name with device name if OLM exists
|
||||
let clientName = client.clients.name;
|
||||
if (client.olms) {
|
||||
const model = client.fingerprints?.deviceModel || null;
|
||||
clientName = getUserDeviceName(model, client.clients.name);
|
||||
}
|
||||
|
||||
const data: GetClientResponse = {
|
||||
...client.clients,
|
||||
name: clientName,
|
||||
olmId: client.olms ? client.olms.olmId : null
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
export * from "./pickClientDefaults";
|
||||
export * from "./createClient";
|
||||
export * from "./deleteClient";
|
||||
export * from "./archiveClient";
|
||||
export * from "./unarchiveClient";
|
||||
export * from "./blockClient";
|
||||
export * from "./unblockClient";
|
||||
export * from "./listClients";
|
||||
export * from "./updateClient";
|
||||
export * from "./getClient";
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
roleClients,
|
||||
sites,
|
||||
userClients,
|
||||
clientSitesAssociationsCache
|
||||
clientSitesAssociationsCache,
|
||||
fingerprints
|
||||
} from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -27,6 +28,7 @@ import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import NodeCache from "node-cache";
|
||||
import semver from "semver";
|
||||
import { getUserDeviceName } from "@server/db/names";
|
||||
|
||||
const olmVersionCache = new NodeCache({ stdTTL: 3600 });
|
||||
|
||||
@@ -56,12 +58,12 @@ async function getLatestOlmVersion(): Promise<string | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tags = await response.json();
|
||||
let tags = await response.json();
|
||||
if (!Array.isArray(tags) || tags.length === 0) {
|
||||
logger.warn("No tags found for Olm repository");
|
||||
return null;
|
||||
}
|
||||
|
||||
tags = tags.filter((version) => !version.name.includes("rc"));
|
||||
const latestVersion = tags[0].name;
|
||||
|
||||
olmVersionCache.set("latestOlmVersion", latestVersion);
|
||||
@@ -136,12 +138,18 @@ function queryClients(
|
||||
username: users.username,
|
||||
userEmail: users.email,
|
||||
niceId: clients.niceId,
|
||||
agent: olms.agent
|
||||
agent: olms.agent,
|
||||
approvalState: clients.approvalState,
|
||||
olmArchived: olms.archived,
|
||||
archived: clients.archived,
|
||||
blocked: clients.blocked,
|
||||
deviceModel: fingerprints.deviceModel
|
||||
})
|
||||
.from(clients)
|
||||
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||
.leftJoin(users, eq(clients.userId, users.userId))
|
||||
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
|
||||
.where(and(...conditions));
|
||||
}
|
||||
|
||||
@@ -160,21 +168,22 @@ async function getSiteAssociations(clientIds: number[]) {
|
||||
.where(inArray(clientSitesAssociationsCache.clientId, clientIds));
|
||||
}
|
||||
|
||||
type OlmWithUpdateAvailable = Awaited<ReturnType<typeof queryClients>>[0] & {
|
||||
type ClientWithSites = Omit<
|
||||
Awaited<ReturnType<typeof queryClients>>[0],
|
||||
"deviceModel"
|
||||
> & {
|
||||
sites: Array<{
|
||||
siteId: number;
|
||||
siteName: string | null;
|
||||
siteNiceId: string | null;
|
||||
}>;
|
||||
olmUpdateAvailable?: boolean;
|
||||
};
|
||||
|
||||
type OlmWithUpdateAvailable = ClientWithSites;
|
||||
|
||||
export type ListClientsResponse = {
|
||||
clients: Array<
|
||||
Awaited<ReturnType<typeof queryClients>>[0] & {
|
||||
sites: Array<{
|
||||
siteId: number;
|
||||
siteName: string | null;
|
||||
siteNiceId: string | null;
|
||||
}>;
|
||||
olmUpdateAvailable?: boolean;
|
||||
}
|
||||
>;
|
||||
clients: Array<ClientWithSites>;
|
||||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
@@ -304,11 +313,17 @@ export async function listClients(
|
||||
>
|
||||
);
|
||||
|
||||
// Merge clients with their site associations
|
||||
const clientsWithSites = clientsList.map((client) => ({
|
||||
...client,
|
||||
sites: sitesByClient[client.clientId] || []
|
||||
}));
|
||||
// Merge clients with their site associations and replace name with device name
|
||||
const clientsWithSites = clientsList.map((client) => {
|
||||
const model = client.deviceModel || null;
|
||||
const newName = getUserDeviceName(model, client.name);
|
||||
const { deviceModel, ...clientWithoutDeviceModel } = client;
|
||||
return {
|
||||
...clientWithoutDeviceModel,
|
||||
name: newName,
|
||||
sites: sitesByClient[client.clientId] || []
|
||||
};
|
||||
});
|
||||
|
||||
const latestOlVersionPromise = getLatestOlmVersion();
|
||||
|
||||
@@ -347,7 +362,7 @@ export async function listClients(
|
||||
|
||||
return response<ListClientsResponse>(res, {
|
||||
data: {
|
||||
clients: clientsWithSites,
|
||||
clients: olmsWithUpdates,
|
||||
pagination: {
|
||||
total: totalCount,
|
||||
limit,
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function addTargets(newtId: string, targets: SubnetProxyTarget[]) {
|
||||
await sendToClient(newtId, {
|
||||
type: `newt/wg/targets/add`,
|
||||
data: batches[i]
|
||||
});
|
||||
}, { incrementConfigVersion: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function removeTargets(
|
||||
await sendToClient(newtId, {
|
||||
type: `newt/wg/targets/remove`,
|
||||
data: batches[i]
|
||||
});
|
||||
},{ incrementConfigVersion: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ export async function updateTargets(
|
||||
oldTargets: oldBatches[i] || [],
|
||||
newTargets: newBatches[i] || []
|
||||
}
|
||||
}).catch((error) => {
|
||||
}, { incrementConfigVersion: true }).catch((error) => {
|
||||
logger.warn(`Error sending message:`, error);
|
||||
});
|
||||
}
|
||||
@@ -101,7 +101,7 @@ export async function addPeerData(
|
||||
remoteSubnets: remoteSubnets,
|
||||
aliases: aliases
|
||||
}
|
||||
}).catch((error) => {
|
||||
}, { incrementConfigVersion: true }).catch((error) => {
|
||||
logger.warn(`Error sending message:`, error);
|
||||
});
|
||||
}
|
||||
@@ -132,7 +132,7 @@ export async function removePeerData(
|
||||
remoteSubnets: remoteSubnets,
|
||||
aliases: aliases
|
||||
}
|
||||
}).catch((error) => {
|
||||
}, { incrementConfigVersion: true }).catch((error) => {
|
||||
logger.warn(`Error sending message:`, error);
|
||||
});
|
||||
}
|
||||
@@ -173,7 +173,7 @@ export async function updatePeerData(
|
||||
...remoteSubnets,
|
||||
...aliases
|
||||
}
|
||||
}).catch((error) => {
|
||||
}, { incrementConfigVersion: true }).catch((error) => {
|
||||
logger.warn(`Error sending message:`, error);
|
||||
});
|
||||
}
|
||||
|
||||
93
server/routers/client/unarchiveClient.ts
Normal file
93
server/routers/client/unarchiveClient.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { clients } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const unarchiveClientSchema = z.strictObject({
|
||||
clientId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/client/{clientId}/unarchive",
|
||||
description: "Unarchive a client by its client ID.",
|
||||
tags: [OpenAPITags.Client],
|
||||
request: {
|
||||
params: unarchiveClientSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function unarchiveClient(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = unarchiveClientSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { clientId } = parsedParams.data;
|
||||
|
||||
// Check if client exists
|
||||
const [client] = await db
|
||||
.select()
|
||||
.from(clients)
|
||||
.where(eq(clients.clientId, clientId))
|
||||
.limit(1);
|
||||
|
||||
if (!client) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Client with ID ${clientId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!client.archived) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
`Client with ID ${clientId} is not archived`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Unarchive the client
|
||||
await db
|
||||
.update(clients)
|
||||
.set({ archived: false })
|
||||
.where(eq(clients.clientId, clientId));
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Client unarchived successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to unarchive client"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
93
server/routers/client/unblockClient.ts
Normal file
93
server/routers/client/unblockClient.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { clients } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const unblockClientSchema = z.strictObject({
|
||||
clientId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/client/{clientId}/unblock",
|
||||
description: "Unblock a client by its client ID.",
|
||||
tags: [OpenAPITags.Client],
|
||||
request: {
|
||||
params: unblockClientSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function unblockClient(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = unblockClientSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { clientId } = parsedParams.data;
|
||||
|
||||
// Check if client exists
|
||||
const [client] = await db
|
||||
.select()
|
||||
.from(clients)
|
||||
.where(eq(clients.clientId, clientId))
|
||||
.limit(1);
|
||||
|
||||
if (!client) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Client with ID ${clientId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!client.blocked) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
`Client with ID ${clientId} is not blocked`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Unblock the client
|
||||
await db
|
||||
.update(clients)
|
||||
.set({ blocked: false, approvalState: null })
|
||||
.where(eq(clients.clientId, clientId));
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Client unblocked successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to unblock client"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -174,6 +174,38 @@ authenticated.delete(
|
||||
client.deleteClient
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/client/:clientId/archive",
|
||||
verifyClientAccess,
|
||||
verifyUserHasAction(ActionsEnum.archiveClient),
|
||||
logActionAudit(ActionsEnum.archiveClient),
|
||||
client.archiveClient
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/client/:clientId/unarchive",
|
||||
verifyClientAccess,
|
||||
verifyUserHasAction(ActionsEnum.unarchiveClient),
|
||||
logActionAudit(ActionsEnum.unarchiveClient),
|
||||
client.unarchiveClient
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/client/:clientId/block",
|
||||
verifyClientAccess,
|
||||
verifyUserHasAction(ActionsEnum.blockClient),
|
||||
logActionAudit(ActionsEnum.blockClient),
|
||||
client.blockClient
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/client/:clientId/unblock",
|
||||
verifyClientAccess,
|
||||
verifyUserHasAction(ActionsEnum.unblockClient),
|
||||
logActionAudit(ActionsEnum.unblockClient),
|
||||
client.unblockClient
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/client/:clientId",
|
||||
verifyClientAccess, // this will check if the user has access to the client
|
||||
@@ -554,6 +586,14 @@ authenticated.get(
|
||||
verifyUserHasAction(ActionsEnum.listRoles),
|
||||
role.listRoles
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/role/:roleId",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateRole),
|
||||
logActionAudit(ActionsEnum.updateRole),
|
||||
role.updateRole
|
||||
);
|
||||
// authenticated.get(
|
||||
// "/role/:roleId",
|
||||
// verifyRoleAccess,
|
||||
@@ -808,11 +848,18 @@ authenticated.put("/user/:userId/olm", verifyIsLoggedInUser, olm.createUserOlm);
|
||||
|
||||
authenticated.get("/user/:userId/olms", verifyIsLoggedInUser, olm.listUserOlms);
|
||||
|
||||
authenticated.delete(
|
||||
"/user/:userId/olm/:olmId",
|
||||
authenticated.post(
|
||||
"/user/:userId/olm/:olmId/archive",
|
||||
verifyIsLoggedInUser,
|
||||
verifyOlmAccess,
|
||||
olm.deleteUserOlm
|
||||
olm.archiveUserOlm
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/user/:userId/olm/:olmId/unarchive",
|
||||
verifyIsLoggedInUser,
|
||||
verifyOlmAccess,
|
||||
olm.unarchiveUserOlm
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -822,6 +869,12 @@ authenticated.get(
|
||||
olm.getUserOlm
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/user/:userId/olm/recover",
|
||||
verifyIsLoggedInUser,
|
||||
olm.recoverOlmWithFingerprint
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/idp/oidc",
|
||||
verifyUserIsServerAdmin,
|
||||
@@ -1068,6 +1121,21 @@ authRouter.post(
|
||||
auth.login
|
||||
);
|
||||
authRouter.post("/logout", auth.logout);
|
||||
authRouter.post(
|
||||
"/lookup-user",
|
||||
rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 15,
|
||||
keyGenerator: (req) =>
|
||||
`lookupUser:${req.body.identifier || ipKeyGenerator(req.ip || "")}`,
|
||||
handler: (req, res, next) => {
|
||||
const message = `You can only lookup users ${15} times every ${15} minutes. Please try again later.`;
|
||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||
},
|
||||
store: createStore()
|
||||
}),
|
||||
auth.lookupUser
|
||||
);
|
||||
authRouter.post(
|
||||
"/newt/get-token",
|
||||
rateLimit({
|
||||
|
||||
@@ -52,7 +52,7 @@ export async function getConfig(
|
||||
}
|
||||
|
||||
// clean up the public key - keep only valid base64 characters (A-Z, a-z, 0-9, +, /, =)
|
||||
const cleanedPublicKey = publicKey.replace(/[^A-Za-z0-9+/=]/g, '');
|
||||
const cleanedPublicKey = publicKey.replace(/[^A-Za-z0-9+/=]/g, "");
|
||||
|
||||
const exitNode = await createExitNode(cleanedPublicKey, reachableAt);
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ const bodySchema = z.strictObject({
|
||||
emailPath: z.string().optional(),
|
||||
namePath: z.string().optional(),
|
||||
scopes: z.string().nonempty(),
|
||||
autoProvision: z.boolean().optional()
|
||||
autoProvision: z.boolean().optional(),
|
||||
tags: z.string().optional()
|
||||
});
|
||||
|
||||
export type CreateIdpResponse = {
|
||||
@@ -75,7 +76,8 @@ export async function createOidcIdp(
|
||||
emailPath,
|
||||
namePath,
|
||||
name,
|
||||
autoProvision
|
||||
autoProvision,
|
||||
tags
|
||||
} = parsedBody.data;
|
||||
|
||||
const key = config.getRawConfig().server.secret!;
|
||||
@@ -90,7 +92,8 @@ export async function createOidcIdp(
|
||||
.values({
|
||||
name,
|
||||
autoProvision,
|
||||
type: "oidc"
|
||||
type: "oidc",
|
||||
tags
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user