mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-19 11:26:37 +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
|
id: check-rc
|
||||||
run: |
|
run: |
|
||||||
TAG=${{ env.TAG }}
|
TAG=${{ env.TAG }}
|
||||||
if [[ "$TAG" == *".rc."* ]]; then
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
echo "IS_RC=true" >> $GITHUB_ENV
|
echo "IS_RC=true" >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo "IS_RC=false" >> $GITHUB_ENV
|
echo "IS_RC=false" >> $GITHUB_ENV
|
||||||
@@ -171,7 +171,7 @@ jobs:
|
|||||||
id: check-rc
|
id: check-rc
|
||||||
run: |
|
run: |
|
||||||
TAG=${{ env.TAG }}
|
TAG=${{ env.TAG }}
|
||||||
if [[ "$TAG" == *".rc."* ]]; then
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
echo "IS_RC=true" >> $GITHUB_ENV
|
echo "IS_RC=true" >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo "IS_RC=false" >> $GITHUB_ENV
|
echo "IS_RC=false" >> $GITHUB_ENV
|
||||||
@@ -219,7 +219,7 @@ jobs:
|
|||||||
id: check-rc
|
id: check-rc
|
||||||
run: |
|
run: |
|
||||||
TAG=${{ env.TAG }}
|
TAG=${{ env.TAG }}
|
||||||
if [[ "$TAG" == *".rc."* ]]; then
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
echo "IS_RC=true" >> $GITHUB_ENV
|
echo "IS_RC=true" >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo "IS_RC=false" >> $GITHUB_ENV
|
echo "IS_RC=false" >> $GITHUB_ENV
|
||||||
@@ -322,22 +322,96 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
|
env:
|
||||||
|
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
|
||||||
run: |
|
run: |
|
||||||
|
mkdir -p "$(dirname "$REGISTRY_AUTH_FILE")"
|
||||||
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
|
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Copy tag from Docker Hub to GHCR
|
- name: Copy tags from Docker Hub to GHCR
|
||||||
# Mirror the already-built image (all architectures) to GHCR so we can sign it
|
# 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
|
# Wait a bit for both architectures to be available in Docker Hub manifest
|
||||||
|
env:
|
||||||
|
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
TAG=${{ env.TAG }}
|
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
|
sleep 30
|
||||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
|
|
||||||
skopeo copy --all --retry-times 3 \
|
# Determine if this is an RC release
|
||||||
docker://$DOCKERHUB_IMAGE:$TAG \
|
IS_RC="false"
|
||||||
docker://$GHCR_IMAGE:$TAG
|
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
|
shell: bash
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry (for cosign)
|
- name: Login to GitHub Container Registry (for cosign)
|
||||||
@@ -366,28 +440,62 @@ jobs:
|
|||||||
issuer="https://token.actions.githubusercontent.com"
|
issuer="https://token.actions.githubusercontent.com"
|
||||||
id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
|
id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
|
||||||
|
|
||||||
for IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
|
# Determine if this is an RC release
|
||||||
echo "Processing ${IMAGE}:${TAG}"
|
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')"
|
# Define image variants to sign
|
||||||
REF="${IMAGE}@${DIGEST}"
|
if [ "$IS_RC" = "true" ]; then
|
||||||
echo "Resolved digest: ${REF}"
|
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}"
|
# Sign each image variant for both registries
|
||||||
cosign sign --recursive "${REF}"
|
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}"
|
DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${IMAGE_TAG} | jq -r '.Digest')"
|
||||||
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
|
REF="${BASE_IMAGE}@${DIGEST}"
|
||||||
|
echo "Resolved digest: ${REF}"
|
||||||
|
|
||||||
echo "==> cosign verify (public key) ${REF}"
|
echo "==> cosign sign (keyless) --recursive ${REF}"
|
||||||
cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text
|
cosign sign --recursive "${REF}"
|
||||||
|
|
||||||
echo "==> cosign verify (keyless policy) ${REF}"
|
echo "==> cosign sign (key) --recursive ${REF}"
|
||||||
cosign verify \
|
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
|
||||||
--certificate-oidc-issuer "${issuer}" \
|
|
||||||
--certificate-identity-regexp "${id_regex}" \
|
echo "==> cosign verify (public key) ${REF}"
|
||||||
"${REF}" -o text
|
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
|
done
|
||||||
|
|
||||||
|
echo "All images signed and verified successfully!"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
post-run:
|
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: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
skopeo list-tags --retry-times 3 docker://"${SOURCE_IMAGE}" \
|
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)"
|
echo "Found source tags: $(wc -l < src-tags.txt)"
|
||||||
head -n 20 src-tags.txt || true
|
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
|
*.mmdb
|
||||||
scratch/
|
scratch/
|
||||||
tsconfig.json
|
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
|
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
|
WORKDIR /app
|
||||||
|
|
||||||
ARG BUILD=oss
|
ARG BUILD=oss
|
||||||
ARG DATABASE=sqlite
|
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++
|
RUN apk add --no-cache curl tzdata python3 make g++
|
||||||
|
|
||||||
# COPY package.json package-lock.json ./
|
# 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 server/db/names.json ./dist/names.json
|
||||||
COPY public ./public
|
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"]
|
CMD ["npm", "run", "start"]
|
||||||
|
|||||||
205
Makefile
205
Makefile
@@ -3,6 +3,25 @@
|
|||||||
major_tag := $(shell echo $(tag) | cut -d. -f1)
|
major_tag := $(shell echo $(tag) | cut -d. -f1)
|
||||||
minor_tag := $(shell echo $(tag) | cut -d. -f1,2)
|
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
|
.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
|
build-release: build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
|
||||||
@@ -15,6 +34,7 @@ build-sqlite:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
|
$(OCI_ARGS_OSS) \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:latest \
|
--tag fosrl/pangolin:latest \
|
||||||
--tag fosrl/pangolin:$(major_tag) \
|
--tag fosrl/pangolin:$(major_tag) \
|
||||||
@@ -30,6 +50,7 @@ build-postgresql:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
|
$(OCI_ARGS_OSS) \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:postgresql-latest \
|
--tag fosrl/pangolin:postgresql-latest \
|
||||||
--tag fosrl/pangolin:postgresql-$(major_tag) \
|
--tag fosrl/pangolin:postgresql-$(major_tag) \
|
||||||
@@ -45,6 +66,7 @@ build-ee-sqlite:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
|
$(OCI_ARGS_EE) \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-latest \
|
--tag fosrl/pangolin:ee-latest \
|
||||||
--tag fosrl/pangolin:ee-$(major_tag) \
|
--tag fosrl/pangolin:ee-$(major_tag) \
|
||||||
@@ -60,6 +82,7 @@ build-ee-postgresql:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
|
$(OCI_ARGS_EE) \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-latest \
|
--tag fosrl/pangolin:ee-postgresql-latest \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$(major_tag) \
|
--tag fosrl/pangolin:ee-postgresql-$(major_tag) \
|
||||||
@@ -67,6 +90,18 @@ build-ee-postgresql:
|
|||||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||||
--push .
|
--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:
|
build-release-arm:
|
||||||
@if [ -z "$(tag)" ]; then \
|
@if [ -z "$(tag)" ]; then \
|
||||||
echo "Error: tag is required. Usage: make build-release-arm tag=<tag>"; \
|
echo "Error: tag is required. Usage: make build-release-arm tag=<tag>"; \
|
||||||
@@ -74,9 +109,16 @@ build-release-arm:
|
|||||||
fi
|
fi
|
||||||
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
||||||
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
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 \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:latest-arm64 \
|
--tag fosrl/pangolin:latest-arm64 \
|
||||||
--tag fosrl/pangolin:$$MAJOR_TAG-arm64 \
|
--tag fosrl/pangolin:$$MAJOR_TAG-arm64 \
|
||||||
@@ -86,6 +128,11 @@ build-release-arm:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:postgresql-latest-arm64 \
|
--tag fosrl/pangolin:postgresql-latest-arm64 \
|
||||||
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-arm64 \
|
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-arm64 \
|
||||||
@@ -95,6 +142,12 @@ build-release-arm:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:ee-latest-arm64 \
|
--tag fosrl/pangolin:ee-latest-arm64 \
|
||||||
--tag fosrl/pangolin:ee-$$MAJOR_TAG-arm64 \
|
--tag fosrl/pangolin:ee-$$MAJOR_TAG-arm64 \
|
||||||
@@ -104,6 +157,12 @@ build-release-arm:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-latest-arm64 \
|
--tag fosrl/pangolin:ee-postgresql-latest-arm64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-arm64 \
|
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-arm64 \
|
||||||
@@ -118,9 +177,16 @@ build-release-amd:
|
|||||||
fi
|
fi
|
||||||
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
||||||
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
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 \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/amd64 \
|
||||||
--tag fosrl/pangolin:latest-amd64 \
|
--tag fosrl/pangolin:latest-amd64 \
|
||||||
--tag fosrl/pangolin:$$MAJOR_TAG-amd64 \
|
--tag fosrl/pangolin:$$MAJOR_TAG-amd64 \
|
||||||
@@ -130,6 +196,11 @@ build-release-amd:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/amd64 \
|
||||||
--tag fosrl/pangolin:postgresql-latest-amd64 \
|
--tag fosrl/pangolin:postgresql-latest-amd64 \
|
||||||
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-amd64 \
|
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-amd64 \
|
||||||
@@ -139,6 +210,12 @@ build-release-amd:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-latest-amd64 \
|
--tag fosrl/pangolin:ee-latest-amd64 \
|
||||||
--tag fosrl/pangolin:ee-$$MAJOR_TAG-amd64 \
|
--tag fosrl/pangolin:ee-$$MAJOR_TAG-amd64 \
|
||||||
@@ -148,6 +225,12 @@ build-release-amd:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-latest-amd64 \
|
--tag fosrl/pangolin:ee-postgresql-latest-amd64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-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>"; \
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
|
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:$(tag) \
|
--tag fosrl/pangolin:$(tag) \
|
||||||
--push .
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||||
--push .
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-$(tag) \
|
--tag fosrl/pangolin:ee-$(tag) \
|
||||||
--push .
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||||
--push .
|
--push .
|
||||||
@@ -231,27 +338,51 @@ build-rc-arm:
|
|||||||
echo "Error: tag is required. Usage: make build-rc-arm tag=<tag>"; \
|
echo "Error: tag is required. Usage: make build-rc-arm tag=<tag>"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
|
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:$(tag)-arm64 \
|
--tag fosrl/pangolin:$(tag)-arm64 \
|
||||||
--push . && \
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:postgresql-$(tag)-arm64 \
|
--tag fosrl/pangolin:postgresql-$(tag)-arm64 \
|
||||||
--push . && \
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:ee-$(tag)-arm64 \
|
--tag fosrl/pangolin:ee-$(tag)-arm64 \
|
||||||
--push . && \
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/arm64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$(tag)-arm64 \
|
--tag fosrl/pangolin:ee-postgresql-$(tag)-arm64 \
|
||||||
--push .
|
--push .
|
||||||
@@ -261,27 +392,51 @@ build-rc-amd:
|
|||||||
echo "Error: tag is required. Usage: make build-rc-amd tag=<tag>"; \
|
echo "Error: tag is required. Usage: make build-rc-amd tag=<tag>"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
|
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/amd64 \
|
||||||
--tag fosrl/pangolin:$(tag)-amd64 \
|
--tag fosrl/pangolin:$(tag)-amd64 \
|
||||||
--push . && \
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/amd64 \
|
||||||
--tag fosrl/pangolin:postgresql-$(tag)-amd64 \
|
--tag fosrl/pangolin:postgresql-$(tag)-amd64 \
|
||||||
--push . && \
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--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 \
|
--platform linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-$(tag)-amd64 \
|
--tag fosrl/pangolin:ee-$(tag)-amd64 \
|
||||||
--push . && \
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--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 \
|
--platform linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$(tag)-amd64 \
|
--tag fosrl/pangolin:ee-postgresql-$(tag)-amd64 \
|
||||||
--push .
|
--push .
|
||||||
@@ -314,16 +469,52 @@ create-manifests-rc:
|
|||||||
echo "All RC multi-arch manifests created successfully!"
|
echo "All RC multi-arch manifests created successfully!"
|
||||||
|
|
||||||
build-arm:
|
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:
|
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:
|
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:
|
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:
|
test:
|
||||||
docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest
|
docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
http:
|
http:
|
||||||
middlewares:
|
middlewares:
|
||||||
|
badger:
|
||||||
|
plugin:
|
||||||
|
badger:
|
||||||
|
disableForwardAuth: true
|
||||||
redirect-to-https:
|
redirect-to-https:
|
||||||
redirectScheme:
|
redirectScheme:
|
||||||
scheme: https
|
scheme: https
|
||||||
@@ -13,6 +17,7 @@ http:
|
|||||||
- web
|
- web
|
||||||
middlewares:
|
middlewares:
|
||||||
- redirect-to-https
|
- redirect-to-https
|
||||||
|
- badger
|
||||||
|
|
||||||
# Next.js router (handles everything except API and WebSocket paths)
|
# Next.js router (handles everything except API and WebSocket paths)
|
||||||
next-router:
|
next-router:
|
||||||
@@ -21,6 +26,8 @@ http:
|
|||||||
priority: 10
|
priority: 10
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
@@ -31,6 +38,8 @@ http:
|
|||||||
priority: 100
|
priority: 100
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
|
|||||||
@@ -43,9 +43,12 @@ entryPoints:
|
|||||||
http:
|
http:
|
||||||
tls:
|
tls:
|
||||||
certResolver: "letsencrypt"
|
certResolver: "letsencrypt"
|
||||||
|
encodedCharacters:
|
||||||
|
allowEncodedSlash: true
|
||||||
|
allowEncodedQuestionMark: true
|
||||||
|
|
||||||
serversTransport:
|
serversTransport:
|
||||||
insecureSkipVerify: true
|
insecureSkipVerify: true
|
||||||
|
|
||||||
ping:
|
ping:
|
||||||
entryPoint: "web"
|
entryPoint: "web"
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ func collectUserInput(reader *bufio.Reader) Config {
|
|||||||
// Basic configuration
|
// Basic configuration
|
||||||
fmt.Println("\n=== 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)", "")
|
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||||
|
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"orgPolicyConfig": "Конфигуриране на достъп за организация",
|
"orgPolicyConfig": "Конфигуриране на достъп за организация",
|
||||||
"idpUpdatedDescription": "Идентификационният доставчик беше актуализиран успешно",
|
"idpUpdatedDescription": "Идентификационният доставчик беше актуализиран успешно",
|
||||||
"redirectUrl": "URL за пренасочване",
|
"redirectUrl": "URL за пренасочване",
|
||||||
|
"orgIdpRedirectUrls": "URL адреси за пренасочване",
|
||||||
"redirectUrlAbout": "За URL за пренасочване",
|
"redirectUrlAbout": "За URL за пренасочване",
|
||||||
"redirectUrlAboutDescription": "Това е URL адресът, към който потребителите ще бъдат пренасочени след удостоверяване. Трябва да конфигурирате този URL адрес в настройките на доставчика на идентичност.",
|
"redirectUrlAboutDescription": "Това е URL адресът, към който потребителите ще бъдат пренасочени след удостоверяване. Трябва да конфигурирате този URL адрес в настройките на доставчика на идентичност.",
|
||||||
"pangolinAuth": "Authent - Pangolin",
|
"pangolinAuth": "Authent - Pangolin",
|
||||||
@@ -1479,7 +1480,7 @@
|
|||||||
"IAgreeToThe": "Съгласен съм с",
|
"IAgreeToThe": "Съгласен съм с",
|
||||||
"termsOfService": "условията за ползване",
|
"termsOfService": "условията за ползване",
|
||||||
"and": "и",
|
"and": "и",
|
||||||
"privacyPolicy": "политиката за поверителност"
|
"privacyPolicy": "политика за поверителност."
|
||||||
},
|
},
|
||||||
"signUpMarketing": {
|
"signUpMarketing": {
|
||||||
"keepMeInTheLoop": "Дръж ме в течение с новини, актуализации и нови функции чрез имейл."
|
"keepMeInTheLoop": "Дръж ме в течение с новини, актуализации и нови функции чрез имейл."
|
||||||
@@ -2349,6 +2350,7 @@
|
|||||||
"enterConfirmation": "Въведете потвърждение.",
|
"enterConfirmation": "Въведете потвърждение.",
|
||||||
"blueprintViewDetails": "Подробности.",
|
"blueprintViewDetails": "Подробности.",
|
||||||
"defaultIdentityProvider": "По подразбиране доставчик на идентичност.",
|
"defaultIdentityProvider": "По подразбиране доставчик на идентичност.",
|
||||||
|
"defaultIdentityProviderDescription": "Когато е избран основен доставчик на идентичност, потребителят ще бъде автоматично пренасочен към доставчика за удостоверяване.",
|
||||||
"editInternalResourceDialogNetworkSettings": "Мрежови настройки.",
|
"editInternalResourceDialogNetworkSettings": "Мрежови настройки.",
|
||||||
"editInternalResourceDialogAccessPolicy": "Политика за достъп.",
|
"editInternalResourceDialogAccessPolicy": "Политика за достъп.",
|
||||||
"editInternalResourceDialogAddRoles": "Добавяне на роли.",
|
"editInternalResourceDialogAddRoles": "Добавяне на роли.",
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"orgPolicyConfig": "Konfigurace přístupu pro organizaci",
|
"orgPolicyConfig": "Konfigurace přístupu pro organizaci",
|
||||||
"idpUpdatedDescription": "Poskytovatel identity byl úspěšně aktualizován",
|
"idpUpdatedDescription": "Poskytovatel identity byl úspěšně aktualizován",
|
||||||
"redirectUrl": "Přesměrovat URL",
|
"redirectUrl": "Přesměrovat URL",
|
||||||
|
"orgIdpRedirectUrls": "Přesměrovat URL",
|
||||||
"redirectUrlAbout": "O přesměrování 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.",
|
"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",
|
"pangolinAuth": "Auth - Pangolin",
|
||||||
@@ -1479,7 +1480,7 @@
|
|||||||
"IAgreeToThe": "Souhlasím s",
|
"IAgreeToThe": "Souhlasím s",
|
||||||
"termsOfService": "podmínky služby",
|
"termsOfService": "podmínky služby",
|
||||||
"and": "a",
|
"and": "a",
|
||||||
"privacyPolicy": "zásady ochrany osobních údajů"
|
"privacyPolicy": "zásady ochrany osobních údajů."
|
||||||
},
|
},
|
||||||
"signUpMarketing": {
|
"signUpMarketing": {
|
||||||
"keepMeInTheLoop": "Udržujte mě ve smyčce s novinkami, aktualizacemi a novými funkcemi e-mailem."
|
"keepMeInTheLoop": "Udržujte mě ve smyčce s novinkami, aktualizacemi a novými funkcemi e-mailem."
|
||||||
@@ -2349,6 +2350,7 @@
|
|||||||
"enterConfirmation": "Zadejte potvrzení",
|
"enterConfirmation": "Zadejte potvrzení",
|
||||||
"blueprintViewDetails": "Detaily",
|
"blueprintViewDetails": "Detaily",
|
||||||
"defaultIdentityProvider": "Výchozí poskytovatel identity",
|
"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ě",
|
"editInternalResourceDialogNetworkSettings": "Nastavení sítě",
|
||||||
"editInternalResourceDialogAccessPolicy": "Přístupová politika",
|
"editInternalResourceDialogAccessPolicy": "Přístupová politika",
|
||||||
"editInternalResourceDialogAddRoles": "Přidat role",
|
"editInternalResourceDialogAddRoles": "Přidat role",
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"orgPolicyConfig": "Zugriff für eine Organisation konfigurieren",
|
"orgPolicyConfig": "Zugriff für eine Organisation konfigurieren",
|
||||||
"idpUpdatedDescription": "Identitätsanbieter erfolgreich aktualisiert",
|
"idpUpdatedDescription": "Identitätsanbieter erfolgreich aktualisiert",
|
||||||
"redirectUrl": "Weiterleitungs-URL",
|
"redirectUrl": "Weiterleitungs-URL",
|
||||||
|
"orgIdpRedirectUrls": "Umleitungs-URLs",
|
||||||
"redirectUrlAbout": "Über die Weiterleitungs-URL",
|
"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.",
|
"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",
|
"pangolinAuth": "Authentifizierung - Pangolin",
|
||||||
@@ -1479,7 +1480,7 @@
|
|||||||
"IAgreeToThe": "Ich stimme den",
|
"IAgreeToThe": "Ich stimme den",
|
||||||
"termsOfService": "Nutzungsbedingungen zu",
|
"termsOfService": "Nutzungsbedingungen zu",
|
||||||
"and": "und",
|
"and": "und",
|
||||||
"privacyPolicy": "Datenschutzrichtlinie"
|
"privacyPolicy": "datenschutzrichtlinie."
|
||||||
},
|
},
|
||||||
"signUpMarketing": {
|
"signUpMarketing": {
|
||||||
"keepMeInTheLoop": "Halten Sie mich auf dem Laufenden mit Neuigkeiten, Updates und neuen Funktionen per E-Mail."
|
"keepMeInTheLoop": "Halten Sie mich auf dem Laufenden mit Neuigkeiten, Updates und neuen Funktionen per E-Mail."
|
||||||
@@ -2349,6 +2350,7 @@
|
|||||||
"enterConfirmation": "Bestätigung eingeben",
|
"enterConfirmation": "Bestätigung eingeben",
|
||||||
"blueprintViewDetails": "Details",
|
"blueprintViewDetails": "Details",
|
||||||
"defaultIdentityProvider": "Standard Identitätsanbieter",
|
"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",
|
"editInternalResourceDialogNetworkSettings": "Netzwerkeinstellungen",
|
||||||
"editInternalResourceDialogAccessPolicy": "Zugriffsrichtlinie",
|
"editInternalResourceDialogAccessPolicy": "Zugriffsrichtlinie",
|
||||||
"editInternalResourceDialogAddRoles": "Rollen hinzufügen",
|
"editInternalResourceDialogAddRoles": "Rollen hinzufügen",
|
||||||
|
|||||||
@@ -257,6 +257,8 @@
|
|||||||
"accessRolesSearch": "Search roles...",
|
"accessRolesSearch": "Search roles...",
|
||||||
"accessRolesAdd": "Add Role",
|
"accessRolesAdd": "Add Role",
|
||||||
"accessRoleDelete": "Delete Role",
|
"accessRoleDelete": "Delete Role",
|
||||||
|
"accessApprovalsManage": "Manage Approvals",
|
||||||
|
"accessApprovalsDescription": "Manage approval requests in the organization",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"inviteTitle": "Open Invitations",
|
"inviteTitle": "Open Invitations",
|
||||||
"inviteDescription": "Manage invitations for other users to join the organization",
|
"inviteDescription": "Manage invitations for other users to join the organization",
|
||||||
@@ -450,6 +452,18 @@
|
|||||||
"selectDuration": "Select duration",
|
"selectDuration": "Select duration",
|
||||||
"selectResource": "Select Resource",
|
"selectResource": "Select Resource",
|
||||||
"filterByResource": "Filter By 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",
|
"resetFilters": "Reset Filters",
|
||||||
"totalBlocked": "Requests Blocked By Pangolin",
|
"totalBlocked": "Requests Blocked By Pangolin",
|
||||||
"totalRequests": "Total Requests",
|
"totalRequests": "Total Requests",
|
||||||
@@ -729,16 +743,28 @@
|
|||||||
"countries": "Countries",
|
"countries": "Countries",
|
||||||
"accessRoleCreate": "Create Role",
|
"accessRoleCreate": "Create Role",
|
||||||
"accessRoleCreateDescription": "Create a new role to group users and manage their permissions.",
|
"accessRoleCreateDescription": "Create a new role to group users and manage their permissions.",
|
||||||
|
"accessRoleEdit": "Edit Role",
|
||||||
|
"accessRoleEditDescription": "Edit role information.",
|
||||||
"accessRoleCreateSubmit": "Create Role",
|
"accessRoleCreateSubmit": "Create Role",
|
||||||
"accessRoleCreated": "Role created",
|
"accessRoleCreated": "Role created",
|
||||||
"accessRoleCreatedDescription": "The role has been successfully created.",
|
"accessRoleCreatedDescription": "The role has been successfully created.",
|
||||||
"accessRoleErrorCreate": "Failed to create role",
|
"accessRoleErrorCreate": "Failed to create role",
|
||||||
"accessRoleErrorCreateDescription": "An error occurred while creating the 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",
|
"accessRoleErrorNewRequired": "New role is required",
|
||||||
"accessRoleErrorRemove": "Failed to remove role",
|
"accessRoleErrorRemove": "Failed to remove role",
|
||||||
"accessRoleErrorRemoveDescription": "An error occurred while removing the role.",
|
"accessRoleErrorRemoveDescription": "An error occurred while removing the role.",
|
||||||
"accessRoleName": "Role Name",
|
"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",
|
"accessRoleRemove": "Remove Role",
|
||||||
"accessRoleRemoveDescription": "Remove a role from the organization",
|
"accessRoleRemoveDescription": "Remove a role from the organization",
|
||||||
"accessRoleRemoveSubmit": "Remove Role",
|
"accessRoleRemoveSubmit": "Remove Role",
|
||||||
@@ -850,6 +876,7 @@
|
|||||||
"orgPolicyConfig": "Configure access for an organization",
|
"orgPolicyConfig": "Configure access for an organization",
|
||||||
"idpUpdatedDescription": "Identity provider updated successfully",
|
"idpUpdatedDescription": "Identity provider updated successfully",
|
||||||
"redirectUrl": "Redirect URL",
|
"redirectUrl": "Redirect URL",
|
||||||
|
"orgIdpRedirectUrls": "Redirect URLs",
|
||||||
"redirectUrlAbout": "About Redirect URL",
|
"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.",
|
"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",
|
"pangolinAuth": "Auth - Pangolin",
|
||||||
@@ -873,7 +900,7 @@
|
|||||||
"inviteAlready": "Looks like you've been invited!",
|
"inviteAlready": "Looks like you've been invited!",
|
||||||
"inviteAlreadyDescription": "To accept the invite, you must log in or create an account.",
|
"inviteAlreadyDescription": "To accept the invite, you must log in or create an account.",
|
||||||
"signupQuestion": "Already have an account?",
|
"signupQuestion": "Already have an account?",
|
||||||
"login": "Log in",
|
"login": "Log In",
|
||||||
"resourceNotFound": "Resource Not Found",
|
"resourceNotFound": "Resource Not Found",
|
||||||
"resourceNotFoundDescription": "The resource you're trying to access does not exist.",
|
"resourceNotFoundDescription": "The resource you're trying to access does not exist.",
|
||||||
"pincodeRequirementsLength": "PIN must be exactly 6 digits",
|
"pincodeRequirementsLength": "PIN must be exactly 6 digits",
|
||||||
@@ -953,13 +980,13 @@
|
|||||||
"passwordExpiryDescription": "This organization requires you to change your password every {maxDays} days.",
|
"passwordExpiryDescription": "This organization requires you to change your password every {maxDays} days.",
|
||||||
"changePasswordNow": "Change Password Now",
|
"changePasswordNow": "Change Password Now",
|
||||||
"pincodeAuth": "Authenticator Code",
|
"pincodeAuth": "Authenticator Code",
|
||||||
"pincodeSubmit2": "Submit Code",
|
"pincodeSubmit2": "Submit code",
|
||||||
"passwordResetSubmit": "Request Reset",
|
"passwordResetSubmit": "Request Reset",
|
||||||
"passwordResetAlreadyHaveCode": "Enter Code",
|
"passwordResetAlreadyHaveCode": "Enter Code",
|
||||||
"passwordResetSmtpRequired": "Please contact your administrator",
|
"passwordResetSmtpRequired": "Please contact your administrator",
|
||||||
"passwordResetSmtpRequiredDescription": "A password reset code is required to reset your password. Please contact your administrator for assistance.",
|
"passwordResetSmtpRequiredDescription": "A password reset code is required to reset your password. Please contact your administrator for assistance.",
|
||||||
"passwordBack": "Back to Password",
|
"passwordBack": "Back to Password",
|
||||||
"loginBack": "Go back to log in",
|
"loginBack": "Go back to main login page",
|
||||||
"signup": "Sign up",
|
"signup": "Sign up",
|
||||||
"loginStart": "Log in to get started",
|
"loginStart": "Log in to get started",
|
||||||
"idpOidcTokenValidating": "Validating OIDC token",
|
"idpOidcTokenValidating": "Validating OIDC token",
|
||||||
@@ -1117,6 +1144,10 @@
|
|||||||
"actionUpdateIdpOrg": "Update IDP Org",
|
"actionUpdateIdpOrg": "Update IDP Org",
|
||||||
"actionCreateClient": "Create Client",
|
"actionCreateClient": "Create Client",
|
||||||
"actionDeleteClient": "Delete Client",
|
"actionDeleteClient": "Delete Client",
|
||||||
|
"actionArchiveClient": "Archive Client",
|
||||||
|
"actionUnarchiveClient": "Unarchive Client",
|
||||||
|
"actionBlockClient": "Block Client",
|
||||||
|
"actionUnblockClient": "Unblock Client",
|
||||||
"actionUpdateClient": "Update Client",
|
"actionUpdateClient": "Update Client",
|
||||||
"actionListClients": "List Clients",
|
"actionListClients": "List Clients",
|
||||||
"actionGetClient": "Get Client",
|
"actionGetClient": "Get Client",
|
||||||
@@ -1133,14 +1164,14 @@
|
|||||||
"searchProgress": "Search...",
|
"searchProgress": "Search...",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"orgs": "Organizations",
|
"orgs": "Organizations",
|
||||||
"loginError": "An error occurred while logging in",
|
"loginError": "An unexpected error occurred. Please try again.",
|
||||||
"loginRequiredForDevice": "Login is required to authenticate your device.",
|
"loginRequiredForDevice": "Login is required for your device.",
|
||||||
"passwordForgot": "Forgot your password?",
|
"passwordForgot": "Forgot your password?",
|
||||||
"otpAuth": "Two-Factor Authentication",
|
"otpAuth": "Two-Factor Authentication",
|
||||||
"otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.",
|
"otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.",
|
||||||
"otpAuthSubmit": "Submit Code",
|
"otpAuthSubmit": "Submit Code",
|
||||||
"idpContinue": "Or continue with",
|
"idpContinue": "Or continue with",
|
||||||
"otpAuthBack": "Back to Log In",
|
"otpAuthBack": "Back to Password",
|
||||||
"navbar": "Navigation Menu",
|
"navbar": "Navigation Menu",
|
||||||
"navbarDescription": "Main navigation menu for the application",
|
"navbarDescription": "Main navigation menu for the application",
|
||||||
"navbarDocsLink": "Documentation",
|
"navbarDocsLink": "Documentation",
|
||||||
@@ -1188,6 +1219,7 @@
|
|||||||
"sidebarOverview": "Overview",
|
"sidebarOverview": "Overview",
|
||||||
"sidebarHome": "Home",
|
"sidebarHome": "Home",
|
||||||
"sidebarSites": "Sites",
|
"sidebarSites": "Sites",
|
||||||
|
"sidebarApprovals": "Approval Requests",
|
||||||
"sidebarResources": "Resources",
|
"sidebarResources": "Resources",
|
||||||
"sidebarProxyResources": "Public",
|
"sidebarProxyResources": "Public",
|
||||||
"sidebarClientResources": "Private",
|
"sidebarClientResources": "Private",
|
||||||
@@ -1204,7 +1236,7 @@
|
|||||||
"sidebarIdentityProviders": "Identity Providers",
|
"sidebarIdentityProviders": "Identity Providers",
|
||||||
"sidebarLicense": "License",
|
"sidebarLicense": "License",
|
||||||
"sidebarClients": "Clients",
|
"sidebarClients": "Clients",
|
||||||
"sidebarUserDevices": "Users",
|
"sidebarUserDevices": "User Devices",
|
||||||
"sidebarMachineClients": "Machines",
|
"sidebarMachineClients": "Machines",
|
||||||
"sidebarDomains": "Domains",
|
"sidebarDomains": "Domains",
|
||||||
"sidebarGeneral": "Manage",
|
"sidebarGeneral": "Manage",
|
||||||
@@ -1303,6 +1335,7 @@
|
|||||||
"refreshError": "Failed to refresh data",
|
"refreshError": "Failed to refresh data",
|
||||||
"verified": "Verified",
|
"verified": "Verified",
|
||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
|
"pendingApproval": "Pending Approval",
|
||||||
"sidebarBilling": "Billing",
|
"sidebarBilling": "Billing",
|
||||||
"billing": "Billing",
|
"billing": "Billing",
|
||||||
"orgBillingDescription": "Manage billing information and subscriptions",
|
"orgBillingDescription": "Manage billing information and subscriptions",
|
||||||
@@ -1419,7 +1452,7 @@
|
|||||||
"securityKeyRemoveSuccess": "Security key removed successfully",
|
"securityKeyRemoveSuccess": "Security key removed successfully",
|
||||||
"securityKeyRemoveError": "Failed to remove security key",
|
"securityKeyRemoveError": "Failed to remove security key",
|
||||||
"securityKeyLoadError": "Failed to load security keys",
|
"securityKeyLoadError": "Failed to load security keys",
|
||||||
"securityKeyLogin": "Continue with security key",
|
"securityKeyLogin": "Use Security Key",
|
||||||
"securityKeyAuthError": "Failed to authenticate with security key",
|
"securityKeyAuthError": "Failed to authenticate with security key",
|
||||||
"securityKeyRecommendation": "Register a backup security key on another device to ensure you always have access to your account.",
|
"securityKeyRecommendation": "Register a backup security key on another device to ensure you always have access to your account.",
|
||||||
"registering": "Registering...",
|
"registering": "Registering...",
|
||||||
@@ -1479,7 +1512,7 @@
|
|||||||
"IAgreeToThe": "I agree to the",
|
"IAgreeToThe": "I agree to the",
|
||||||
"termsOfService": "terms of service",
|
"termsOfService": "terms of service",
|
||||||
"and": "and",
|
"and": "and",
|
||||||
"privacyPolicy": "privacy policy"
|
"privacyPolicy": "privacy policy."
|
||||||
},
|
},
|
||||||
"signUpMarketing": {
|
"signUpMarketing": {
|
||||||
"keepMeInTheLoop": "Keep me in the loop with news, updates, and new features by email."
|
"keepMeInTheLoop": "Keep me in the loop with news, updates, and new features by email."
|
||||||
@@ -1546,6 +1579,8 @@
|
|||||||
"IntervalSeconds": "Healthy Interval",
|
"IntervalSeconds": "Healthy Interval",
|
||||||
"timeoutSeconds": "Timeout (sec)",
|
"timeoutSeconds": "Timeout (sec)",
|
||||||
"timeIsInSeconds": "Time is in seconds",
|
"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",
|
"retryAttempts": "Retry Attempts",
|
||||||
"expectedResponseCodes": "Expected Response Codes",
|
"expectedResponseCodes": "Expected Response Codes",
|
||||||
"expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.",
|
"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)",
|
"deviceCodeInvalidFormat": "Code must be 9 characters (e.g., A1AJ-N5JD)",
|
||||||
"deviceCodeInvalidOrExpired": "Invalid or expired code",
|
"deviceCodeInvalidOrExpired": "Invalid or expired code",
|
||||||
"deviceCodeVerifyFailed": "Failed to verify device code",
|
"deviceCodeVerifyFailed": "Failed to verify device code",
|
||||||
|
"deviceCodeValidating": "Validating device code...",
|
||||||
|
"deviceCodeVerifying": "Verifying device authorization...",
|
||||||
"signedInAs": "Signed in as",
|
"signedInAs": "Signed in as",
|
||||||
"deviceCodeEnterPrompt": "Enter the code displayed on the device",
|
"deviceCodeEnterPrompt": "Enter the code displayed on the device",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
@@ -2243,7 +2280,7 @@
|
|||||||
"deviceOrganizationsAccess": "Access to all organizations your account has access to",
|
"deviceOrganizationsAccess": "Access to all organizations your account has access to",
|
||||||
"deviceAuthorize": "Authorize {applicationName}",
|
"deviceAuthorize": "Authorize {applicationName}",
|
||||||
"deviceConnected": "Device Connected!",
|
"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",
|
"pangolinCloud": "Pangolin Cloud",
|
||||||
"viewDevices": "View Devices",
|
"viewDevices": "View Devices",
|
||||||
"viewDevicesDescription": "Manage your connected devices",
|
"viewDevicesDescription": "Manage your connected devices",
|
||||||
@@ -2305,6 +2342,7 @@
|
|||||||
"identifier": "Identifier",
|
"identifier": "Identifier",
|
||||||
"deviceLoginUseDifferentAccount": "Not you? Use a different account.",
|
"deviceLoginUseDifferentAccount": "Not you? Use a different account.",
|
||||||
"deviceLoginDeviceRequestingAccessToAccount": "A device is requesting access to this account.",
|
"deviceLoginDeviceRequestingAccessToAccount": "A device is requesting access to this account.",
|
||||||
|
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
|
||||||
"noData": "No Data",
|
"noData": "No Data",
|
||||||
"machineClients": "Machine Clients",
|
"machineClients": "Machine Clients",
|
||||||
"install": "Install",
|
"install": "Install",
|
||||||
@@ -2349,6 +2387,7 @@
|
|||||||
"enterConfirmation": "Enter confirmation",
|
"enterConfirmation": "Enter confirmation",
|
||||||
"blueprintViewDetails": "Details",
|
"blueprintViewDetails": "Details",
|
||||||
"defaultIdentityProvider": "Default Identity Provider",
|
"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",
|
"editInternalResourceDialogNetworkSettings": "Network Settings",
|
||||||
"editInternalResourceDialogAccessPolicy": "Access Policy",
|
"editInternalResourceDialogAccessPolicy": "Access Policy",
|
||||||
"editInternalResourceDialogAddRoles": "Add Roles",
|
"editInternalResourceDialogAddRoles": "Add Roles",
|
||||||
@@ -2392,5 +2431,56 @@
|
|||||||
"maintenanceScreenTitle": "Service Temporarily Unavailable",
|
"maintenanceScreenTitle": "Service Temporarily Unavailable",
|
||||||
"maintenanceScreenMessage": "We are currently experiencing technical difficulties. Please check back soon.",
|
"maintenanceScreenMessage": "We are currently experiencing technical difficulties. Please check back soon.",
|
||||||
"maintenanceScreenEstimatedCompletion": "Estimated Completion:",
|
"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",
|
"orgPolicyConfig": "Configurar acceso para una organización",
|
||||||
"idpUpdatedDescription": "Proveedor de identidad actualizado correctamente",
|
"idpUpdatedDescription": "Proveedor de identidad actualizado correctamente",
|
||||||
"redirectUrl": "URL de redirección",
|
"redirectUrl": "URL de redirección",
|
||||||
|
"orgIdpRedirectUrls": "Redirigir URL",
|
||||||
"redirectUrlAbout": "Acerca de la URL de redirección",
|
"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.",
|
"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",
|
"pangolinAuth": "Autenticación - Pangolin",
|
||||||
@@ -1479,7 +1480,7 @@
|
|||||||
"IAgreeToThe": "Estoy de acuerdo con los",
|
"IAgreeToThe": "Estoy de acuerdo con los",
|
||||||
"termsOfService": "términos del servicio",
|
"termsOfService": "términos del servicio",
|
||||||
"and": "y",
|
"and": "y",
|
||||||
"privacyPolicy": "política de privacidad"
|
"privacyPolicy": "política de privacidad."
|
||||||
},
|
},
|
||||||
"signUpMarketing": {
|
"signUpMarketing": {
|
||||||
"keepMeInTheLoop": "Mantenerme en el bucle con noticias, actualizaciones y nuevas características por correo electrónico."
|
"keepMeInTheLoop": "Mantenerme en el bucle con noticias, actualizaciones y nuevas características por correo electrónico."
|
||||||
@@ -2349,6 +2350,7 @@
|
|||||||
"enterConfirmation": "Ingresar confirmación",
|
"enterConfirmation": "Ingresar confirmación",
|
||||||
"blueprintViewDetails": "Detalles",
|
"blueprintViewDetails": "Detalles",
|
||||||
"defaultIdentityProvider": "Proveedor de identidad predeterminado",
|
"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",
|
"editInternalResourceDialogNetworkSettings": "Configuración de red",
|
||||||
"editInternalResourceDialogAccessPolicy": "Política de acceso",
|
"editInternalResourceDialogAccessPolicy": "Política de acceso",
|
||||||
"editInternalResourceDialogAddRoles": "Agregar roles",
|
"editInternalResourceDialogAddRoles": "Agregar roles",
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"orgPolicyConfig": "Configurer l'accès pour une organisation",
|
"orgPolicyConfig": "Configurer l'accès pour une organisation",
|
||||||
"idpUpdatedDescription": "Fournisseur d'identité mis à jour avec succès",
|
"idpUpdatedDescription": "Fournisseur d'identité mis à jour avec succès",
|
||||||
"redirectUrl": "URL de redirection",
|
"redirectUrl": "URL de redirection",
|
||||||
|
"orgIdpRedirectUrls": "URL de redirection",
|
||||||
"redirectUrlAbout": "À propos de l'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é.",
|
"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",
|
"pangolinAuth": "Auth - Pangolin",
|
||||||
@@ -1479,7 +1480,7 @@
|
|||||||
"IAgreeToThe": "Je suis d'accord avec",
|
"IAgreeToThe": "Je suis d'accord avec",
|
||||||
"termsOfService": "les conditions d'utilisation",
|
"termsOfService": "les conditions d'utilisation",
|
||||||
"and": "et",
|
"and": "et",
|
||||||
"privacyPolicy": "la politique de confidentialité"
|
"privacyPolicy": "politique de confidentialité."
|
||||||
},
|
},
|
||||||
"signUpMarketing": {
|
"signUpMarketing": {
|
||||||
"keepMeInTheLoop": "Gardez-moi dans la boucle avec des nouvelles, des mises à jour et de nouvelles fonctionnalités par courriel."
|
"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",
|
"enterConfirmation": "Entrez la confirmation",
|
||||||
"blueprintViewDetails": "Détails",
|
"blueprintViewDetails": "Détails",
|
||||||
"defaultIdentityProvider": "Fournisseur d'identité par défaut",
|
"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",
|
"editInternalResourceDialogNetworkSettings": "Paramètres réseau",
|
||||||
"editInternalResourceDialogAccessPolicy": "Politique d'accès",
|
"editInternalResourceDialogAccessPolicy": "Politique d'accès",
|
||||||
"editInternalResourceDialogAddRoles": "Ajouter des rôles",
|
"editInternalResourceDialogAddRoles": "Ajouter des rôles",
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"orgPolicyConfig": "Configura l'accesso per un'organizzazione",
|
"orgPolicyConfig": "Configura l'accesso per un'organizzazione",
|
||||||
"idpUpdatedDescription": "Provider di identità aggiornato con successo",
|
"idpUpdatedDescription": "Provider di identità aggiornato con successo",
|
||||||
"redirectUrl": "URL di Reindirizzamento",
|
"redirectUrl": "URL di Reindirizzamento",
|
||||||
|
"orgIdpRedirectUrls": "Reindirizza URL",
|
||||||
"redirectUrlAbout": "Informazioni sull'URL di Reindirizzamento",
|
"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à.",
|
"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",
|
"pangolinAuth": "Autenticazione - Pangolina",
|
||||||
@@ -1479,7 +1480,7 @@
|
|||||||
"IAgreeToThe": "Accetto i",
|
"IAgreeToThe": "Accetto i",
|
||||||
"termsOfService": "termini di servizio",
|
"termsOfService": "termini di servizio",
|
||||||
"and": "e",
|
"and": "e",
|
||||||
"privacyPolicy": "informativa sulla privacy"
|
"privacyPolicy": "informativa sulla privacy."
|
||||||
},
|
},
|
||||||
"signUpMarketing": {
|
"signUpMarketing": {
|
||||||
"keepMeInTheLoop": "Tienimi in loop con notizie, aggiornamenti e nuove funzionalità via e-mail."
|
"keepMeInTheLoop": "Tienimi in loop con notizie, aggiornamenti e nuove funzionalità via e-mail."
|
||||||
@@ -2349,6 +2350,7 @@
|
|||||||
"enterConfirmation": "Inserisci conferma",
|
"enterConfirmation": "Inserisci conferma",
|
||||||
"blueprintViewDetails": "Dettagli",
|
"blueprintViewDetails": "Dettagli",
|
||||||
"defaultIdentityProvider": "Provider di Identità Predefinito",
|
"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",
|
"editInternalResourceDialogNetworkSettings": "Impostazioni di Rete",
|
||||||
"editInternalResourceDialogAccessPolicy": "Politica di Accesso",
|
"editInternalResourceDialogAccessPolicy": "Politica di Accesso",
|
||||||
"editInternalResourceDialogAddRoles": "Aggiungi Ruoli",
|
"editInternalResourceDialogAddRoles": "Aggiungi Ruoli",
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"orgPolicyConfig": "조직에 대한 접근을 구성하십시오.",
|
"orgPolicyConfig": "조직에 대한 접근을 구성하십시오.",
|
||||||
"idpUpdatedDescription": "아이덴티티 제공자가 성공적으로 업데이트되었습니다",
|
"idpUpdatedDescription": "아이덴티티 제공자가 성공적으로 업데이트되었습니다",
|
||||||
"redirectUrl": "리디렉션 URL",
|
"redirectUrl": "리디렉션 URL",
|
||||||
|
"orgIdpRedirectUrls": "리디렉션 URL",
|
||||||
"redirectUrlAbout": "리디렉션 URL에 대한 정보",
|
"redirectUrlAbout": "리디렉션 URL에 대한 정보",
|
||||||
"redirectUrlAboutDescription": "사용자가 인증 후 리디렉션될 URL입니다. 이 URL을 신원 제공자 설정에서 구성해야 합니다.",
|
"redirectUrlAboutDescription": "사용자가 인증 후 리디렉션될 URL입니다. 이 URL을 신원 제공자 설정에서 구성해야 합니다.",
|
||||||
"pangolinAuth": "인증 - 판골린",
|
"pangolinAuth": "인증 - 판골린",
|
||||||
@@ -1479,7 +1480,7 @@
|
|||||||
"IAgreeToThe": "동의합니다",
|
"IAgreeToThe": "동의합니다",
|
||||||
"termsOfService": "서비스 약관",
|
"termsOfService": "서비스 약관",
|
||||||
"and": "및",
|
"and": "및",
|
||||||
"privacyPolicy": "개인 정보 보호 정책"
|
"privacyPolicy": "개인 정보 보호 정책."
|
||||||
},
|
},
|
||||||
"signUpMarketing": {
|
"signUpMarketing": {
|
||||||
"keepMeInTheLoop": "이메일을 통해 소식, 업데이트 및 새로운 기능을 받아보세요."
|
"keepMeInTheLoop": "이메일을 통해 소식, 업데이트 및 새로운 기능을 받아보세요."
|
||||||
@@ -2349,6 +2350,7 @@
|
|||||||
"enterConfirmation": "확인 입력",
|
"enterConfirmation": "확인 입력",
|
||||||
"blueprintViewDetails": "세부 정보",
|
"blueprintViewDetails": "세부 정보",
|
||||||
"defaultIdentityProvider": "기본 아이덴티티 공급자",
|
"defaultIdentityProvider": "기본 아이덴티티 공급자",
|
||||||
|
"defaultIdentityProviderDescription": "기본 ID 공급자가 선택되면, 사용자는 인증을 위해 자동으로 해당 공급자로 리디렉션됩니다.",
|
||||||
"editInternalResourceDialogNetworkSettings": "네트워크 설정",
|
"editInternalResourceDialogNetworkSettings": "네트워크 설정",
|
||||||
"editInternalResourceDialogAccessPolicy": "액세스 정책",
|
"editInternalResourceDialogAccessPolicy": "액세스 정책",
|
||||||
"editInternalResourceDialogAddRoles": "역할 추가",
|
"editInternalResourceDialogAddRoles": "역할 추가",
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"orgPolicyConfig": "Konfigurer tilgang for en organisasjon",
|
"orgPolicyConfig": "Konfigurer tilgang for en organisasjon",
|
||||||
"idpUpdatedDescription": "Identitetsleverandør vellykket oppdatert",
|
"idpUpdatedDescription": "Identitetsleverandør vellykket oppdatert",
|
||||||
"redirectUrl": "Omdirigerings-URL",
|
"redirectUrl": "Omdirigerings-URL",
|
||||||
|
"orgIdpRedirectUrls": "Omadressere URL'er",
|
||||||
"redirectUrlAbout": "Om omdirigerings-URL",
|
"redirectUrlAbout": "Om omdirigerings-URL",
|
||||||
"redirectUrlAboutDescription": "Dette er URLen som brukere vil bli omdirigert etter autentisering. Du må konfigurere denne URLen i identitetsleverandørens innstillinger.",
|
"redirectUrlAboutDescription": "Dette er URLen som brukere vil bli omdirigert etter autentisering. Du må konfigurere denne URLen i identitetsleverandørens innstillinger.",
|
||||||
"pangolinAuth": "Autentisering - Pangolin",
|
"pangolinAuth": "Autentisering - Pangolin",
|
||||||
@@ -1479,7 +1480,7 @@
|
|||||||
"IAgreeToThe": "Jeg godtar",
|
"IAgreeToThe": "Jeg godtar",
|
||||||
"termsOfService": "brukervilkårene",
|
"termsOfService": "brukervilkårene",
|
||||||
"and": "og",
|
"and": "og",
|
||||||
"privacyPolicy": "personvernerklæringen"
|
"privacyPolicy": "retningslinjer for personvern"
|
||||||
},
|
},
|
||||||
"signUpMarketing": {
|
"signUpMarketing": {
|
||||||
"keepMeInTheLoop": "Hold meg i løken med nyheter, oppdateringer og nye funksjoner via e-post."
|
"keepMeInTheLoop": "Hold meg i løken med nyheter, oppdateringer og nye funksjoner via e-post."
|
||||||
@@ -2349,6 +2350,7 @@
|
|||||||
"enterConfirmation": "Skriv inn bekreftelse",
|
"enterConfirmation": "Skriv inn bekreftelse",
|
||||||
"blueprintViewDetails": "Detaljer",
|
"blueprintViewDetails": "Detaljer",
|
||||||
"defaultIdentityProvider": "Standard identitetsleverandør",
|
"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",
|
"editInternalResourceDialogNetworkSettings": "Nettverksinnstillinger",
|
||||||
"editInternalResourceDialogAccessPolicy": "Tilgangsregler for tilgang",
|
"editInternalResourceDialogAccessPolicy": "Tilgangsregler for tilgang",
|
||||||
"editInternalResourceDialogAddRoles": "Legg til roller",
|
"editInternalResourceDialogAddRoles": "Legg til roller",
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"orgPolicyConfig": "Toegang voor een organisatie configureren",
|
"orgPolicyConfig": "Toegang voor een organisatie configureren",
|
||||||
"idpUpdatedDescription": "Identity provider succesvol bijgewerkt",
|
"idpUpdatedDescription": "Identity provider succesvol bijgewerkt",
|
||||||
"redirectUrl": "Omleidings URL",
|
"redirectUrl": "Omleidings URL",
|
||||||
|
"orgIdpRedirectUrls": "URL's omleiden",
|
||||||
"redirectUrlAbout": "Over omleidings-URL",
|
"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.",
|
"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",
|
"pangolinAuth": "Authenticatie - Pangolin",
|
||||||
@@ -1479,7 +1480,7 @@
|
|||||||
"IAgreeToThe": "Ik ga akkoord met de",
|
"IAgreeToThe": "Ik ga akkoord met de",
|
||||||
"termsOfService": "servicevoorwaarden",
|
"termsOfService": "servicevoorwaarden",
|
||||||
"and": "en",
|
"and": "en",
|
||||||
"privacyPolicy": "privacybeleid"
|
"privacyPolicy": "privacy beleid"
|
||||||
},
|
},
|
||||||
"signUpMarketing": {
|
"signUpMarketing": {
|
||||||
"keepMeInTheLoop": "Houd me op de hoogte met nieuws, updates en nieuwe functies per e-mail."
|
"keepMeInTheLoop": "Houd me op de hoogte met nieuws, updates en nieuwe functies per e-mail."
|
||||||
@@ -2349,6 +2350,7 @@
|
|||||||
"enterConfirmation": "Bevestiging invoeren",
|
"enterConfirmation": "Bevestiging invoeren",
|
||||||
"blueprintViewDetails": "Details",
|
"blueprintViewDetails": "Details",
|
||||||
"defaultIdentityProvider": "Standaard Identiteitsprovider",
|
"defaultIdentityProvider": "Standaard Identiteitsprovider",
|
||||||
|
"defaultIdentityProviderDescription": "Wanneer een standaard identity provider is geselecteerd, zal de gebruiker automatisch worden doorgestuurd naar de provider voor authenticatie.",
|
||||||
"editInternalResourceDialogNetworkSettings": "Netwerkinstellingen",
|
"editInternalResourceDialogNetworkSettings": "Netwerkinstellingen",
|
||||||
"editInternalResourceDialogAccessPolicy": "Toegangsbeleid",
|
"editInternalResourceDialogAccessPolicy": "Toegangsbeleid",
|
||||||
"editInternalResourceDialogAddRoles": "Rollen toevoegen",
|
"editInternalResourceDialogAddRoles": "Rollen toevoegen",
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"orgPolicyConfig": "Skonfiguruj dostęp dla organizacji",
|
"orgPolicyConfig": "Skonfiguruj dostęp dla organizacji",
|
||||||
"idpUpdatedDescription": "Dostawca tożsamości został pomyślnie zaktualizowany",
|
"idpUpdatedDescription": "Dostawca tożsamości został pomyślnie zaktualizowany",
|
||||||
"redirectUrl": "URL przekierowania",
|
"redirectUrl": "URL przekierowania",
|
||||||
|
"orgIdpRedirectUrls": "Przekieruj adresy URL",
|
||||||
"redirectUrlAbout": "O URL przekierowania",
|
"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.",
|
"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",
|
"pangolinAuth": "Autoryzacja - Pangolin",
|
||||||
@@ -1479,7 +1480,7 @@
|
|||||||
"IAgreeToThe": "Zgadzam się z",
|
"IAgreeToThe": "Zgadzam się z",
|
||||||
"termsOfService": "warunkami usługi",
|
"termsOfService": "warunkami usługi",
|
||||||
"and": "oraz",
|
"and": "oraz",
|
||||||
"privacyPolicy": "polityką prywatności"
|
"privacyPolicy": "polityka prywatności."
|
||||||
},
|
},
|
||||||
"signUpMarketing": {
|
"signUpMarketing": {
|
||||||
"keepMeInTheLoop": "Zachowaj mnie w pętli z wiadomościami, aktualizacjami i nowymi funkcjami przez e-mail."
|
"keepMeInTheLoop": "Zachowaj mnie w pętli z wiadomościami, aktualizacjami i nowymi funkcjami przez e-mail."
|
||||||
@@ -2349,6 +2350,7 @@
|
|||||||
"enterConfirmation": "Wprowadź potwierdzenie",
|
"enterConfirmation": "Wprowadź potwierdzenie",
|
||||||
"blueprintViewDetails": "Szczegóły",
|
"blueprintViewDetails": "Szczegóły",
|
||||||
"defaultIdentityProvider": "Domyślny dostawca tożsamości",
|
"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",
|
"editInternalResourceDialogNetworkSettings": "Ustawienia sieci",
|
||||||
"editInternalResourceDialogAccessPolicy": "Polityka dostępowa",
|
"editInternalResourceDialogAccessPolicy": "Polityka dostępowa",
|
||||||
"editInternalResourceDialogAddRoles": "Dodaj role",
|
"editInternalResourceDialogAddRoles": "Dodaj role",
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"orgPolicyConfig": "Configurar acesso para uma organização",
|
"orgPolicyConfig": "Configurar acesso para uma organização",
|
||||||
"idpUpdatedDescription": "Provedor de identidade atualizado com sucesso",
|
"idpUpdatedDescription": "Provedor de identidade atualizado com sucesso",
|
||||||
"redirectUrl": "URL de Redirecionamento",
|
"redirectUrl": "URL de Redirecionamento",
|
||||||
|
"orgIdpRedirectUrls": "Redirecionar URLs",
|
||||||
"redirectUrlAbout": "Sobre o URL de Redirecionamento",
|
"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.",
|
"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",
|
"pangolinAuth": "Autenticação - Pangolin",
|
||||||
@@ -1479,7 +1480,7 @@
|
|||||||
"IAgreeToThe": "Concordo com",
|
"IAgreeToThe": "Concordo com",
|
||||||
"termsOfService": "os termos de serviço",
|
"termsOfService": "os termos de serviço",
|
||||||
"and": "e",
|
"and": "e",
|
||||||
"privacyPolicy": "política de privacidade"
|
"privacyPolicy": "política de privacidade."
|
||||||
},
|
},
|
||||||
"signUpMarketing": {
|
"signUpMarketing": {
|
||||||
"keepMeInTheLoop": "Mantenha-me à disposição com notícias, atualizações e novos recursos por e-mail."
|
"keepMeInTheLoop": "Mantenha-me à disposição com notícias, atualizações e novos recursos por e-mail."
|
||||||
@@ -2349,6 +2350,7 @@
|
|||||||
"enterConfirmation": "Inserir confirmação",
|
"enterConfirmation": "Inserir confirmação",
|
||||||
"blueprintViewDetails": "Detalhes",
|
"blueprintViewDetails": "Detalhes",
|
||||||
"defaultIdentityProvider": "Provedor de Identidade Padrão",
|
"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",
|
"editInternalResourceDialogNetworkSettings": "Configurações de Rede",
|
||||||
"editInternalResourceDialogAccessPolicy": "Política de Acesso",
|
"editInternalResourceDialogAccessPolicy": "Política de Acesso",
|
||||||
"editInternalResourceDialogAddRoles": "Adicionar Funções",
|
"editInternalResourceDialogAddRoles": "Adicionar Funções",
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"orgPolicyConfig": "Настроить доступ для организации",
|
"orgPolicyConfig": "Настроить доступ для организации",
|
||||||
"idpUpdatedDescription": "Поставщик удостоверений успешно обновлён",
|
"idpUpdatedDescription": "Поставщик удостоверений успешно обновлён",
|
||||||
"redirectUrl": "URL редиректа",
|
"redirectUrl": "URL редиректа",
|
||||||
|
"orgIdpRedirectUrls": "Перенаправление URL",
|
||||||
"redirectUrlAbout": "О редиректе URL",
|
"redirectUrlAbout": "О редиректе URL",
|
||||||
"redirectUrlAboutDescription": "Это URL, на который пользователи будут перенаправлены после аутентификации. Вам нужно настроить этот URL в настройках провайдера.",
|
"redirectUrlAboutDescription": "Это URL, на который пользователи будут перенаправлены после аутентификации. Вам нужно настроить этот URL в настройках провайдера.",
|
||||||
"pangolinAuth": "Аутентификация - Pangolin",
|
"pangolinAuth": "Аутентификация - Pangolin",
|
||||||
@@ -1479,7 +1480,7 @@
|
|||||||
"IAgreeToThe": "Я согласен с",
|
"IAgreeToThe": "Я согласен с",
|
||||||
"termsOfService": "условия использования",
|
"termsOfService": "условия использования",
|
||||||
"and": "и",
|
"and": "и",
|
||||||
"privacyPolicy": "политика конфиденциальности"
|
"privacyPolicy": "политика конфиденциальности."
|
||||||
},
|
},
|
||||||
"signUpMarketing": {
|
"signUpMarketing": {
|
||||||
"keepMeInTheLoop": "Держите меня в цикле с новостями, обновлениями и новыми функциями по электронной почте."
|
"keepMeInTheLoop": "Держите меня в цикле с новостями, обновлениями и новыми функциями по электронной почте."
|
||||||
@@ -2349,6 +2350,7 @@
|
|||||||
"enterConfirmation": "Введите подтверждение",
|
"enterConfirmation": "Введите подтверждение",
|
||||||
"blueprintViewDetails": "Подробности",
|
"blueprintViewDetails": "Подробности",
|
||||||
"defaultIdentityProvider": "Поставщик удостоверений по умолчанию",
|
"defaultIdentityProvider": "Поставщик удостоверений по умолчанию",
|
||||||
|
"defaultIdentityProviderDescription": "Когда выбран поставщик идентификации по умолчанию, пользователь будет автоматически перенаправлен на провайдер для аутентификации.",
|
||||||
"editInternalResourceDialogNetworkSettings": "Настройки сети",
|
"editInternalResourceDialogNetworkSettings": "Настройки сети",
|
||||||
"editInternalResourceDialogAccessPolicy": "Политика доступа",
|
"editInternalResourceDialogAccessPolicy": "Политика доступа",
|
||||||
"editInternalResourceDialogAddRoles": "Добавить роли",
|
"editInternalResourceDialogAddRoles": "Добавить роли",
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"orgPolicyConfig": "Bir kuruluş için erişimi yapılandırın",
|
"orgPolicyConfig": "Bir kuruluş için erişimi yapılandırın",
|
||||||
"idpUpdatedDescription": "Kimlik sağlayıcı başarıyla güncellendi",
|
"idpUpdatedDescription": "Kimlik sağlayıcı başarıyla güncellendi",
|
||||||
"redirectUrl": "Yönlendirme URL'si",
|
"redirectUrl": "Yönlendirme URL'si",
|
||||||
|
"orgIdpRedirectUrls": "Yönlendirme URL'leri",
|
||||||
"redirectUrlAbout": "Yönlendirme URL'si Hakkında",
|
"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.",
|
"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",
|
"pangolinAuth": "Yetkilendirme - Pangolin",
|
||||||
@@ -1479,7 +1480,7 @@
|
|||||||
"IAgreeToThe": "Kabul ediyorum",
|
"IAgreeToThe": "Kabul ediyorum",
|
||||||
"termsOfService": "hizmet şartları",
|
"termsOfService": "hizmet şartları",
|
||||||
"and": "ve",
|
"and": "ve",
|
||||||
"privacyPolicy": "gizlilik politikası"
|
"privacyPolicy": "gizlilik politikası."
|
||||||
},
|
},
|
||||||
"signUpMarketing": {
|
"signUpMarketing": {
|
||||||
"keepMeInTheLoop": "Bana e-posta yoluyla haberler, güncellemeler ve yeni özellikler hakkında bilgi verin."
|
"keepMeInTheLoop": "Bana e-posta yoluyla haberler, güncellemeler ve yeni özellikler hakkında bilgi verin."
|
||||||
@@ -2349,6 +2350,7 @@
|
|||||||
"enterConfirmation": "Onayı girin",
|
"enterConfirmation": "Onayı girin",
|
||||||
"blueprintViewDetails": "Detaylar",
|
"blueprintViewDetails": "Detaylar",
|
||||||
"defaultIdentityProvider": "Varsayılan Kimlik Sağlayıcı",
|
"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ı",
|
"editInternalResourceDialogNetworkSettings": "Ağ Ayarları",
|
||||||
"editInternalResourceDialogAccessPolicy": "Erişim Politikası",
|
"editInternalResourceDialogAccessPolicy": "Erişim Politikası",
|
||||||
"editInternalResourceDialogAddRoles": "Roller Ekle",
|
"editInternalResourceDialogAddRoles": "Roller Ekle",
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"orgPolicyConfig": "配置组织访问权限",
|
"orgPolicyConfig": "配置组织访问权限",
|
||||||
"idpUpdatedDescription": "身份提供商更新成功",
|
"idpUpdatedDescription": "身份提供商更新成功",
|
||||||
"redirectUrl": "重定向网址",
|
"redirectUrl": "重定向网址",
|
||||||
|
"orgIdpRedirectUrls": "重定向URL",
|
||||||
"redirectUrlAbout": "关于重定向网址",
|
"redirectUrlAbout": "关于重定向网址",
|
||||||
"redirectUrlAboutDescription": "这是用户在验证后将被重定向到的URL。您需要在身份提供者的设置中配置此URL。",
|
"redirectUrlAboutDescription": "这是用户在验证后将被重定向到的URL。您需要在身份提供者的设置中配置此URL。",
|
||||||
"pangolinAuth": "认证 - Pangolin",
|
"pangolinAuth": "认证 - Pangolin",
|
||||||
@@ -1479,7 +1480,7 @@
|
|||||||
"IAgreeToThe": "我同意",
|
"IAgreeToThe": "我同意",
|
||||||
"termsOfService": "服务条款",
|
"termsOfService": "服务条款",
|
||||||
"and": "和",
|
"and": "和",
|
||||||
"privacyPolicy": "隐私政策"
|
"privacyPolicy": "隐私政策。"
|
||||||
},
|
},
|
||||||
"signUpMarketing": {
|
"signUpMarketing": {
|
||||||
"keepMeInTheLoop": "通过电子邮件让我在循环中保持新闻、更新和新功能。"
|
"keepMeInTheLoop": "通过电子邮件让我在循环中保持新闻、更新和新功能。"
|
||||||
@@ -2349,6 +2350,7 @@
|
|||||||
"enterConfirmation": "输入确认",
|
"enterConfirmation": "输入确认",
|
||||||
"blueprintViewDetails": "详细信息",
|
"blueprintViewDetails": "详细信息",
|
||||||
"defaultIdentityProvider": "默认身份提供商",
|
"defaultIdentityProvider": "默认身份提供商",
|
||||||
|
"defaultIdentityProviderDescription": "当选择默认身份提供商时,用户将自动重定向到提供商进行身份验证。",
|
||||||
"editInternalResourceDialogNetworkSettings": "网络设置",
|
"editInternalResourceDialogNetworkSettings": "网络设置",
|
||||||
"editInternalResourceDialogAccessPolicy": "访问策略",
|
"editInternalResourceDialogAccessPolicy": "访问策略",
|
||||||
"editInternalResourceDialogAddRoles": "添加角色",
|
"editInternalResourceDialogAddRoles": "添加角色",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"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",
|
"homepage": "https://github.com/fosrl/pangolin",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ export enum ActionsEnum {
|
|||||||
updateSiteResource = "updateSiteResource",
|
updateSiteResource = "updateSiteResource",
|
||||||
createClient = "createClient",
|
createClient = "createClient",
|
||||||
deleteClient = "deleteClient",
|
deleteClient = "deleteClient",
|
||||||
|
archiveClient = "archiveClient",
|
||||||
|
unarchiveClient = "unarchiveClient",
|
||||||
|
blockClient = "blockClient",
|
||||||
|
unblockClient = "unblockClient",
|
||||||
updateClient = "updateClient",
|
updateClient = "updateClient",
|
||||||
listClients = "listClients",
|
listClients = "listClients",
|
||||||
getClient = "getClient",
|
getClient = "getClient",
|
||||||
@@ -125,7 +129,9 @@ export enum ActionsEnum {
|
|||||||
getBlueprint = "getBlueprint",
|
getBlueprint = "getBlueprint",
|
||||||
applyBlueprint = "applyBlueprint",
|
applyBlueprint = "applyBlueprint",
|
||||||
viewLogs = "viewLogs",
|
viewLogs = "viewLogs",
|
||||||
exportLogs = "exportLogs"
|
exportLogs = "exportLogs",
|
||||||
|
listApprovals = "listApprovals",
|
||||||
|
updateApprovals = "updateApprovals"
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkUserActionPermission(
|
export async function checkUserActionPermission(
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const MAJOR_ASNS = [
|
|||||||
code: "AS36351",
|
code: "AS36351",
|
||||||
asn: 36351
|
asn: 36351
|
||||||
},
|
},
|
||||||
|
|
||||||
// CDNs
|
// CDNs
|
||||||
{
|
{
|
||||||
name: "Cloudflare",
|
name: "Cloudflare",
|
||||||
@@ -90,7 +90,7 @@ export const MAJOR_ASNS = [
|
|||||||
code: "AS16625",
|
code: "AS16625",
|
||||||
asn: 16625
|
asn: 16625
|
||||||
},
|
},
|
||||||
|
|
||||||
// Mobile Carriers - US
|
// Mobile Carriers - US
|
||||||
{
|
{
|
||||||
name: "T-Mobile USA",
|
name: "T-Mobile USA",
|
||||||
@@ -117,7 +117,7 @@ export const MAJOR_ASNS = [
|
|||||||
code: "AS6430",
|
code: "AS6430",
|
||||||
asn: 6430
|
asn: 6430
|
||||||
},
|
},
|
||||||
|
|
||||||
// Mobile Carriers - Europe
|
// Mobile Carriers - Europe
|
||||||
{
|
{
|
||||||
name: "Vodafone UK",
|
name: "Vodafone UK",
|
||||||
@@ -144,7 +144,7 @@ export const MAJOR_ASNS = [
|
|||||||
code: "AS12430",
|
code: "AS12430",
|
||||||
asn: 12430
|
asn: 12430
|
||||||
},
|
},
|
||||||
|
|
||||||
// Mobile Carriers - Asia
|
// Mobile Carriers - Asia
|
||||||
{
|
{
|
||||||
name: "NTT DoCoMo (Japan)",
|
name: "NTT DoCoMo (Japan)",
|
||||||
@@ -176,7 +176,7 @@ export const MAJOR_ASNS = [
|
|||||||
code: "AS9808",
|
code: "AS9808",
|
||||||
asn: 9808
|
asn: 9808
|
||||||
},
|
},
|
||||||
|
|
||||||
// Major US ISPs
|
// Major US ISPs
|
||||||
{
|
{
|
||||||
name: "AT&T Services",
|
name: "AT&T Services",
|
||||||
@@ -208,7 +208,7 @@ export const MAJOR_ASNS = [
|
|||||||
code: "AS209",
|
code: "AS209",
|
||||||
asn: 209
|
asn: 209
|
||||||
},
|
},
|
||||||
|
|
||||||
// Major European ISPs
|
// Major European ISPs
|
||||||
{
|
{
|
||||||
name: "Deutsche Telekom",
|
name: "Deutsche Telekom",
|
||||||
@@ -235,7 +235,7 @@ export const MAJOR_ASNS = [
|
|||||||
code: "AS12956",
|
code: "AS12956",
|
||||||
asn: 12956
|
asn: 12956
|
||||||
},
|
},
|
||||||
|
|
||||||
// Major Asian ISPs
|
// Major Asian ISPs
|
||||||
{
|
{
|
||||||
name: "China Telecom",
|
name: "China Telecom",
|
||||||
@@ -262,7 +262,7 @@ export const MAJOR_ASNS = [
|
|||||||
code: "AS55836",
|
code: "AS55836",
|
||||||
asn: 55836
|
asn: 55836
|
||||||
},
|
},
|
||||||
|
|
||||||
// VPN/Proxy Providers
|
// VPN/Proxy Providers
|
||||||
{
|
{
|
||||||
name: "Private Internet Access",
|
name: "Private Internet Access",
|
||||||
@@ -279,7 +279,7 @@ export const MAJOR_ASNS = [
|
|||||||
code: "AS213281",
|
code: "AS213281",
|
||||||
asn: 213281
|
asn: 213281
|
||||||
},
|
},
|
||||||
|
|
||||||
// Social Media / Major Tech
|
// Social Media / Major Tech
|
||||||
{
|
{
|
||||||
name: "Facebook/Meta",
|
name: "Facebook/Meta",
|
||||||
@@ -301,7 +301,7 @@ export const MAJOR_ASNS = [
|
|||||||
code: "AS2906",
|
code: "AS2906",
|
||||||
asn: 2906
|
asn: 2906
|
||||||
},
|
},
|
||||||
|
|
||||||
// Academic/Research
|
// Academic/Research
|
||||||
{
|
{
|
||||||
name: "MIT",
|
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"));
|
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> {
|
export async function getUniqueClientName(orgId: string): Promise<string> {
|
||||||
let loops = 0;
|
let loops = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -159,3 +177,29 @@ export function generateName(): string {
|
|||||||
// clean out any non-alphanumeric characters except for dashes
|
// clean out any non-alphanumeric characters except for dashes
|
||||||
return name.replace(/[^a-z0-9-]/g, "");
|
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
|
index
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
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", {
|
export const certificates = pgTable("certificates", {
|
||||||
certId: serial("certId").primaryKey(),
|
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 Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
export type Certificate = InferSelectModel<typeof certificates>;
|
export type Certificate = InferSelectModel<typeof certificates>;
|
||||||
|
|||||||
@@ -134,13 +134,15 @@ export const resources = pgTable("resources", {
|
|||||||
proxyProtocol: boolean("proxyProtocol").notNull().default(false),
|
proxyProtocol: boolean("proxyProtocol").notNull().default(false),
|
||||||
proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
|
proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
|
||||||
|
|
||||||
maintenanceModeEnabled: boolean("maintenanceModeEnabled").notNull().default(false),
|
maintenanceModeEnabled: boolean("maintenanceModeEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
maintenanceModeType: text("maintenanceModeType", {
|
maintenanceModeType: text("maintenanceModeType", {
|
||||||
enum: ["forced", "automatic"]
|
enum: ["forced", "automatic"]
|
||||||
}).default("forced"), // "forced" = always show, "automatic" = only when down
|
}).default("forced"), // "forced" = always show, "automatic" = only when down
|
||||||
maintenanceTitle: text("maintenanceTitle"),
|
maintenanceTitle: text("maintenanceTitle"),
|
||||||
maintenanceMessage: text("maintenanceMessage"),
|
maintenanceMessage: text("maintenanceMessage"),
|
||||||
maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
|
maintenanceEstimatedTime: text("maintenanceEstimatedTime")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = pgTable("targets", {
|
export const targets = pgTable("targets", {
|
||||||
@@ -223,8 +225,8 @@ export const siteResources = pgTable("siteResources", {
|
|||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
alias: varchar("alias"),
|
alias: varchar("alias"),
|
||||||
aliasAddress: varchar("aliasAddress"),
|
aliasAddress: varchar("aliasAddress"),
|
||||||
tcpPortRangeString: varchar("tcpPortRangeString"),
|
tcpPortRangeString: varchar("tcpPortRangeString").notNull().default("*"),
|
||||||
udpPortRangeString: varchar("udpPortRangeString"),
|
udpPortRangeString: varchar("udpPortRangeString").notNull().default("*"),
|
||||||
disableIcmp: boolean("disableIcmp").notNull().default(false)
|
disableIcmp: boolean("disableIcmp").notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -363,7 +365,8 @@ export const roles = pgTable("roles", {
|
|||||||
.notNull(),
|
.notNull(),
|
||||||
isAdmin: boolean("isAdmin"),
|
isAdmin: boolean("isAdmin"),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
description: varchar("description")
|
description: varchar("description"),
|
||||||
|
requireDeviceApproval: boolean("requireDeviceApproval").default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const roleActions = pgTable("roleActions", {
|
export const roleActions = pgTable("roleActions", {
|
||||||
@@ -464,13 +467,22 @@ export const resourceHeaderAuth = pgTable("resourceHeaderAuth", {
|
|||||||
headerAuthHash: varchar("headerAuthHash").notNull()
|
headerAuthHash: varchar("headerAuthHash").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resourceHeaderAuthExtendedCompatibility = pgTable("resourceHeaderAuthExtendedCompatibility", {
|
export const resourceHeaderAuthExtendedCompatibility = pgTable(
|
||||||
headerAuthExtendedCompatibilityId: serial("headerAuthExtendedCompatibilityId").primaryKey(),
|
"resourceHeaderAuthExtendedCompatibility",
|
||||||
resourceId: integer("resourceId")
|
{
|
||||||
.notNull()
|
headerAuthExtendedCompatibilityId: serial(
|
||||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
"headerAuthExtendedCompatibilityId"
|
||||||
extendedCompatibilityIsActivated: boolean("extendedCompatibilityIsActivated").notNull().default(true),
|
).primaryKey(),
|
||||||
});
|
resourceId: integer("resourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
|
extendedCompatibilityIsActivated: boolean(
|
||||||
|
"extendedCompatibilityIsActivated"
|
||||||
|
)
|
||||||
|
.notNull()
|
||||||
|
.default(true)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
||||||
accessTokenId: varchar("accessTokenId").primaryKey(),
|
accessTokenId: varchar("accessTokenId").primaryKey(),
|
||||||
@@ -580,7 +592,8 @@ export const idp = pgTable("idp", {
|
|||||||
type: varchar("type").notNull(),
|
type: varchar("type").notNull(),
|
||||||
defaultRoleMapping: varchar("defaultRoleMapping"),
|
defaultRoleMapping: varchar("defaultRoleMapping"),
|
||||||
defaultOrgMapping: varchar("defaultOrgMapping"),
|
defaultOrgMapping: varchar("defaultOrgMapping"),
|
||||||
autoProvision: boolean("autoProvision").notNull().default(false)
|
autoProvision: boolean("autoProvision").notNull().default(false),
|
||||||
|
tags: text("tags")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const idpOidcConfig = pgTable("idpOidcConfig", {
|
export const idpOidcConfig = pgTable("idpOidcConfig", {
|
||||||
@@ -677,7 +690,12 @@ export const clients = pgTable("clients", {
|
|||||||
online: boolean("online").notNull().default(false),
|
online: boolean("online").notNull().default(false),
|
||||||
// endpoint: varchar("endpoint"),
|
// endpoint: varchar("endpoint"),
|
||||||
lastHolePunch: integer("lastHolePunch"),
|
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(
|
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", {
|
export const olms = pgTable("olms", {
|
||||||
olmId: varchar("id").primaryKey(),
|
olmId: varchar("id").primaryKey(),
|
||||||
secretHash: varchar("secretHash").notNull(),
|
secretHash: varchar("secretHash").notNull(),
|
||||||
@@ -715,7 +776,29 @@ export const olms = pgTable("olms", {
|
|||||||
userId: text("userId").references(() => users.userId, {
|
userId: text("userId").references(() => users.userId, {
|
||||||
// optionally tied to a user and in this case delete when the user deletes
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
onDelete: "cascade"
|
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", {
|
export const olmSessions = pgTable("clientSession", {
|
||||||
@@ -872,7 +955,9 @@ export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
|||||||
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||||
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
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 ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
||||||
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
||||||
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import {
|
import { db, loginPage, LoginPage, loginPageOrg, Org, orgs, roles } from "@server/db";
|
||||||
db, loginPage, LoginPage, loginPageOrg, Org, orgs,
|
|
||||||
} from "@server/db";
|
|
||||||
import {
|
import {
|
||||||
Resource,
|
Resource,
|
||||||
ResourcePassword,
|
ResourcePassword,
|
||||||
@@ -27,7 +25,7 @@ export type ResourceWithAuth = {
|
|||||||
pincode: ResourcePincode | null;
|
pincode: ResourcePincode | null;
|
||||||
password: ResourcePassword | null;
|
password: ResourcePassword | null;
|
||||||
headerAuth: ResourceHeaderAuth | null;
|
headerAuth: ResourceHeaderAuth | null;
|
||||||
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null
|
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
||||||
org: Org;
|
org: Org;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,12 +57,12 @@ export async function getResourceByDomain(
|
|||||||
)
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
resourceHeaderAuthExtendedCompatibility,
|
resourceHeaderAuthExtendedCompatibility,
|
||||||
eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId)
|
eq(
|
||||||
)
|
resourceHeaderAuthExtendedCompatibility.resourceId,
|
||||||
.innerJoin(
|
resources.resourceId
|
||||||
orgs,
|
)
|
||||||
eq(orgs.orgId, resources.orgId)
|
|
||||||
)
|
)
|
||||||
|
.innerJoin(orgs, eq(orgs.orgId, resources.orgId))
|
||||||
.where(eq(resources.fullDomain, domain))
|
.where(eq(resources.fullDomain, domain))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
@@ -77,7 +75,8 @@ export async function getResourceByDomain(
|
|||||||
pincode: result.resourcePincode,
|
pincode: result.resourcePincode,
|
||||||
password: result.resourcePassword,
|
password: result.resourcePassword,
|
||||||
headerAuth: result.resourceHeaderAuth,
|
headerAuth: result.resourceHeaderAuth,
|
||||||
headerAuthExtendedCompatibility: result.resourceHeaderAuthExtendedCompatibility,
|
headerAuthExtendedCompatibility:
|
||||||
|
result.resourceHeaderAuthExtendedCompatibility,
|
||||||
org: result.orgs
|
org: result.orgs
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -109,9 +108,17 @@ export async function getUserSessionWithUser(
|
|||||||
*/
|
*/
|
||||||
export async function getUserOrgRole(userId: string, orgId: string) {
|
export async function getUserOrgRole(userId: string, orgId: string) {
|
||||||
const userOrgRole = await db
|
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)
|
.from(userOrgs)
|
||||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
||||||
|
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
return userOrgRole.length > 0 ? userOrgRole[0] : null;
|
return userOrgRole.length > 0 ? userOrgRole[0] : null;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
sqliteTable,
|
sqliteTable,
|
||||||
text
|
text
|
||||||
} from "drizzle-orm/sqlite-core";
|
} 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", {
|
export const certificates = sqliteTable("certificates", {
|
||||||
certId: integer("certId").primaryKey({ autoIncrement: true }),
|
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 Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
export type Certificate = InferSelectModel<typeof certificates>;
|
export type Certificate = InferSelectModel<typeof certificates>;
|
||||||
|
|||||||
@@ -12,22 +12,22 @@ import { no } from "zod/v4/locales";
|
|||||||
export const domains = sqliteTable("domains", {
|
export const domains = sqliteTable("domains", {
|
||||||
domainId: text("domainId").primaryKey(),
|
domainId: text("domainId").primaryKey(),
|
||||||
baseDomain: text("baseDomain").notNull(),
|
baseDomain: text("baseDomain").notNull(),
|
||||||
configManaged: integer("configManaged", {mode: "boolean"})
|
configManaged: integer("configManaged", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
type: text("type"), // "ns", "cname", "wildcard"
|
type: text("type"), // "ns", "cname", "wildcard"
|
||||||
verified: integer("verified", {mode: "boolean"}).notNull().default(false),
|
verified: integer("verified", { mode: "boolean" }).notNull().default(false),
|
||||||
failed: integer("failed", {mode: "boolean"}).notNull().default(false),
|
failed: integer("failed", { mode: "boolean" }).notNull().default(false),
|
||||||
tries: integer("tries").notNull().default(0),
|
tries: integer("tries").notNull().default(0),
|
||||||
certResolver: text("certResolver"),
|
certResolver: text("certResolver"),
|
||||||
preferWildcardCert: integer("preferWildcardCert", {mode: "boolean"})
|
preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const dnsRecords = sqliteTable("dnsRecords", {
|
export const dnsRecords = sqliteTable("dnsRecords", {
|
||||||
id: integer("id").primaryKey({autoIncrement: true}),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
domainId: text("domainId")
|
domainId: text("domainId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => domains.domainId, {onDelete: "cascade"}),
|
.references(() => domains.domainId, { onDelete: "cascade" }),
|
||||||
|
|
||||||
recordType: text("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT"
|
recordType: text("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT"
|
||||||
baseDomain: text("baseDomain"),
|
baseDomain: text("baseDomain"),
|
||||||
@@ -41,7 +41,7 @@ export const orgs = sqliteTable("orgs", {
|
|||||||
subnet: text("subnet"),
|
subnet: text("subnet"),
|
||||||
utilitySubnet: text("utilitySubnet"), // this is the subnet for utility addresses
|
utilitySubnet: text("utilitySubnet"), // this is the subnet for utility addresses
|
||||||
createdAt: text("createdAt"),
|
createdAt: text("createdAt"),
|
||||||
requireTwoFactor: integer("requireTwoFactor", {mode: "boolean"}),
|
requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" }),
|
||||||
maxSessionLengthHours: integer("maxSessionLengthHours"), // hours
|
maxSessionLengthHours: integer("maxSessionLengthHours"), // hours
|
||||||
passwordExpiryDays: integer("passwordExpiryDays"), // days
|
passwordExpiryDays: integer("passwordExpiryDays"), // days
|
||||||
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
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", {
|
export const userDomains = sqliteTable("userDomains", {
|
||||||
userId: text("userId")
|
userId: text("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, {onDelete: "cascade"}),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
domainId: text("domainId")
|
domainId: text("domainId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => domains.domainId, {onDelete: "cascade"})
|
.references(() => domains.domainId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const orgDomains = sqliteTable("orgDomains", {
|
export const orgDomains = sqliteTable("orgDomains", {
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => orgs.orgId, {onDelete: "cascade"}),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
domainId: text("domainId")
|
domainId: text("domainId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => domains.domainId, {onDelete: "cascade"})
|
.references(() => domains.domainId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const sites = sqliteTable("sites", {
|
export const sites = sqliteTable("sites", {
|
||||||
siteId: integer("siteId").primaryKey({autoIncrement: true}),
|
siteId: integer("siteId").primaryKey({ autoIncrement: true }),
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
.references(() => orgs.orgId, {
|
.references(() => orgs.orgId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
@@ -91,7 +91,7 @@ export const sites = sqliteTable("sites", {
|
|||||||
megabytesOut: integer("bytesOut").default(0),
|
megabytesOut: integer("bytesOut").default(0),
|
||||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||||
type: text("type").notNull(), // "newt" or "wireguard"
|
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
|
// 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
|
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
|
publicKey: text("publicKey"), // TODO: Fix typo in publicKey
|
||||||
lastHolePunch: integer("lastHolePunch"),
|
lastHolePunch: integer("lastHolePunch"),
|
||||||
listenPort: integer("listenPort"),
|
listenPort: integer("listenPort"),
|
||||||
dockerSocketEnabled: integer("dockerSocketEnabled", {mode: "boolean"})
|
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(true)
|
.default(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resources = sqliteTable("resources", {
|
export const resources = sqliteTable("resources", {
|
||||||
resourceId: integer("resourceId").primaryKey({autoIncrement: true}),
|
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
|
||||||
resourceGuid: text("resourceGuid", {length: 36})
|
resourceGuid: text("resourceGuid", { length: 36 })
|
||||||
.unique()
|
.unique()
|
||||||
.notNull()
|
.notNull()
|
||||||
.$defaultFn(() => randomUUID()),
|
.$defaultFn(() => randomUUID()),
|
||||||
@@ -122,35 +122,39 @@ export const resources = sqliteTable("resources", {
|
|||||||
domainId: text("domainId").references(() => domains.domainId, {
|
domainId: text("domainId").references(() => domains.domainId, {
|
||||||
onDelete: "set null"
|
onDelete: "set null"
|
||||||
}),
|
}),
|
||||||
ssl: integer("ssl", {mode: "boolean"}).notNull().default(false),
|
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
|
||||||
blockAccess: integer("blockAccess", {mode: "boolean"})
|
blockAccess: integer("blockAccess", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
sso: integer("sso", {mode: "boolean"}).notNull().default(true),
|
sso: integer("sso", { mode: "boolean" }).notNull().default(true),
|
||||||
http: integer("http", {mode: "boolean"}).notNull().default(true),
|
http: integer("http", { mode: "boolean" }).notNull().default(true),
|
||||||
protocol: text("protocol").notNull(),
|
protocol: text("protocol").notNull(),
|
||||||
proxyPort: integer("proxyPort"),
|
proxyPort: integer("proxyPort"),
|
||||||
emailWhitelistEnabled: integer("emailWhitelistEnabled", {mode: "boolean"})
|
emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
applyRules: integer("applyRules", {mode: "boolean"})
|
applyRules: integer("applyRules", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
enabled: integer("enabled", {mode: "boolean"}).notNull().default(true),
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
stickySession: integer("stickySession", {mode: "boolean"})
|
stickySession: integer("stickySession", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
tlsServerName: text("tlsServerName"),
|
tlsServerName: text("tlsServerName"),
|
||||||
setHostHeader: text("setHostHeader"),
|
setHostHeader: text("setHostHeader"),
|
||||||
enableProxy: integer("enableProxy", {mode: "boolean"}).default(true),
|
enableProxy: integer("enableProxy", { mode: "boolean" }).default(true),
|
||||||
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
||||||
onDelete: "set null"
|
onDelete: "set null"
|
||||||
}),
|
}),
|
||||||
headers: text("headers"), // comma-separated list of headers to add to the request
|
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),
|
proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
|
||||||
|
|
||||||
maintenanceModeEnabled: integer("maintenanceModeEnabled", { mode: "boolean" })
|
maintenanceModeEnabled: integer("maintenanceModeEnabled", {
|
||||||
|
mode: "boolean"
|
||||||
|
})
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
maintenanceModeType: text("maintenanceModeType", {
|
maintenanceModeType: text("maintenanceModeType", {
|
||||||
@@ -158,12 +162,11 @@ export const resources = sqliteTable("resources", {
|
|||||||
}).default("forced"), // "forced" = always show, "automatic" = only when down
|
}).default("forced"), // "forced" = always show, "automatic" = only when down
|
||||||
maintenanceTitle: text("maintenanceTitle"),
|
maintenanceTitle: text("maintenanceTitle"),
|
||||||
maintenanceMessage: text("maintenanceMessage"),
|
maintenanceMessage: text("maintenanceMessage"),
|
||||||
maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
|
maintenanceEstimatedTime: text("maintenanceEstimatedTime")
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = sqliteTable("targets", {
|
export const targets = sqliteTable("targets", {
|
||||||
targetId: integer("targetId").primaryKey({autoIncrement: true}),
|
targetId: integer("targetId").primaryKey({ autoIncrement: true }),
|
||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
.references(() => resources.resourceId, {
|
.references(() => resources.resourceId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
@@ -178,7 +181,7 @@ export const targets = sqliteTable("targets", {
|
|||||||
method: text("method"),
|
method: text("method"),
|
||||||
port: integer("port").notNull(),
|
port: integer("port").notNull(),
|
||||||
internalPort: integer("internalPort"),
|
internalPort: integer("internalPort"),
|
||||||
enabled: integer("enabled", {mode: "boolean"}).notNull().default(true),
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
path: text("path"),
|
path: text("path"),
|
||||||
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
||||||
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
|
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")
|
targetId: integer("targetId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => targets.targetId, {onDelete: "cascade"}),
|
.references(() => targets.targetId, { onDelete: "cascade" }),
|
||||||
hcEnabled: integer("hcEnabled", {mode: "boolean"})
|
hcEnabled: integer("hcEnabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
hcPath: text("hcPath"),
|
hcPath: text("hcPath"),
|
||||||
@@ -215,7 +218,7 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const exitNodes = sqliteTable("exitNodes", {
|
export const exitNodes = sqliteTable("exitNodes", {
|
||||||
exitNodeId: integer("exitNodeId").primaryKey({autoIncrement: true}),
|
exitNodeId: integer("exitNodeId").primaryKey({ autoIncrement: true }),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
address: text("address").notNull(), // this is the address of the wireguard interface in gerbil
|
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
|
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(),
|
listenPort: integer("listenPort").notNull(),
|
||||||
reachableAt: text("reachableAt"), // this is the internal address of the gerbil http server for command control
|
reachableAt: text("reachableAt"), // this is the internal address of the gerbil http server for command control
|
||||||
maxConnections: integer("maxConnections"),
|
maxConnections: integer("maxConnections"),
|
||||||
online: integer("online", {mode: "boolean"}).notNull().default(false),
|
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||||
lastPing: integer("lastPing"),
|
lastPing: integer("lastPing"),
|
||||||
type: text("type").default("gerbil"), // gerbil, remoteExitNode
|
type: text("type").default("gerbil"), // gerbil, remoteExitNode
|
||||||
region: text("region")
|
region: text("region")
|
||||||
@@ -236,10 +239,10 @@ export const siteResources = sqliteTable("siteResources", {
|
|||||||
}),
|
}),
|
||||||
siteId: integer("siteId")
|
siteId: integer("siteId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => sites.siteId, {onDelete: "cascade"}),
|
.references(() => sites.siteId, { onDelete: "cascade" }),
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => orgs.orgId, {onDelete: "cascade"}),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
niceId: text("niceId").notNull(),
|
niceId: text("niceId").notNull(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
mode: text("mode").notNull(), // "host" | "cidr" | "port"
|
mode: text("mode").notNull(), // "host" | "cidr" | "port"
|
||||||
@@ -250,9 +253,11 @@ export const siteResources = sqliteTable("siteResources", {
|
|||||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
alias: text("alias"),
|
alias: text("alias"),
|
||||||
aliasAddress: text("aliasAddress"),
|
aliasAddress: text("aliasAddress"),
|
||||||
tcpPortRangeString: text("tcpPortRangeString"),
|
tcpPortRangeString: text("tcpPortRangeString").notNull().default("*"),
|
||||||
udpPortRangeString: text("udpPortRangeString"),
|
udpPortRangeString: text("udpPortRangeString").notNull().default("*"),
|
||||||
disableIcmp: integer("disableIcmp", { mode: "boolean" })
|
disableIcmp: integer("disableIcmp", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clientSiteResources = sqliteTable("clientSiteResources", {
|
export const clientSiteResources = sqliteTable("clientSiteResources", {
|
||||||
@@ -292,20 +297,20 @@ export const users = sqliteTable("user", {
|
|||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
passwordHash: text("passwordHash"),
|
passwordHash: text("passwordHash"),
|
||||||
twoFactorEnabled: integer("twoFactorEnabled", {mode: "boolean"})
|
twoFactorEnabled: integer("twoFactorEnabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
twoFactorSetupRequested: integer("twoFactorSetupRequested", {
|
twoFactorSetupRequested: integer("twoFactorSetupRequested", {
|
||||||
mode: "boolean"
|
mode: "boolean"
|
||||||
}).default(false),
|
}).default(false),
|
||||||
twoFactorSecret: text("twoFactorSecret"),
|
twoFactorSecret: text("twoFactorSecret"),
|
||||||
emailVerified: integer("emailVerified", {mode: "boolean"})
|
emailVerified: integer("emailVerified", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
dateCreated: text("dateCreated").notNull(),
|
dateCreated: text("dateCreated").notNull(),
|
||||||
termsAcceptedTimestamp: text("termsAcceptedTimestamp"),
|
termsAcceptedTimestamp: text("termsAcceptedTimestamp"),
|
||||||
termsVersion: text("termsVersion"),
|
termsVersion: text("termsVersion"),
|
||||||
serverAdmin: integer("serverAdmin", {mode: "boolean"})
|
serverAdmin: integer("serverAdmin", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
lastPasswordChange: integer("lastPasswordChange")
|
lastPasswordChange: integer("lastPasswordChange")
|
||||||
@@ -339,7 +344,7 @@ export const webauthnChallenge = sqliteTable("webauthnChallenge", {
|
|||||||
export const setupTokens = sqliteTable("setupTokens", {
|
export const setupTokens = sqliteTable("setupTokens", {
|
||||||
tokenId: text("tokenId").primaryKey(),
|
tokenId: text("tokenId").primaryKey(),
|
||||||
token: text("token").notNull(),
|
token: text("token").notNull(),
|
||||||
used: integer("used", {mode: "boolean"}).notNull().default(false),
|
used: integer("used", { mode: "boolean" }).notNull().default(false),
|
||||||
dateCreated: text("dateCreated").notNull(),
|
dateCreated: text("dateCreated").notNull(),
|
||||||
dateUsed: text("dateUsed")
|
dateUsed: text("dateUsed")
|
||||||
});
|
});
|
||||||
@@ -378,9 +383,14 @@ export const clients = sqliteTable("clients", {
|
|||||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||||
lastPing: integer("lastPing"),
|
lastPing: integer("lastPing"),
|
||||||
type: text("type").notNull(), // "olm"
|
type: text("type").notNull(), // "olm"
|
||||||
online: integer("online", {mode: "boolean"}).notNull().default(false),
|
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||||
// endpoint: text("endpoint"),
|
// 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(
|
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", {
|
export const olms = sqliteTable("olms", {
|
||||||
olmId: text("id").primaryKey(),
|
olmId: text("id").primaryKey(),
|
||||||
secretHash: text("secretHash").notNull(),
|
secretHash: text("secretHash").notNull(),
|
||||||
@@ -420,14 +493,36 @@ export const olms = sqliteTable("olms", {
|
|||||||
userId: text("userId").references(() => users.userId, {
|
userId: text("userId").references(() => users.userId, {
|
||||||
// optionally tied to a user and in this case delete when the user deletes
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
onDelete: "cascade"
|
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", {
|
export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", {
|
||||||
codeId: integer("id").primaryKey({autoIncrement: true}),
|
codeId: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
userId: text("userId")
|
userId: text("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, {onDelete: "cascade"}),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
codeHash: text("codeHash").notNull()
|
codeHash: text("codeHash").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -435,7 +530,7 @@ export const sessions = sqliteTable("session", {
|
|||||||
sessionId: text("id").primaryKey(),
|
sessionId: text("id").primaryKey(),
|
||||||
userId: text("userId")
|
userId: text("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, {onDelete: "cascade"}),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
expiresAt: integer("expiresAt").notNull(),
|
expiresAt: integer("expiresAt").notNull(),
|
||||||
issuedAt: integer("issuedAt"),
|
issuedAt: integer("issuedAt"),
|
||||||
deviceAuthUsed: integer("deviceAuthUsed", { mode: "boolean" })
|
deviceAuthUsed: integer("deviceAuthUsed", { mode: "boolean" })
|
||||||
@@ -447,7 +542,7 @@ export const newtSessions = sqliteTable("newtSession", {
|
|||||||
sessionId: text("id").primaryKey(),
|
sessionId: text("id").primaryKey(),
|
||||||
newtId: text("newtId")
|
newtId: text("newtId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => newts.newtId, {onDelete: "cascade"}),
|
.references(() => newts.newtId, { onDelete: "cascade" }),
|
||||||
expiresAt: integer("expiresAt").notNull()
|
expiresAt: integer("expiresAt").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -455,14 +550,14 @@ export const olmSessions = sqliteTable("clientSession", {
|
|||||||
sessionId: text("id").primaryKey(),
|
sessionId: text("id").primaryKey(),
|
||||||
olmId: text("olmId")
|
olmId: text("olmId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => olms.olmId, {onDelete: "cascade"}),
|
.references(() => olms.olmId, { onDelete: "cascade" }),
|
||||||
expiresAt: integer("expiresAt").notNull()
|
expiresAt: integer("expiresAt").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userOrgs = sqliteTable("userOrgs", {
|
export const userOrgs = sqliteTable("userOrgs", {
|
||||||
userId: text("userId")
|
userId: text("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, {onDelete: "cascade"}),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
.references(() => orgs.orgId, {
|
.references(() => orgs.orgId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
@@ -471,28 +566,28 @@ export const userOrgs = sqliteTable("userOrgs", {
|
|||||||
roleId: integer("roleId")
|
roleId: integer("roleId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => roles.roleId),
|
.references(() => roles.roleId),
|
||||||
isOwner: integer("isOwner", {mode: "boolean"}).notNull().default(false),
|
isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false),
|
||||||
autoProvisioned: integer("autoProvisioned", {
|
autoProvisioned: integer("autoProvisioned", {
|
||||||
mode: "boolean"
|
mode: "boolean"
|
||||||
}).default(false)
|
}).default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const emailVerificationCodes = sqliteTable("emailVerificationCodes", {
|
export const emailVerificationCodes = sqliteTable("emailVerificationCodes", {
|
||||||
codeId: integer("id").primaryKey({autoIncrement: true}),
|
codeId: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
userId: text("userId")
|
userId: text("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, {onDelete: "cascade"}),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
email: text("email").notNull(),
|
email: text("email").notNull(),
|
||||||
code: text("code").notNull(),
|
code: text("code").notNull(),
|
||||||
expiresAt: integer("expiresAt").notNull()
|
expiresAt: integer("expiresAt").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const passwordResetTokens = sqliteTable("passwordResetTokens", {
|
export const passwordResetTokens = sqliteTable("passwordResetTokens", {
|
||||||
tokenId: integer("id").primaryKey({autoIncrement: true}),
|
tokenId: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
email: text("email").notNull(),
|
email: text("email").notNull(),
|
||||||
userId: text("userId")
|
userId: text("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, {onDelete: "cascade"}),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
tokenHash: text("tokenHash").notNull(),
|
tokenHash: text("tokenHash").notNull(),
|
||||||
expiresAt: integer("expiresAt").notNull()
|
expiresAt: integer("expiresAt").notNull()
|
||||||
});
|
});
|
||||||
@@ -504,106 +599,109 @@ export const actions = sqliteTable("actions", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const roles = sqliteTable("roles", {
|
export const roles = sqliteTable("roles", {
|
||||||
roleId: integer("roleId").primaryKey({autoIncrement: true}),
|
roleId: integer("roleId").primaryKey({ autoIncrement: true }),
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
.references(() => orgs.orgId, {
|
.references(() => orgs.orgId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
isAdmin: integer("isAdmin", {mode: "boolean"}),
|
isAdmin: integer("isAdmin", { mode: "boolean" }),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
description: text("description")
|
description: text("description"),
|
||||||
|
requireDeviceApproval: integer("requireDeviceApproval", {
|
||||||
|
mode: "boolean"
|
||||||
|
}).default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const roleActions = sqliteTable("roleActions", {
|
export const roleActions = sqliteTable("roleActions", {
|
||||||
roleId: integer("roleId")
|
roleId: integer("roleId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => roles.roleId, {onDelete: "cascade"}),
|
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||||
actionId: text("actionId")
|
actionId: text("actionId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => actions.actionId, {onDelete: "cascade"}),
|
.references(() => actions.actionId, { onDelete: "cascade" }),
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => orgs.orgId, {onDelete: "cascade"})
|
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userActions = sqliteTable("userActions", {
|
export const userActions = sqliteTable("userActions", {
|
||||||
userId: text("userId")
|
userId: text("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, {onDelete: "cascade"}),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
actionId: text("actionId")
|
actionId: text("actionId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => actions.actionId, {onDelete: "cascade"}),
|
.references(() => actions.actionId, { onDelete: "cascade" }),
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => orgs.orgId, {onDelete: "cascade"})
|
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const roleSites = sqliteTable("roleSites", {
|
export const roleSites = sqliteTable("roleSites", {
|
||||||
roleId: integer("roleId")
|
roleId: integer("roleId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => roles.roleId, {onDelete: "cascade"}),
|
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||||
siteId: integer("siteId")
|
siteId: integer("siteId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => sites.siteId, {onDelete: "cascade"})
|
.references(() => sites.siteId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userSites = sqliteTable("userSites", {
|
export const userSites = sqliteTable("userSites", {
|
||||||
userId: text("userId")
|
userId: text("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, {onDelete: "cascade"}),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
siteId: integer("siteId")
|
siteId: integer("siteId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => sites.siteId, {onDelete: "cascade"})
|
.references(() => sites.siteId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userClients = sqliteTable("userClients", {
|
export const userClients = sqliteTable("userClients", {
|
||||||
userId: text("userId")
|
userId: text("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, {onDelete: "cascade"}),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
clientId: integer("clientId")
|
clientId: integer("clientId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => clients.clientId, {onDelete: "cascade"})
|
.references(() => clients.clientId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const roleClients = sqliteTable("roleClients", {
|
export const roleClients = sqliteTable("roleClients", {
|
||||||
roleId: integer("roleId")
|
roleId: integer("roleId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => roles.roleId, {onDelete: "cascade"}),
|
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||||
clientId: integer("clientId")
|
clientId: integer("clientId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => clients.clientId, {onDelete: "cascade"})
|
.references(() => clients.clientId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const roleResources = sqliteTable("roleResources", {
|
export const roleResources = sqliteTable("roleResources", {
|
||||||
roleId: integer("roleId")
|
roleId: integer("roleId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => roles.roleId, {onDelete: "cascade"}),
|
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => resources.resourceId, {onDelete: "cascade"})
|
.references(() => resources.resourceId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userResources = sqliteTable("userResources", {
|
export const userResources = sqliteTable("userResources", {
|
||||||
userId: text("userId")
|
userId: text("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, {onDelete: "cascade"}),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => resources.resourceId, {onDelete: "cascade"})
|
.references(() => resources.resourceId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userInvites = sqliteTable("userInvites", {
|
export const userInvites = sqliteTable("userInvites", {
|
||||||
inviteId: text("inviteId").primaryKey(),
|
inviteId: text("inviteId").primaryKey(),
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => orgs.orgId, {onDelete: "cascade"}),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
email: text("email").notNull(),
|
email: text("email").notNull(),
|
||||||
expiresAt: integer("expiresAt").notNull(),
|
expiresAt: integer("expiresAt").notNull(),
|
||||||
tokenHash: text("token").notNull(),
|
tokenHash: text("token").notNull(),
|
||||||
roleId: integer("roleId")
|
roleId: integer("roleId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => roles.roleId, {onDelete: "cascade"})
|
.references(() => roles.roleId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resourcePincode = sqliteTable("resourcePincode", {
|
export const resourcePincode = sqliteTable("resourcePincode", {
|
||||||
@@ -612,7 +710,7 @@ export const resourcePincode = sqliteTable("resourcePincode", {
|
|||||||
}),
|
}),
|
||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => resources.resourceId, {onDelete: "cascade"}),
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
pincodeHash: text("pincodeHash").notNull(),
|
pincodeHash: text("pincodeHash").notNull(),
|
||||||
digitLength: integer("digitLength").notNull()
|
digitLength: integer("digitLength").notNull()
|
||||||
});
|
});
|
||||||
@@ -623,7 +721,7 @@ export const resourcePassword = sqliteTable("resourcePassword", {
|
|||||||
}),
|
}),
|
||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => resources.resourceId, {onDelete: "cascade"}),
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
passwordHash: text("passwordHash").notNull()
|
passwordHash: text("passwordHash").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -633,28 +731,38 @@ export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", {
|
|||||||
}),
|
}),
|
||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => resources.resourceId, {onDelete: "cascade"}),
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
headerAuthHash: text("headerAuthHash").notNull()
|
headerAuthHash: text("headerAuthHash").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resourceHeaderAuthExtendedCompatibility = sqliteTable("resourceHeaderAuthExtendedCompatibility", {
|
export const resourceHeaderAuthExtendedCompatibility = sqliteTable(
|
||||||
headerAuthExtendedCompatibilityId: integer("headerAuthExtendedCompatibilityId").primaryKey({
|
"resourceHeaderAuthExtendedCompatibility",
|
||||||
autoIncrement: true
|
{
|
||||||
}),
|
headerAuthExtendedCompatibilityId: integer(
|
||||||
resourceId: integer("resourceId")
|
"headerAuthExtendedCompatibilityId"
|
||||||
.notNull()
|
).primaryKey({
|
||||||
.references(() => resources.resourceId, {onDelete: "cascade"}),
|
autoIncrement: true
|
||||||
extendedCompatibilityIsActivated: integer("extendedCompatibilityIsActivated", {mode: "boolean"}).notNull().default(true)
|
}),
|
||||||
});
|
resourceId: integer("resourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
|
extendedCompatibilityIsActivated: integer(
|
||||||
|
"extendedCompatibilityIsActivated",
|
||||||
|
{ mode: "boolean" }
|
||||||
|
)
|
||||||
|
.notNull()
|
||||||
|
.default(true)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const resourceAccessToken = sqliteTable("resourceAccessToken", {
|
export const resourceAccessToken = sqliteTable("resourceAccessToken", {
|
||||||
accessTokenId: text("accessTokenId").primaryKey(),
|
accessTokenId: text("accessTokenId").primaryKey(),
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => orgs.orgId, {onDelete: "cascade"}),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => resources.resourceId, {onDelete: "cascade"}),
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
tokenHash: text("tokenHash").notNull(),
|
tokenHash: text("tokenHash").notNull(),
|
||||||
sessionLength: integer("sessionLength").notNull(),
|
sessionLength: integer("sessionLength").notNull(),
|
||||||
expiresAt: integer("expiresAt"),
|
expiresAt: integer("expiresAt"),
|
||||||
@@ -667,13 +775,13 @@ export const resourceSessions = sqliteTable("resourceSessions", {
|
|||||||
sessionId: text("id").primaryKey(),
|
sessionId: text("id").primaryKey(),
|
||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => resources.resourceId, {onDelete: "cascade"}),
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
expiresAt: integer("expiresAt").notNull(),
|
expiresAt: integer("expiresAt").notNull(),
|
||||||
sessionLength: integer("sessionLength").notNull(),
|
sessionLength: integer("sessionLength").notNull(),
|
||||||
doNotExtend: integer("doNotExtend", {mode: "boolean"})
|
doNotExtend: integer("doNotExtend", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
isRequestToken: integer("isRequestToken", {mode: "boolean"}),
|
isRequestToken: integer("isRequestToken", { mode: "boolean" }),
|
||||||
userSessionId: text("userSessionId").references(() => sessions.sessionId, {
|
userSessionId: text("userSessionId").references(() => sessions.sessionId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
@@ -705,11 +813,11 @@ export const resourceSessions = sqliteTable("resourceSessions", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const resourceWhitelist = sqliteTable("resourceWhitelist", {
|
export const resourceWhitelist = sqliteTable("resourceWhitelist", {
|
||||||
whitelistId: integer("id").primaryKey({autoIncrement: true}),
|
whitelistId: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
email: text("email").notNull(),
|
email: text("email").notNull(),
|
||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => resources.resourceId, {onDelete: "cascade"})
|
.references(() => resources.resourceId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resourceOtp = sqliteTable("resourceOtp", {
|
export const resourceOtp = sqliteTable("resourceOtp", {
|
||||||
@@ -718,7 +826,7 @@ export const resourceOtp = sqliteTable("resourceOtp", {
|
|||||||
}),
|
}),
|
||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => resources.resourceId, {onDelete: "cascade"}),
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
email: text("email").notNull(),
|
email: text("email").notNull(),
|
||||||
otpHash: text("otpHash").notNull(),
|
otpHash: text("otpHash").notNull(),
|
||||||
expiresAt: integer("expiresAt").notNull()
|
expiresAt: integer("expiresAt").notNull()
|
||||||
@@ -730,11 +838,11 @@ export const versionMigrations = sqliteTable("versionMigrations", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const resourceRules = sqliteTable("resourceRules", {
|
export const resourceRules = sqliteTable("resourceRules", {
|
||||||
ruleId: integer("ruleId").primaryKey({autoIncrement: true}),
|
ruleId: integer("ruleId").primaryKey({ autoIncrement: true }),
|
||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => resources.resourceId, {onDelete: "cascade"}),
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
enabled: integer("enabled", {mode: "boolean"}).notNull().default(true),
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
priority: integer("priority").notNull(),
|
priority: integer("priority").notNull(),
|
||||||
action: text("action").notNull(), // ACCEPT, DROP, PASS
|
action: text("action").notNull(), // ACCEPT, DROP, PASS
|
||||||
match: text("match").notNull(), // CIDR, PATH, IP
|
match: text("match").notNull(), // CIDR, PATH, IP
|
||||||
@@ -742,17 +850,17 @@ export const resourceRules = sqliteTable("resourceRules", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const supporterKey = sqliteTable("supporterKey", {
|
export const supporterKey = sqliteTable("supporterKey", {
|
||||||
keyId: integer("keyId").primaryKey({autoIncrement: true}),
|
keyId: integer("keyId").primaryKey({ autoIncrement: true }),
|
||||||
key: text("key").notNull(),
|
key: text("key").notNull(),
|
||||||
githubUsername: text("githubUsername").notNull(),
|
githubUsername: text("githubUsername").notNull(),
|
||||||
phrase: text("phrase"),
|
phrase: text("phrase"),
|
||||||
tier: text("tier"),
|
tier: text("tier"),
|
||||||
valid: integer("valid", {mode: "boolean"}).notNull().default(false)
|
valid: integer("valid", { mode: "boolean" }).notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Identity Providers
|
// Identity Providers
|
||||||
export const idp = sqliteTable("idp", {
|
export const idp = sqliteTable("idp", {
|
||||||
idpId: integer("idpId").primaryKey({autoIncrement: true}),
|
idpId: integer("idpId").primaryKey({ autoIncrement: true }),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
type: text("type").notNull(),
|
type: text("type").notNull(),
|
||||||
defaultRoleMapping: text("defaultRoleMapping"),
|
defaultRoleMapping: text("defaultRoleMapping"),
|
||||||
@@ -761,7 +869,8 @@ export const idp = sqliteTable("idp", {
|
|||||||
mode: "boolean"
|
mode: "boolean"
|
||||||
})
|
})
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false)
|
.default(false),
|
||||||
|
tags: text("tags")
|
||||||
});
|
});
|
||||||
|
|
||||||
// Identity Provider OAuth Configuration
|
// Identity Provider OAuth Configuration
|
||||||
@@ -772,7 +881,7 @@ export const idpOidcConfig = sqliteTable("idpOidcConfig", {
|
|||||||
variant: text("variant").notNull().default("oidc"),
|
variant: text("variant").notNull().default("oidc"),
|
||||||
idpId: integer("idpId")
|
idpId: integer("idpId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => idp.idpId, {onDelete: "cascade"}),
|
.references(() => idp.idpId, { onDelete: "cascade" }),
|
||||||
clientId: text("clientId").notNull(),
|
clientId: text("clientId").notNull(),
|
||||||
clientSecret: text("clientSecret").notNull(),
|
clientSecret: text("clientSecret").notNull(),
|
||||||
authUrl: text("authUrl").notNull(),
|
authUrl: text("authUrl").notNull(),
|
||||||
@@ -800,22 +909,22 @@ export const apiKeys = sqliteTable("apiKeys", {
|
|||||||
apiKeyHash: text("apiKeyHash").notNull(),
|
apiKeyHash: text("apiKeyHash").notNull(),
|
||||||
lastChars: text("lastChars").notNull(),
|
lastChars: text("lastChars").notNull(),
|
||||||
createdAt: text("dateCreated").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", {
|
export const apiKeyActions = sqliteTable("apiKeyActions", {
|
||||||
apiKeyId: text("apiKeyId")
|
apiKeyId: text("apiKeyId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => apiKeys.apiKeyId, {onDelete: "cascade"}),
|
.references(() => apiKeys.apiKeyId, { onDelete: "cascade" }),
|
||||||
actionId: text("actionId")
|
actionId: text("actionId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => actions.actionId, {onDelete: "cascade"})
|
.references(() => actions.actionId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiKeyOrg = sqliteTable("apiKeyOrg", {
|
export const apiKeyOrg = sqliteTable("apiKeyOrg", {
|
||||||
apiKeyId: text("apiKeyId")
|
apiKeyId: text("apiKeyId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => apiKeys.apiKeyId, {onDelete: "cascade"}),
|
.references(() => apiKeys.apiKeyId, { onDelete: "cascade" }),
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
.references(() => orgs.orgId, {
|
.references(() => orgs.orgId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
@@ -826,10 +935,10 @@ export const apiKeyOrg = sqliteTable("apiKeyOrg", {
|
|||||||
export const idpOrg = sqliteTable("idpOrg", {
|
export const idpOrg = sqliteTable("idpOrg", {
|
||||||
idpId: integer("idpId")
|
idpId: integer("idpId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => idp.idpId, {onDelete: "cascade"}),
|
.references(() => idp.idpId, { onDelete: "cascade" }),
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => orgs.orgId, {onDelete: "cascade"}),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
roleMapping: text("roleMapping"),
|
roleMapping: text("roleMapping"),
|
||||||
orgMapping: text("orgMapping")
|
orgMapping: text("orgMapping")
|
||||||
});
|
});
|
||||||
@@ -847,19 +956,19 @@ export const blueprints = sqliteTable("blueprints", {
|
|||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
source: text("source").notNull(),
|
source: text("source").notNull(),
|
||||||
createdAt: integer("createdAt").notNull(),
|
createdAt: integer("createdAt").notNull(),
|
||||||
succeeded: integer("succeeded", {mode: "boolean"}).notNull(),
|
succeeded: integer("succeeded", { mode: "boolean" }).notNull(),
|
||||||
contents: text("contents").notNull(),
|
contents: text("contents").notNull(),
|
||||||
message: text("message")
|
message: text("message")
|
||||||
});
|
});
|
||||||
export const requestAuditLog = sqliteTable(
|
export const requestAuditLog = sqliteTable(
|
||||||
"requestAuditLog",
|
"requestAuditLog",
|
||||||
{
|
{
|
||||||
id: integer("id").primaryKey({autoIncrement: true}),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
|
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
|
||||||
orgId: text("orgId").references(() => orgs.orgId, {
|
orgId: text("orgId").references(() => orgs.orgId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
action: integer("action", {mode: "boolean"}).notNull(),
|
action: integer("action", { mode: "boolean" }).notNull(),
|
||||||
reason: integer("reason").notNull(),
|
reason: integer("reason").notNull(),
|
||||||
actorType: text("actorType"),
|
actorType: text("actorType"),
|
||||||
actor: text("actor"),
|
actor: text("actor"),
|
||||||
@@ -876,7 +985,7 @@ export const requestAuditLog = sqliteTable(
|
|||||||
host: text("host"),
|
host: text("host"),
|
||||||
path: text("path"),
|
path: text("path"),
|
||||||
method: text("method"),
|
method: text("method"),
|
||||||
tls: integer("tls", {mode: "boolean"})
|
tls: integer("tls", { mode: "boolean" })
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
index("idx_requestAuditLog_timestamp").on(table.timestamp),
|
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 ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||||
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
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 ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
||||||
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
||||||
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
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 { Config, ConfigSchema } from "./types";
|
||||||
import { ProxyResourcesResults, updateProxyResources } from "./proxyResources";
|
import { ProxyResourcesResults, updateProxyResources } from "./proxyResources";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
@@ -126,7 +136,7 @@ export async function applyBlueprint({
|
|||||||
)
|
)
|
||||||
.then((rows) => rows.map((row) => row.roleId));
|
.then((rows) => rows.map((row) => row.roleId));
|
||||||
|
|
||||||
const existingUserIds= await trx
|
const existingUserIds = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(userSiteResources)
|
.from(userSiteResources)
|
||||||
.where(
|
.where(
|
||||||
@@ -134,7 +144,8 @@ export async function applyBlueprint({
|
|||||||
userSiteResources.siteResourceId,
|
userSiteResources.siteResourceId,
|
||||||
result.oldSiteResource.siteResourceId
|
result.oldSiteResource.siteResourceId
|
||||||
)
|
)
|
||||||
).then((rows) => rows.map((row) => row.userId));
|
)
|
||||||
|
.then((rows) => rows.map((row) => row.userId));
|
||||||
|
|
||||||
const existingClientIds = await trx
|
const existingClientIds = await trx
|
||||||
.select()
|
.select()
|
||||||
@@ -144,13 +155,19 @@ export async function applyBlueprint({
|
|||||||
clientSiteResources.siteResourceId,
|
clientSiteResources.siteResourceId,
|
||||||
result.oldSiteResource.siteResourceId
|
result.oldSiteResource.siteResourceId
|
||||||
)
|
)
|
||||||
).then((rows) => rows.map((row) => row.clientId));
|
)
|
||||||
|
.then((rows) => rows.map((row) => row.clientId));
|
||||||
|
|
||||||
// delete the existing site resource
|
// delete the existing site resource
|
||||||
await trx
|
await trx
|
||||||
.delete(siteResources)
|
.delete(siteResources)
|
||||||
.where(
|
.where(
|
||||||
and(eq(siteResources.siteResourceId, result.oldSiteResource.siteResourceId))
|
and(
|
||||||
|
eq(
|
||||||
|
siteResources.siteResourceId,
|
||||||
|
result.oldSiteResource.siteResourceId
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
await rebuildClientAssociationsFromSiteResource(
|
await rebuildClientAssociationsFromSiteResource(
|
||||||
@@ -161,7 +178,7 @@ export async function applyBlueprint({
|
|||||||
const [insertedSiteResource] = await trx
|
const [insertedSiteResource] = await trx
|
||||||
.insert(siteResources)
|
.insert(siteResources)
|
||||||
.values({
|
.values({
|
||||||
...result.newSiteResource,
|
...result.newSiteResource
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -172,18 +189,20 @@ export async function applyBlueprint({
|
|||||||
|
|
||||||
if (existingRoleIds.length > 0) {
|
if (existingRoleIds.length > 0) {
|
||||||
await trx.insert(roleSiteResources).values(
|
await trx.insert(roleSiteResources).values(
|
||||||
existingRoleIds.map((roleId) => ({
|
existingRoleIds.map((roleId) => ({
|
||||||
roleId,
|
roleId,
|
||||||
siteResourceId: insertedSiteResource!.siteResourceId
|
siteResourceId:
|
||||||
|
insertedSiteResource!.siteResourceId
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingUserIds.length > 0) {
|
if (existingUserIds.length > 0) {
|
||||||
await trx.insert(userSiteResources).values(
|
await trx.insert(userSiteResources).values(
|
||||||
existingUserIds.map((userId) => ({
|
existingUserIds.map((userId) => ({
|
||||||
userId,
|
userId,
|
||||||
siteResourceId: insertedSiteResource!.siteResourceId
|
siteResourceId:
|
||||||
|
insertedSiteResource!.siteResourceId
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -192,7 +211,8 @@ export async function applyBlueprint({
|
|||||||
await trx.insert(clientSiteResources).values(
|
await trx.insert(clientSiteResources).values(
|
||||||
existingClientIds.map((clientId) => ({
|
existingClientIds.map((clientId) => ({
|
||||||
clientId,
|
clientId,
|
||||||
siteResourceId: insertedSiteResource!.siteResourceId
|
siteResourceId:
|
||||||
|
insertedSiteResource!.siteResourceId
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -201,7 +221,6 @@ export async function applyBlueprint({
|
|||||||
insertedSiteResource,
|
insertedSiteResource,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
const [newSite] = await trx
|
const [newSite] = await trx
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import {
|
|||||||
domains,
|
domains,
|
||||||
orgDomains,
|
orgDomains,
|
||||||
Resource,
|
Resource,
|
||||||
resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility,
|
resourceHeaderAuth,
|
||||||
|
resourceHeaderAuthExtendedCompatibility,
|
||||||
resourcePincode,
|
resourcePincode,
|
||||||
resourceRules,
|
resourceRules,
|
||||||
resourceWhitelist,
|
resourceWhitelist,
|
||||||
@@ -16,8 +17,8 @@ import {
|
|||||||
userResources,
|
userResources,
|
||||||
users
|
users
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import {resources, targets, sites} from "@server/db";
|
import { resources, targets, sites } from "@server/db";
|
||||||
import {eq, and, asc, or, ne, count, isNotNull} from "drizzle-orm";
|
import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
Config,
|
Config,
|
||||||
ConfigSchema,
|
ConfigSchema,
|
||||||
@@ -25,12 +26,13 @@ import {
|
|||||||
TargetData
|
TargetData
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import {createCertificate} from "#dynamic/routers/certificates/createCertificate";
|
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||||
import {pickPort} from "@server/routers/target/helpers";
|
import { pickPort } from "@server/routers/target/helpers";
|
||||||
import {resourcePassword} from "@server/db";
|
import { resourcePassword } from "@server/db";
|
||||||
import {hashPassword} from "@server/auth/password";
|
import { hashPassword } from "@server/auth/password";
|
||||||
import {isValidCIDR, isValidIP, isValidUrlGlobPattern} from "../validators";
|
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||||
import {get} from "http";
|
import { isLicensedOrSubscribed } from "../isLicencedOrSubscribed";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
export type ProxyResourcesResults = {
|
export type ProxyResourcesResults = {
|
||||||
proxyResource: Resource;
|
proxyResource: Resource;
|
||||||
@@ -63,7 +65,7 @@ export async function updateProxyResources(
|
|||||||
if (targetSiteId) {
|
if (targetSiteId) {
|
||||||
// Look up site by niceId
|
// Look up site by niceId
|
||||||
[site] = await trx
|
[site] = await trx
|
||||||
.select({siteId: sites.siteId})
|
.select({ siteId: sites.siteId })
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
@@ -75,7 +77,7 @@ export async function updateProxyResources(
|
|||||||
} else if (siteId) {
|
} else if (siteId) {
|
||||||
// Use the provided siteId directly, but verify it belongs to the org
|
// Use the provided siteId directly, but verify it belongs to the org
|
||||||
[site] = await trx
|
[site] = await trx
|
||||||
.select({siteId: sites.siteId})
|
.select({ siteId: sites.siteId })
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(
|
.where(
|
||||||
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
|
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
|
||||||
@@ -93,7 +95,7 @@ export async function updateProxyResources(
|
|||||||
|
|
||||||
let internalPortToCreate;
|
let internalPortToCreate;
|
||||||
if (!targetData["internal-port"]) {
|
if (!targetData["internal-port"]) {
|
||||||
const {internalPort, targetIps} = await pickPort(
|
const { internalPort, targetIps } = await pickPort(
|
||||||
site.siteId!,
|
site.siteId!,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
@@ -209,6 +211,16 @@ export async function updateProxyResources(
|
|||||||
resource = existingResource;
|
resource = existingResource;
|
||||||
} else {
|
} else {
|
||||||
// Update existing resource
|
// 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
|
[resource] = await trx
|
||||||
.update(resources)
|
.update(resources)
|
||||||
.set({
|
.set({
|
||||||
@@ -228,12 +240,19 @@ export async function updateProxyResources(
|
|||||||
tlsServerName: resourceData["tls-server-name"] || null,
|
tlsServerName: resourceData["tls-server-name"] || null,
|
||||||
emailWhitelistEnabled: resourceData.auth?.[
|
emailWhitelistEnabled: resourceData.auth?.[
|
||||||
"whitelist-users"
|
"whitelist-users"
|
||||||
]
|
]
|
||||||
? resourceData.auth["whitelist-users"].length > 0
|
? resourceData.auth["whitelist-users"].length > 0
|
||||||
: false,
|
: false,
|
||||||
headers: headers || null,
|
headers: headers || null,
|
||||||
applyRules:
|
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(
|
.where(
|
||||||
eq(resources.resourceId, existingResource.resourceId)
|
eq(resources.resourceId, existingResource.resourceId)
|
||||||
@@ -303,8 +322,13 @@ export async function updateProxyResources(
|
|||||||
const headerAuthPassword =
|
const headerAuthPassword =
|
||||||
resourceData.auth?.["basic-auth"]?.password;
|
resourceData.auth?.["basic-auth"]?.password;
|
||||||
const headerAuthExtendedCompatibility =
|
const headerAuthExtendedCompatibility =
|
||||||
resourceData.auth?.["basic-auth"]?.extendedCompatibility;
|
resourceData.auth?.["basic-auth"]
|
||||||
if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) {
|
?.extendedCompatibility;
|
||||||
|
if (
|
||||||
|
headerAuthUser &&
|
||||||
|
headerAuthPassword &&
|
||||||
|
headerAuthExtendedCompatibility !== null
|
||||||
|
) {
|
||||||
const headerAuthHash = await hashPassword(
|
const headerAuthHash = await hashPassword(
|
||||||
Buffer.from(
|
Buffer.from(
|
||||||
`${headerAuthUser}:${headerAuthPassword}`
|
`${headerAuthUser}:${headerAuthPassword}`
|
||||||
@@ -315,10 +339,13 @@ export async function updateProxyResources(
|
|||||||
resourceId: existingResource.resourceId,
|
resourceId: existingResource.resourceId,
|
||||||
headerAuthHash
|
headerAuthHash
|
||||||
}),
|
}),
|
||||||
trx.insert(resourceHeaderAuthExtendedCompatibility).values({
|
trx
|
||||||
resourceId: existingResource.resourceId,
|
.insert(resourceHeaderAuthExtendedCompatibility)
|
||||||
extendedCompatibilityIsActivated: headerAuthExtendedCompatibility
|
.values({
|
||||||
})
|
resourceId: existingResource.resourceId,
|
||||||
|
extendedCompatibilityIsActivated:
|
||||||
|
headerAuthExtendedCompatibility
|
||||||
|
})
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,7 +407,7 @@ export async function updateProxyResources(
|
|||||||
if (targetSiteId) {
|
if (targetSiteId) {
|
||||||
// Look up site by niceId
|
// Look up site by niceId
|
||||||
[site] = await trx
|
[site] = await trx
|
||||||
.select({siteId: sites.siteId})
|
.select({ siteId: sites.siteId })
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
@@ -392,7 +419,7 @@ export async function updateProxyResources(
|
|||||||
} else if (siteId) {
|
} else if (siteId) {
|
||||||
// Use the provided siteId directly, but verify it belongs to the org
|
// Use the provided siteId directly, but verify it belongs to the org
|
||||||
[site] = await trx
|
[site] = await trx
|
||||||
.select({siteId: sites.siteId})
|
.select({ siteId: sites.siteId })
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
@@ -437,7 +464,7 @@ export async function updateProxyResources(
|
|||||||
if (checkIfTargetChanged(existingTarget, updatedTarget)) {
|
if (checkIfTargetChanged(existingTarget, updatedTarget)) {
|
||||||
let internalPortToUpdate;
|
let internalPortToUpdate;
|
||||||
if (!targetData["internal-port"]) {
|
if (!targetData["internal-port"]) {
|
||||||
const {internalPort, targetIps} = await pickPort(
|
const { internalPort, targetIps } = await pickPort(
|
||||||
site.siteId!,
|
site.siteId!,
|
||||||
trx
|
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
|
// Create new resource
|
||||||
const [newResource] = await trx
|
const [newResource] = await trx
|
||||||
.insert(resources)
|
.insert(resources)
|
||||||
@@ -643,7 +679,13 @@ export async function updateProxyResources(
|
|||||||
ssl: resourceSsl,
|
ssl: resourceSsl,
|
||||||
headers: headers || null,
|
headers: headers || null,
|
||||||
applyRules:
|
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();
|
.returning();
|
||||||
|
|
||||||
@@ -674,9 +716,14 @@ export async function updateProxyResources(
|
|||||||
const headerAuthUser = resourceData.auth?.["basic-auth"]?.user;
|
const headerAuthUser = resourceData.auth?.["basic-auth"]?.user;
|
||||||
const headerAuthPassword =
|
const headerAuthPassword =
|
||||||
resourceData.auth?.["basic-auth"]?.password;
|
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(
|
const headerAuthHash = await hashPassword(
|
||||||
Buffer.from(
|
Buffer.from(
|
||||||
`${headerAuthUser}:${headerAuthPassword}`
|
`${headerAuthUser}:${headerAuthPassword}`
|
||||||
@@ -688,10 +735,13 @@ export async function updateProxyResources(
|
|||||||
resourceId: newResource.resourceId,
|
resourceId: newResource.resourceId,
|
||||||
headerAuthHash
|
headerAuthHash
|
||||||
}),
|
}),
|
||||||
trx.insert(resourceHeaderAuthExtendedCompatibility).values({
|
trx
|
||||||
resourceId: newResource.resourceId,
|
.insert(resourceHeaderAuthExtendedCompatibility)
|
||||||
extendedCompatibilityIsActivated: headerAuthExtendedCompatibility
|
.values({
|
||||||
}),
|
resourceId: newResource.resourceId,
|
||||||
|
extendedCompatibilityIsActivated:
|
||||||
|
headerAuthExtendedCompatibility
|
||||||
|
})
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1043,7 +1093,7 @@ async function getDomain(
|
|||||||
trx: Transaction
|
trx: Transaction
|
||||||
) {
|
) {
|
||||||
const [fullDomainExists] = await trx
|
const [fullDomainExists] = await trx
|
||||||
.select({resourceId: resources.resourceId})
|
.select({ resourceId: resources.resourceId })
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { portRangeStringSchema } from "@server/lib/ip";
|
import { portRangeStringSchema } from "@server/lib/ip";
|
||||||
|
import { MaintenanceSchema } from "#dynamic/lib/blueprints/MaintenanceSchema";
|
||||||
|
|
||||||
export const SiteSchema = z.object({
|
export const SiteSchema = z.object({
|
||||||
name: z.string().min(1).max(100),
|
name: z.string().min(1).max(100),
|
||||||
@@ -53,11 +54,13 @@ export const AuthSchema = z.object({
|
|||||||
// pincode has to have 6 digits
|
// pincode has to have 6 digits
|
||||||
pincode: z.number().min(100000).max(999999).optional(),
|
pincode: z.number().min(100000).max(999999).optional(),
|
||||||
password: z.string().min(1).optional(),
|
password: z.string().min(1).optional(),
|
||||||
"basic-auth": z.object({
|
"basic-auth": z
|
||||||
user: z.string().min(1),
|
.object({
|
||||||
password: z.string().min(1),
|
user: z.string().min(1),
|
||||||
extendedCompatibility: z.boolean().default(true)
|
password: z.string().min(1),
|
||||||
}).optional(),
|
extendedCompatibility: z.boolean().default(true)
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
"sso-enabled": z.boolean().optional().default(false),
|
"sso-enabled": z.boolean().optional().default(false),
|
||||||
"sso-roles": z
|
"sso-roles": z
|
||||||
.array(z.string())
|
.array(z.string())
|
||||||
@@ -108,32 +111,30 @@ export const RuleSchema = z
|
|||||||
.refine(
|
.refine(
|
||||||
(rule) => {
|
(rule) => {
|
||||||
if (rule.match === "country") {
|
if (rule.match === "country") {
|
||||||
// Check if it's a valid 2-letter country code
|
// Check if it's a valid 2-letter country code or "ALL"
|
||||||
return /^[A-Z]{2}$/.test(rule.value);
|
return /^[A-Z]{2}$/.test(rule.value) || rule.value === "ALL";
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ["value"],
|
path: ["value"],
|
||||||
message:
|
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(
|
.refine(
|
||||||
(rule) => {
|
(rule) => {
|
||||||
if (rule.match === "asn") {
|
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 asNumberPattern = /^AS\d+$/i;
|
||||||
const isASFormat = asNumberPattern.test(rule.value);
|
return asNumberPattern.test(rule.value) || rule.value === "ALL";
|
||||||
const isNumeric = /^\d+$/.test(rule.value);
|
|
||||||
return isASFormat || isNumeric;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ["value"],
|
path: ["value"],
|
||||||
message:
|
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(),
|
"host-header": z.string().optional(),
|
||||||
"tls-server-name": z.string().optional(),
|
"tls-server-name": z.string().optional(),
|
||||||
headers: z.array(HeaderSchema).optional(),
|
headers: z.array(HeaderSchema).optional(),
|
||||||
rules: z.array(RuleSchema).optional()
|
rules: z.array(RuleSchema).optional(),
|
||||||
|
maintenance: MaintenanceSchema.optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(resource) => {
|
(resource) => {
|
||||||
@@ -288,8 +290,8 @@ export const ClientResourceSchema = z
|
|||||||
alias: z
|
alias: z
|
||||||
.string()
|
.string()
|
||||||
.regex(
|
.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])?$/,
|
/^(?:[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)"
|
"Alias must be a fully qualified domain name with optional wildcards (e.g., example.com, *.example.com, host-0?.example.internal)"
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
roles: z
|
roles: z
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
|
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
||||||
|
import { build } from "@server/build";
|
||||||
import {
|
import {
|
||||||
|
approvals,
|
||||||
clients,
|
clients,
|
||||||
db,
|
db,
|
||||||
olms,
|
olms,
|
||||||
orgs,
|
orgs,
|
||||||
roleClients,
|
roleClients,
|
||||||
roles,
|
roles,
|
||||||
|
Transaction,
|
||||||
userClients,
|
userClients,
|
||||||
userOrgs,
|
userOrgs
|
||||||
Transaction
|
|
||||||
} from "@server/db";
|
} 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 { 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(
|
export async function calculateUserClientsForOrgs(
|
||||||
userId: string,
|
userId: string,
|
||||||
@@ -38,13 +41,15 @@ export async function calculateUserClientsForOrgs(
|
|||||||
const allUserOrgs = await transaction
|
const allUserOrgs = await transaction
|
||||||
.select()
|
.select()
|
||||||
.from(userOrgs)
|
.from(userOrgs)
|
||||||
|
.innerJoin(roles, eq(roles.roleId, userOrgs.roleId))
|
||||||
.where(eq(userOrgs.userId, userId));
|
.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 each OLM, ensure there's a client in each org the user is in
|
||||||
for (const olm of userOlms) {
|
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 orgId = userOrg.orgId;
|
||||||
|
|
||||||
const [org] = await transaction
|
const [org] = await transaction
|
||||||
@@ -182,21 +187,46 @@ export async function calculateUserClientsForOrgs(
|
|||||||
|
|
||||||
const niceId = await getUniqueClientName(orgId);
|
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
|
// Create the client
|
||||||
const [newClient] = await transaction
|
const [newClient] = await transaction
|
||||||
.insert(clients)
|
.insert(clients)
|
||||||
.values({
|
.values(newClientData)
|
||||||
userId,
|
|
||||||
orgId: userOrg.orgId,
|
|
||||||
exitNodeId: randomExitNode.exitNodeId,
|
|
||||||
name: olm.name || "User Client",
|
|
||||||
subnet: updatedSubnet,
|
|
||||||
olmId: olm.olmId,
|
|
||||||
type: "olm",
|
|
||||||
niceId
|
|
||||||
})
|
|
||||||
.returning();
|
.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(
|
await rebuildClientAssociationsFromClient(
|
||||||
newClient,
|
newClient,
|
||||||
transaction
|
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) {
|
for (const org of orgsToClean) {
|
||||||
const {
|
const {
|
||||||
orgId,
|
orgId,
|
||||||
|
|||||||
@@ -84,6 +84,10 @@ export class Config {
|
|||||||
?.disable_basic_wireguard_sites
|
?.disable_basic_wireguard_sites
|
||||||
? "true"
|
? "true"
|
||||||
: "false";
|
: "false";
|
||||||
|
process.env.FLAGS_DISABLE_PRODUCT_HELP_BANNERS = parsedConfig.flags
|
||||||
|
?.disable_product_help_banners
|
||||||
|
? "true"
|
||||||
|
: "false";
|
||||||
|
|
||||||
process.env.PRODUCT_UPDATES_NOTIFICATION_ENABLED = parsedConfig.app
|
process.env.PRODUCT_UPDATES_NOTIFICATION_ENABLED = parsedConfig.app
|
||||||
.notifications.product_updates
|
.notifications.product_updates
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { and, eq, isNotNull } from "drizzle-orm";
|
|||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
import semver from "semver";
|
||||||
|
|
||||||
interface IPRange {
|
interface IPRange {
|
||||||
start: bigint;
|
start: bigint;
|
||||||
@@ -318,10 +319,7 @@ export function doCidrsOverlap(cidr1: string, cidr2: string): boolean {
|
|||||||
const range2 = cidrToRange(cidr2);
|
const range2 = cidrToRange(cidr2);
|
||||||
|
|
||||||
// Overlap if the ranges intersect
|
// Overlap if the ranges intersect
|
||||||
return (
|
return range1.start <= range2.end && range2.start <= range1.end;
|
||||||
range1.start <= range2.end &&
|
|
||||||
range2.start <= range1.end
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNextAvailableClientSubnet(
|
export async function getNextAvailableClientSubnet(
|
||||||
@@ -686,3 +684,35 @@ export function parsePortRangeString(
|
|||||||
|
|
||||||
return result;
|
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"]),
|
.default(["newt", "wireguard", "local"]),
|
||||||
allow_raw_resources: z.boolean().optional().default(true),
|
allow_raw_resources: z.boolean().optional().default(true),
|
||||||
file_mode: z.boolean().optional().default(false),
|
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()
|
.optional()
|
||||||
.prefault({}),
|
.prefault({}),
|
||||||
@@ -327,7 +330,8 @@ export const configSchema = z
|
|||||||
enable_integration_api: z.boolean().optional(),
|
enable_integration_api: z.boolean().optional(),
|
||||||
disable_local_sites: z.boolean().optional(),
|
disable_local_sites: z.boolean().optional(),
|
||||||
disable_basic_wireguard_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(),
|
.optional(),
|
||||||
dns: z
|
dns: z
|
||||||
|
|||||||
@@ -41,9 +41,10 @@ type TargetWithSite = Target & {
|
|||||||
export async function getTraefikConfig(
|
export async function getTraefikConfig(
|
||||||
exitNodeId: number,
|
exitNodeId: number,
|
||||||
siteTypes: string[],
|
siteTypes: string[],
|
||||||
filterOutNamespaceDomains = false,
|
filterOutNamespaceDomains = false, // UNUSED BUT USED IN PRIVATE
|
||||||
generateLoginPageRouters = false,
|
generateLoginPageRouters = false, // UNUSED BUT USED IN PRIVATE
|
||||||
allowRawResources = true
|
allowRawResources = true,
|
||||||
|
allowMaintenancePage = true, // UNUSED BUT USED IN PRIVATE
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
// Get resources with their targets and sites in a single optimized query
|
// 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
|
// Start from sites on this exit node, then join to targets and resources
|
||||||
@@ -294,12 +295,12 @@ export async function getTraefikConfig(
|
|||||||
certResolver: resolverName,
|
certResolver: resolverName,
|
||||||
...(preferWildcard
|
...(preferWildcard
|
||||||
? {
|
? {
|
||||||
domains: [
|
domains: [
|
||||||
{
|
{
|
||||||
main: wildCard
|
main: wildCard
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -475,9 +476,9 @@ export async function getTraefikConfig(
|
|||||||
// RECEIVE BANDWIDTH ENDPOINT.
|
// RECEIVE BANDWIDTH ENDPOINT.
|
||||||
|
|
||||||
// TODO: HOW TO HANDLE ^^^^^^ BETTER
|
// TODO: HOW TO HANDLE ^^^^^^ BETTER
|
||||||
const anySitesOnline = (
|
const anySitesOnline = targets.some(
|
||||||
targets
|
(target) => target.site.online
|
||||||
).some((target) => target.site.online);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
targets
|
targets
|
||||||
@@ -544,14 +545,14 @@ export async function getTraefikConfig(
|
|||||||
})(),
|
})(),
|
||||||
...(resource.stickySession
|
...(resource.stickySession
|
||||||
? {
|
? {
|
||||||
sticky: {
|
sticky: {
|
||||||
cookie: {
|
cookie: {
|
||||||
name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
|
name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
|
||||||
secure: resource.ssl,
|
secure: resource.ssl,
|
||||||
httpOnly: true
|
httpOnly: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -603,9 +604,9 @@ export async function getTraefikConfig(
|
|||||||
loadBalancer: {
|
loadBalancer: {
|
||||||
servers: (() => {
|
servers: (() => {
|
||||||
// Check if any sites are online
|
// Check if any sites are online
|
||||||
const anySitesOnline = (
|
const anySitesOnline = targets.some(
|
||||||
targets
|
(target) => target.site.online
|
||||||
).some((target) => target.site.online);
|
);
|
||||||
|
|
||||||
return targets
|
return targets
|
||||||
.filter((target) => {
|
.filter((target) => {
|
||||||
@@ -654,18 +655,18 @@ export async function getTraefikConfig(
|
|||||||
})(),
|
})(),
|
||||||
...(resource.proxyProtocol && protocol == "tcp"
|
...(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
|
...(resource.stickySession
|
||||||
? {
|
? {
|
||||||
sticky: {
|
sticky: {
|
||||||
ipStrategy: {
|
ipStrategy: {
|
||||||
depth: 0,
|
depth: 0,
|
||||||
sourcePort: true
|
sourcePort: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ export * from "./verifyApiKeyIsRoot";
|
|||||||
export * from "./verifyApiKeyApiKeyAccess";
|
export * from "./verifyApiKeyApiKeyAccess";
|
||||||
export * from "./verifyApiKeyClientAccess";
|
export * from "./verifyApiKeyClientAccess";
|
||||||
export * from "./verifyApiKeySiteResourceAccess";
|
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";
|
} from "@server/lib/checkOrgAccessPolicy";
|
||||||
import { UserType } from "@server/types/UserTypes";
|
import { UserType } from "@server/types/UserTypes";
|
||||||
|
|
||||||
export async function enforceResourceSessionLength(
|
export function enforceResourceSessionLength(
|
||||||
resourceSession: ResourceSession,
|
resourceSession: ResourceSession,
|
||||||
org: Org
|
org: Org
|
||||||
): Promise<{ valid: boolean; error?: string }> {
|
): { valid: boolean; error?: string } {
|
||||||
if (org.maxSessionLengthHours) {
|
if (org.maxSessionLengthHours) {
|
||||||
const sessionIssuedAt = resourceSession.issuedAt; // may be null
|
const sessionIssuedAt = resourceSession.issuedAt; // may be null
|
||||||
const maxSessionLengthHours = org.maxSessionLengthHours;
|
const maxSessionLengthHours = org.maxSessionLengthHours;
|
||||||
|
|||||||
@@ -139,6 +139,10 @@ export class PrivateConfig {
|
|||||||
process.env.USE_PANGOLIN_DNS =
|
process.env.USE_PANGOLIN_DNS =
|
||||||
this.rawPrivateConfig.flags.use_pangolin_dns.toString();
|
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() {
|
public getRawPrivateConfig() {
|
||||||
|
|||||||
@@ -50,10 +50,14 @@ export async function sendToExitNode(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendToClient(remoteExitNode.remoteExitNodeId, {
|
return sendToClient(
|
||||||
type: request.remoteType,
|
remoteExitNode.remoteExitNodeId,
|
||||||
data: request.data
|
{
|
||||||
});
|
type: request.remoteType,
|
||||||
|
data: request.data
|
||||||
|
},
|
||||||
|
{ incrementConfigVersion: true }
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
let hostname = exitNode.reachableAt;
|
let hostname = exitNode.reachableAt;
|
||||||
|
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ export function selectBestExitNode(
|
|||||||
const validNodes = pingResults.filter((n) => !n.error && n.weight > 0);
|
const validNodes = pingResults.filter((n) => !n.error && n.weight > 0);
|
||||||
|
|
||||||
if (validNodes.length === 0) {
|
if (validNodes.length === 0) {
|
||||||
logger.error("No valid exit nodes available");
|
logger.debug("No valid exit nodes available");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ export class LockManager {
|
|||||||
*/
|
*/
|
||||||
async acquireLock(
|
async acquireLock(
|
||||||
lockKey: string,
|
lockKey: string,
|
||||||
ttlMs: number = 30000
|
ttlMs: number = 30000,
|
||||||
|
maxRetries: number = 3,
|
||||||
|
retryDelayMs: number = 100
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!redis || !redis.status || redis.status !== "ready") {
|
if (!redis || !redis.status || redis.status !== "ready") {
|
||||||
return true;
|
return true;
|
||||||
@@ -35,49 +37,67 @@ export class LockManager {
|
|||||||
}:${Date.now()}`;
|
}:${Date.now()}`;
|
||||||
const redisKey = `lock:${lockKey}`;
|
const redisKey = `lock:${lockKey}`;
|
||||||
|
|
||||||
try {
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
// Use SET with NX (only set if not exists) and PX (expire in milliseconds)
|
try {
|
||||||
// This is atomic and handles both setting and expiration
|
// Use SET with NX (only set if not exists) and PX (expire in milliseconds)
|
||||||
const result = await redis.set(
|
// This is atomic and handles both setting and expiration
|
||||||
redisKey,
|
const result = await redis.set(
|
||||||
lockValue,
|
redisKey,
|
||||||
"PX",
|
lockValue,
|
||||||
ttlMs,
|
"PX",
|
||||||
"NX"
|
ttlMs,
|
||||||
);
|
"NX"
|
||||||
|
|
||||||
if (result === "OK") {
|
|
||||||
logger.debug(
|
|
||||||
`Lock acquired: ${lockKey} by ${
|
|
||||||
config.getRawConfig().gerbil.exit_node_name
|
|
||||||
}`
|
|
||||||
);
|
);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the existing lock is from this worker (reentrant behavior)
|
if (result === "OK") {
|
||||||
const existingValue = await redis.get(redisKey);
|
logger.debug(
|
||||||
if (
|
`Lock acquired: ${lockKey} by ${
|
||||||
existingValue &&
|
config.getRawConfig().gerbil.exit_node_name
|
||||||
existingValue.startsWith(
|
}`
|
||||||
`${config.getRawConfig().gerbil.exit_node_name}:`
|
);
|
||||||
)
|
return true;
|
||||||
) {
|
}
|
||||||
// Extend the lock TTL since it's the same worker
|
|
||||||
await redis.pexpire(redisKey, ttlMs);
|
|
||||||
logger.debug(
|
|
||||||
`Lock extended: ${lockKey} by ${
|
|
||||||
config.getRawConfig().gerbil.exit_node_name
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
// Check if the existing lock is from this worker (reentrant behavior)
|
||||||
} catch (error) {
|
const existingValue = await redis.get(redisKey);
|
||||||
logger.error(`Failed to acquire lock ${lockKey}:`, error);
|
if (
|
||||||
return false;
|
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 { and, eq, lt } from "drizzle-orm";
|
||||||
import cache from "@server/lib/cache";
|
import cache from "@server/lib/cache";
|
||||||
import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs";
|
import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs";
|
||||||
|
import { stripPortFromHost } from "@server/lib/ip";
|
||||||
|
|
||||||
async function getAccessDays(orgId: string): Promise<number> {
|
async function getAccessDays(orgId: string): Promise<number> {
|
||||||
// check cache first
|
// check cache first
|
||||||
@@ -116,19 +117,7 @@ export async function logAccessAudit(data: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const clientIp = data.requestIp
|
const clientIp = data.requestIp
|
||||||
? (() => {
|
? stripPortFromHost(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;
|
|
||||||
})()
|
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const countryCode = data.requestIp
|
const countryCode = data.requestIp
|
||||||
|
|||||||
@@ -83,7 +83,8 @@ export const privateConfigSchema = z.object({
|
|||||||
flags: z
|
flags: z
|
||||||
.object({
|
.object({
|
||||||
enable_redis: z.boolean().optional().default(false),
|
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()
|
.optional()
|
||||||
.prefault({}),
|
.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> {
|
public async sadd(key: string, member: string): Promise<boolean> {
|
||||||
if (!this.isRedisEnabled() || !this.writeClient) return false;
|
if (!this.isRedisEnabled() || !this.writeClient) return false;
|
||||||
|
|
||||||
|
|||||||
@@ -71,9 +71,9 @@ export async function getTraefikConfig(
|
|||||||
siteTypes: string[],
|
siteTypes: string[],
|
||||||
filterOutNamespaceDomains = false,
|
filterOutNamespaceDomains = false,
|
||||||
generateLoginPageRouters = false,
|
generateLoginPageRouters = false,
|
||||||
allowRawResources = true
|
allowRawResources = true,
|
||||||
|
allowMaintenancePage = true
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
|
||||||
// Get resources with their targets and sites in a single optimized query
|
// 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
|
// Start from sites on this exit node, then join to targets and resources
|
||||||
const resourcesWithTargetsAndSites = await db
|
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 = {};
|
let tls = {};
|
||||||
if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
|
if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
|
||||||
const domainParts = fullDomain.split(".");
|
const domainParts = fullDomain.split(".");
|
||||||
@@ -435,17 +423,27 @@ export async function getTraefikConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableServers = targets.filter(
|
if (resource.ssl) {
|
||||||
(target) => {
|
config_output.http.routers![routerName + "-redirect"] = {
|
||||||
if (!target.enabled) return false;
|
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;
|
const hasHealthyServers = availableServers.length > 0;
|
||||||
|
|
||||||
@@ -458,15 +456,15 @@ export async function getTraefikConfig(
|
|||||||
// );
|
// );
|
||||||
} else if (resource.maintenanceModeType === "automatic") {
|
} else if (resource.maintenanceModeType === "automatic") {
|
||||||
showMaintenancePage = !hasHealthyServers;
|
showMaintenancePage = !hasHealthyServers;
|
||||||
if (showMaintenancePage) {
|
// if (showMaintenancePage) {
|
||||||
logger.warn(
|
// logger.warn(
|
||||||
`Resource ${resource.name} (${fullDomain}) has no healthy servers - showing maintenance page (AUTOMATIC mode)`
|
// `Resource ${resource.name} (${fullDomain}) has no healthy servers - showing maintenance page (AUTOMATIC mode)`
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showMaintenancePage) {
|
if (showMaintenancePage && allowMaintenancePage) {
|
||||||
const maintenanceServiceName = `${key}-maintenance-service`;
|
const maintenanceServiceName = `${key}-maintenance-service`;
|
||||||
const maintenanceRouterName = `${key}-maintenance-router`;
|
const maintenanceRouterName = `${key}-maintenance-router`;
|
||||||
const rewriteMiddlewareName = `${key}-maintenance-rewrite`;
|
const rewriteMiddlewareName = `${key}-maintenance-rewrite`;
|
||||||
@@ -794,9 +792,9 @@ export async function getTraefikConfig(
|
|||||||
loadBalancer: {
|
loadBalancer: {
|
||||||
servers: (() => {
|
servers: (() => {
|
||||||
// Check if any sites are online
|
// Check if any sites are online
|
||||||
const anySitesOnline = (
|
const anySitesOnline = targets.some(
|
||||||
targets
|
(target) => target.site.online
|
||||||
).some((target) => target.site.online);
|
);
|
||||||
|
|
||||||
return targets
|
return targets
|
||||||
.filter((target) => {
|
.filter((target) => {
|
||||||
|
|||||||
@@ -27,7 +27,18 @@ export async function verifyValidSubscription(
|
|||||||
return next();
|
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) {
|
if (!tier.active) {
|
||||||
return next(
|
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 logs from "#private/routers/auditLogs";
|
||||||
import * as misc from "#private/routers/misc";
|
import * as misc from "#private/routers/misc";
|
||||||
import * as reKey from "#private/routers/re-key";
|
import * as reKey from "#private/routers/re-key";
|
||||||
|
import * as approval from "#private/routers/approvals";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
@@ -311,6 +312,24 @@ authenticated.get(
|
|||||||
loginPage.getLoginPage
|
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(
|
authenticated.get(
|
||||||
"/org/:orgId/login-page-branding",
|
"/org/:orgId/login-page-branding",
|
||||||
verifyValidLicense,
|
verifyValidLicense,
|
||||||
@@ -436,18 +455,18 @@ authenticated.get(
|
|||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/re-key/:clientId/regenerate-client-secret",
|
"/re-key/:clientId/regenerate-client-secret",
|
||||||
|
verifyClientAccess, // this is first to set the org id
|
||||||
verifyValidLicense,
|
verifyValidLicense,
|
||||||
verifyValidSubscription,
|
verifyValidSubscription,
|
||||||
verifyClientAccess,
|
|
||||||
verifyUserHasAction(ActionsEnum.reGenerateSecret),
|
verifyUserHasAction(ActionsEnum.reGenerateSecret),
|
||||||
reKey.reGenerateClientSecret
|
reKey.reGenerateClientSecret
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/re-key/:siteId/regenerate-site-secret",
|
"/re-key/:siteId/regenerate-site-secret",
|
||||||
|
verifySiteAccess, // this is first to set the org id
|
||||||
verifyValidLicense,
|
verifyValidLicense,
|
||||||
verifyValidSubscription,
|
verifyValidSubscription,
|
||||||
verifySiteAccess,
|
|
||||||
verifyUserHasAction(ActionsEnum.reGenerateSecret),
|
verifyUserHasAction(ActionsEnum.reGenerateSecret),
|
||||||
reKey.reGenerateSiteSecret
|
reKey.reGenerateSiteSecret
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
ResourceHeaderAuthExtendedCompatibility,
|
ResourceHeaderAuthExtendedCompatibility,
|
||||||
orgs,
|
orgs,
|
||||||
requestAuditLog,
|
requestAuditLog,
|
||||||
|
Org
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import {
|
import {
|
||||||
resources,
|
resources,
|
||||||
@@ -79,6 +80,7 @@ import { maxmindLookup } from "@server/db/maxmind";
|
|||||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { maxmindAsnLookup } from "@server/db/maxmindAsn";
|
import { maxmindAsnLookup } from "@server/db/maxmindAsn";
|
||||||
|
import { checkOrgAccessPolicy } from "@server/lib/checkOrgAccessPolicy";
|
||||||
|
|
||||||
// Zod schemas for request validation
|
// Zod schemas for request validation
|
||||||
const getResourceByDomainParamsSchema = z.strictObject({
|
const getResourceByDomainParamsSchema = z.strictObject({
|
||||||
@@ -94,6 +96,12 @@ const getUserOrgRoleParamsSchema = z.strictObject({
|
|||||||
orgId: z.string().min(1, "Organization ID is required")
|
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({
|
const getRoleResourceAccessParamsSchema = z.strictObject({
|
||||||
roleId: z
|
roleId: z
|
||||||
.string()
|
.string()
|
||||||
@@ -178,6 +186,7 @@ export type ResourceWithAuth = {
|
|||||||
password: ResourcePassword | null;
|
password: ResourcePassword | null;
|
||||||
headerAuth: ResourceHeaderAuth | null;
|
headerAuth: ResourceHeaderAuth | null;
|
||||||
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
||||||
|
org: Org
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserSessionWithUser = {
|
export type UserSessionWithUser = {
|
||||||
@@ -238,7 +247,8 @@ hybridRouter.get(
|
|||||||
["newt", "local", "wireguard"], // Allow them to use all the site types
|
["newt", "local", "wireguard"], // Allow them to use all the site types
|
||||||
true, // But don't allow domain namespace resources
|
true, // But don't allow domain namespace resources
|
||||||
false, // Dont include login pages,
|
false, // Dont include login pages,
|
||||||
true // allow raw resources
|
true, // allow raw resources
|
||||||
|
false // dont generate maintenance page
|
||||||
);
|
);
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
@@ -503,8 +513,12 @@ hybridRouter.get(
|
|||||||
)
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
resourceHeaderAuthExtendedCompatibility,
|
resourceHeaderAuthExtendedCompatibility,
|
||||||
eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId)
|
eq(
|
||||||
|
resourceHeaderAuthExtendedCompatibility.resourceId,
|
||||||
|
resources.resourceId
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
.innerJoin(orgs, eq(orgs.orgId, resources.orgId))
|
||||||
.where(eq(resources.fullDomain, domain))
|
.where(eq(resources.fullDomain, domain))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
@@ -538,7 +552,9 @@ hybridRouter.get(
|
|||||||
pincode: result.resourcePincode,
|
pincode: result.resourcePincode,
|
||||||
password: result.resourcePassword,
|
password: result.resourcePassword,
|
||||||
headerAuth: result.resourceHeaderAuth,
|
headerAuth: result.resourceHeaderAuth,
|
||||||
headerAuthExtendedCompatibility: result.resourceHeaderAuthExtendedCompatibility
|
headerAuthExtendedCompatibility:
|
||||||
|
result.resourceHeaderAuthExtendedCompatibility,
|
||||||
|
org: result.orgs
|
||||||
};
|
};
|
||||||
|
|
||||||
return response<ResourceWithAuth>(res, {
|
return response<ResourceWithAuth>(res, {
|
||||||
@@ -602,6 +618,16 @@ hybridRouter.get(
|
|||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return response<LoginPage | null>(res, {
|
||||||
|
data: null,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Login page not found",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
await checkExitNodeOrg(
|
await checkExitNodeOrg(
|
||||||
remoteExitNode.exitNodeId,
|
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, {
|
return response<LoginPage>(res, {
|
||||||
data: result.loginPage,
|
data: result.loginPage,
|
||||||
success: true,
|
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
|
// Check if role has access to resource
|
||||||
hybridRouter.get(
|
hybridRouter.get(
|
||||||
"/role/:roleId/resource/:resourceId/access",
|
"/role/:roleId/resource/:resourceId/access",
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import * as logs from "#private/routers/auditLogs";
|
|||||||
import {
|
import {
|
||||||
verifyApiKeyHasAction,
|
verifyApiKeyHasAction,
|
||||||
verifyApiKeyIsRoot,
|
verifyApiKeyIsRoot,
|
||||||
verifyApiKeyOrgAccess
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyIdpAccess
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import {
|
import {
|
||||||
verifyValidSubscription,
|
verifyValidSubscription,
|
||||||
@@ -31,6 +32,8 @@ import {
|
|||||||
authenticated as a
|
authenticated as a
|
||||||
} from "@server/routers/integration";
|
} from "@server/routers/integration";
|
||||||
import { logActionAudit } from "#private/middlewares";
|
import { logActionAudit } from "#private/middlewares";
|
||||||
|
import config from "#private/lib/config";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
export const unauthenticated = ua;
|
export const unauthenticated = ua;
|
||||||
export const authenticated = a;
|
export const authenticated = a;
|
||||||
@@ -88,3 +91,49 @@ authenticated.get(
|
|||||||
logActionAudit(ActionsEnum.exportLogs),
|
logActionAudit(ActionsEnum.exportLogs),
|
||||||
logs.exportAccessAuditLogs
|
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(`/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 { TierId } from "@server/lib/billing/tiers";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
const paramsSchema = z
|
const paramsSchema = z.strictObject({
|
||||||
.object({
|
orgId: z.string()
|
||||||
orgId: z.string()
|
});
|
||||||
})
|
|
||||||
.strict();
|
|
||||||
|
|
||||||
export async function getLoginPageBranding(
|
export async function getLoginPageBranding(
|
||||||
req: Request,
|
req: Request,
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ async function query(orgId: string | undefined, fullDomain: string) {
|
|||||||
eq(loginPage.loginPageId, loginPageOrg.loginPageId)
|
eq(loginPage.loginPageId, loginPageOrg.loginPageId)
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...res.loginPage,
|
...res.loginPage,
|
||||||
orgId: res.loginPageOrg.orgId
|
orgId: res.loginPageOrg.orgId
|
||||||
@@ -65,6 +70,11 @@ async function query(orgId: string | undefined, fullDomain: string) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...res,
|
...res,
|
||||||
orgId: orgLink.orgId
|
orgId: orgLink.orgId
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ async function query(orgId: string) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...res,
|
...res,
|
||||||
orgId: orgLink.orgs.orgId,
|
orgId: orgLink.orgs.orgId,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { eq, InferInsertModel } from "drizzle-orm";
|
|||||||
import { getOrgTierData } from "#private/lib/billing";
|
import { getOrgTierData } from "#private/lib/billing";
|
||||||
import { TierId } from "@server/lib/billing/tiers";
|
import { TierId } from "@server/lib/billing/tiers";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import config from "@server/private/lib/config";
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({
|
const paramsSchema = z.strictObject({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
@@ -94,8 +95,10 @@ export async function upsertLoginPageBranding(
|
|||||||
typeof loginPageBranding
|
typeof loginPageBranding
|
||||||
>;
|
>;
|
||||||
|
|
||||||
if (build !== "saas") {
|
if (
|
||||||
// org branding settings are only considered in the saas build
|
build !== "saas" &&
|
||||||
|
!config.getRawPrivateConfig().flags.use_org_only_idp
|
||||||
|
) {
|
||||||
const { orgTitle, orgSubtitle, ...rest } = updateData;
|
const { orgTitle, orgSubtitle, ...rest } = updateData;
|
||||||
updateData = rest;
|
updateData = rest;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,25 +43,27 @@ const bodySchema = z.strictObject({
|
|||||||
scopes: z.string().nonempty(),
|
scopes: z.string().nonempty(),
|
||||||
autoProvision: z.boolean().optional(),
|
autoProvision: z.boolean().optional(),
|
||||||
variant: z.enum(["oidc", "google", "azure"]).optional().default("oidc"),
|
variant: z.enum(["oidc", "google", "azure"]).optional().default("oidc"),
|
||||||
roleMapping: z.string().optional()
|
roleMapping: z.string().optional(),
|
||||||
|
tags: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
// registry.registerPath({
|
registry.registerPath({
|
||||||
// method: "put",
|
method: "put",
|
||||||
// path: "/idp/oidc",
|
path: "/org/{orgId}/idp/oidc",
|
||||||
// description: "Create an OIDC IdP.",
|
description: "Create an OIDC IdP for a specific organization.",
|
||||||
// tags: [OpenAPITags.Idp],
|
tags: [OpenAPITags.Idp, OpenAPITags.Org],
|
||||||
// request: {
|
request: {
|
||||||
// body: {
|
params: paramsSchema,
|
||||||
// content: {
|
body: {
|
||||||
// "application/json": {
|
content: {
|
||||||
// schema: bodySchema
|
"application/json": {
|
||||||
// }
|
schema: bodySchema
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// },
|
}
|
||||||
// responses: {}
|
},
|
||||||
// });
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
export async function createOrgOidcIdp(
|
export async function createOrgOidcIdp(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -103,7 +105,8 @@ export async function createOrgOidcIdp(
|
|||||||
name,
|
name,
|
||||||
autoProvision,
|
autoProvision,
|
||||||
variant,
|
variant,
|
||||||
roleMapping
|
roleMapping,
|
||||||
|
tags
|
||||||
} = parsedBody.data;
|
} = parsedBody.data;
|
||||||
|
|
||||||
if (build === "saas") {
|
if (build === "saas") {
|
||||||
@@ -131,7 +134,8 @@ export async function createOrgOidcIdp(
|
|||||||
.values({
|
.values({
|
||||||
name,
|
name,
|
||||||
autoProvision,
|
autoProvision,
|
||||||
type: "oidc"
|
type: "oidc",
|
||||||
|
tags
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ const paramsSchema = z
|
|||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "delete",
|
method: "delete",
|
||||||
path: "/idp/{idpId}",
|
path: "/org/{orgId}/idp/{idpId}",
|
||||||
description: "Delete IDP.",
|
description: "Delete IDP for a specific organization.",
|
||||||
tags: [OpenAPITags.Idp],
|
tags: [OpenAPITags.Idp, OpenAPITags.Org],
|
||||||
request: {
|
request: {
|
||||||
params: paramsSchema
|
params: paramsSchema
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,16 +48,16 @@ async function query(idpId: number, orgId: string) {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
// registry.registerPath({
|
registry.registerPath({
|
||||||
// method: "get",
|
method: "get",
|
||||||
// path: "/idp/{idpId}",
|
path: "/org/:orgId/idp/:idpId",
|
||||||
// description: "Get an IDP by its IDP ID.",
|
description: "Get an IDP by its IDP ID for a specific organization.",
|
||||||
// tags: [OpenAPITags.Idp],
|
tags: [OpenAPITags.Idp, OpenAPITags.Org],
|
||||||
// request: {
|
request: {
|
||||||
// params: paramsSchema
|
params: paramsSchema
|
||||||
// },
|
},
|
||||||
// responses: {}
|
responses: {}
|
||||||
// });
|
});
|
||||||
|
|
||||||
export async function getOrgIdp(
|
export async function getOrgIdp(
|
||||||
req: Request,
|
req: Request,
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ async function query(orgId: string, limit: number, offset: number) {
|
|||||||
orgId: idpOrg.orgId,
|
orgId: idpOrg.orgId,
|
||||||
name: idp.name,
|
name: idp.name,
|
||||||
type: idp.type,
|
type: idp.type,
|
||||||
variant: idpOidcConfig.variant
|
variant: idpOidcConfig.variant,
|
||||||
|
tags: idp.tags
|
||||||
})
|
})
|
||||||
.from(idpOrg)
|
.from(idpOrg)
|
||||||
.where(eq(idpOrg.orgId, orgId))
|
.where(eq(idpOrg.orgId, orgId))
|
||||||
@@ -62,16 +63,17 @@ async function query(orgId: string, limit: number, offset: number) {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
// registry.registerPath({
|
registry.registerPath({
|
||||||
// method: "get",
|
method: "get",
|
||||||
// path: "/idp",
|
path: "/org/{orgId}/idp",
|
||||||
// description: "List all IDP in the system.",
|
description: "List all IDP for a specific organization.",
|
||||||
// tags: [OpenAPITags.Idp],
|
tags: [OpenAPITags.Idp, OpenAPITags.Org],
|
||||||
// request: {
|
request: {
|
||||||
// query: querySchema
|
query: querySchema,
|
||||||
// },
|
params: paramsSchema
|
||||||
// responses: {}
|
},
|
||||||
// });
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
export async function listOrgIdps(
|
export async function listOrgIdps(
|
||||||
req: Request,
|
req: Request,
|
||||||
|
|||||||
@@ -46,30 +46,31 @@ const bodySchema = z.strictObject({
|
|||||||
namePath: z.string().optional(),
|
namePath: z.string().optional(),
|
||||||
scopes: z.string().optional(),
|
scopes: z.string().optional(),
|
||||||
autoProvision: z.boolean().optional(),
|
autoProvision: z.boolean().optional(),
|
||||||
roleMapping: z.string().optional()
|
roleMapping: z.string().optional(),
|
||||||
|
tags: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UpdateOrgIdpResponse = {
|
export type UpdateOrgIdpResponse = {
|
||||||
idpId: number;
|
idpId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// registry.registerPath({
|
registry.registerPath({
|
||||||
// method: "post",
|
method: "post",
|
||||||
// path: "/idp/{idpId}/oidc",
|
path: "/org/{orgId}/idp/{idpId}/oidc",
|
||||||
// description: "Update an OIDC IdP.",
|
description: "Update an OIDC IdP for a specific organization.",
|
||||||
// tags: [OpenAPITags.Idp],
|
tags: [OpenAPITags.Idp, OpenAPITags.Org],
|
||||||
// request: {
|
request: {
|
||||||
// params: paramsSchema,
|
params: paramsSchema,
|
||||||
// body: {
|
body: {
|
||||||
// content: {
|
content: {
|
||||||
// "application/json": {
|
"application/json": {
|
||||||
// schema: bodySchema
|
schema: bodySchema
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// },
|
},
|
||||||
// responses: {}
|
responses: {}
|
||||||
// });
|
});
|
||||||
|
|
||||||
export async function updateOrgOidcIdp(
|
export async function updateOrgOidcIdp(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -109,7 +110,8 @@ export async function updateOrgOidcIdp(
|
|||||||
namePath,
|
namePath,
|
||||||
name,
|
name,
|
||||||
autoProvision,
|
autoProvision,
|
||||||
roleMapping
|
roleMapping,
|
||||||
|
tags
|
||||||
} = parsedBody.data;
|
} = parsedBody.data;
|
||||||
|
|
||||||
if (build === "saas") {
|
if (build === "saas") {
|
||||||
@@ -167,7 +169,8 @@ export async function updateOrgOidcIdp(
|
|||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
const idpData = {
|
const idpData = {
|
||||||
name,
|
name,
|
||||||
autoProvision
|
autoProvision,
|
||||||
|
tags
|
||||||
};
|
};
|
||||||
|
|
||||||
// only update if at least one key is not undefined
|
// only update if at least one key is not undefined
|
||||||
|
|||||||
@@ -11,4 +11,4 @@
|
|||||||
* This file is not licensed under the AGPLv3.
|
* This file is not licensed under the AGPLv3.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./getMaintenanceInfo";
|
export * from "./getMaintenanceInfo";
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ import {
|
|||||||
WSMessage,
|
WSMessage,
|
||||||
TokenPayload,
|
TokenPayload,
|
||||||
WebSocketRequest,
|
WebSocketRequest,
|
||||||
RedisMessage
|
RedisMessage,
|
||||||
|
SendMessageOptions
|
||||||
} from "@server/routers/ws";
|
} from "@server/routers/ws";
|
||||||
import { validateSessionToken } from "@server/auth/sessions/app";
|
import { validateSessionToken } from "@server/auth/sessions/app";
|
||||||
|
|
||||||
@@ -118,12 +119,21 @@ const processMessage = async (
|
|||||||
if (response.broadcast) {
|
if (response.broadcast) {
|
||||||
await broadcastToAllExcept(
|
await broadcastToAllExcept(
|
||||||
response.message,
|
response.message,
|
||||||
response.excludeSender ? clientId : undefined
|
response.excludeSender ? clientId : undefined,
|
||||||
|
response.options
|
||||||
);
|
);
|
||||||
} else if (response.targetClientId) {
|
} else if (response.targetClientId) {
|
||||||
await sendToClient(response.targetClientId, response.message);
|
await sendToClient(
|
||||||
|
response.targetClientId,
|
||||||
|
response.message,
|
||||||
|
response.options
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
ws.send(JSON.stringify(response.message));
|
await sendToClient(
|
||||||
|
clientId,
|
||||||
|
response.message,
|
||||||
|
response.options
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -172,6 +182,9 @@ const REDIS_CHANNEL = "websocket_messages";
|
|||||||
// Client tracking map (local to this node)
|
// Client tracking map (local to this node)
|
||||||
const connectedClients: Map<string, AuthenticatedWebSocket[]> = new Map();
|
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
|
// Recovery tracking
|
||||||
let isRedisRecoveryInProgress = false;
|
let isRedisRecoveryInProgress = false;
|
||||||
|
|
||||||
@@ -182,6 +195,8 @@ const getClientMapKey = (clientId: string) => clientId;
|
|||||||
const getConnectionsKey = (clientId: string) => `ws:connections:${clientId}`;
|
const getConnectionsKey = (clientId: string) => `ws:connections:${clientId}`;
|
||||||
const getNodeConnectionsKey = (nodeId: string, clientId: string) =>
|
const getNodeConnectionsKey = (nodeId: string, clientId: string) =>
|
||||||
`ws:node:${nodeId}:${clientId}`;
|
`ws:node:${nodeId}:${clientId}`;
|
||||||
|
const getConfigVersionKey = (clientId: string) =>
|
||||||
|
`ws:configVersion:${clientId}`;
|
||||||
|
|
||||||
// Initialize Redis subscription for cross-node messaging
|
// Initialize Redis subscription for cross-node messaging
|
||||||
const initializeRedisSubscription = async (): Promise<void> => {
|
const initializeRedisSubscription = async (): Promise<void> => {
|
||||||
@@ -304,6 +319,45 @@ const addClient = async (
|
|||||||
existingClients.push(ws);
|
existingClients.push(ws);
|
||||||
connectedClients.set(mapKey, existingClients);
|
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
|
// Add to Redis tracking if enabled
|
||||||
if (redisManager.isRedisEnabled()) {
|
if (redisManager.isRedisEnabled()) {
|
||||||
try {
|
try {
|
||||||
@@ -322,7 +376,7 @@ const addClient = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
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)
|
// Local message sending (within this node)
|
||||||
const sendToClientLocal = async (
|
const sendToClientLocal = async (
|
||||||
clientId: string,
|
clientId: string,
|
||||||
message: WSMessage
|
message: WSMessage,
|
||||||
|
options: SendMessageOptions = {}
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
const mapKey = getClientMapKey(clientId);
|
const mapKey = getClientMapKey(clientId);
|
||||||
const clients = connectedClients.get(mapKey);
|
const clients = connectedClients.get(mapKey);
|
||||||
if (!clients || clients.length === 0) {
|
if (!clients || clients.length === 0) {
|
||||||
return false;
|
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) => {
|
clients.forEach((client) => {
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
client.send(messageString);
|
client.send(messageString);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`sendToClient: Message type ${message.type} sent to clientId ${clientId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const broadcastToAllExceptLocal = async (
|
const broadcastToAllExceptLocal = async (
|
||||||
message: WSMessage,
|
message: WSMessage,
|
||||||
excludeClientId?: string
|
excludeClientId?: string,
|
||||||
|
options: SendMessageOptions = {}
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
connectedClients.forEach((clients, mapKey) => {
|
for (const [mapKey, clients] of connectedClients.entries()) {
|
||||||
const [type, id] = mapKey.split(":");
|
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) => {
|
clients.forEach((client) => {
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
client.send(JSON.stringify(message));
|
client.send(JSON.stringify(messageWithVersion));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cross-node message sending (via Redis)
|
// Cross-node message sending (via Redis)
|
||||||
const sendToClient = async (
|
const sendToClient = async (
|
||||||
clientId: string,
|
clientId: string,
|
||||||
message: WSMessage
|
message: WSMessage,
|
||||||
|
options: SendMessageOptions = {}
|
||||||
): Promise<boolean> => {
|
): 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
|
// 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
|
// Only send via Redis if the client is not connected locally and Redis is enabled
|
||||||
if (!localSent && redisManager.isRedisEnabled()) {
|
if (!localSent && redisManager.isRedisEnabled()) {
|
||||||
@@ -431,7 +565,10 @@ const sendToClient = async (
|
|||||||
const redisMessage: RedisMessage = {
|
const redisMessage: RedisMessage = {
|
||||||
type: "direct",
|
type: "direct",
|
||||||
targetClientId: clientId,
|
targetClientId: clientId,
|
||||||
message,
|
message: {
|
||||||
|
...message,
|
||||||
|
configVersion
|
||||||
|
},
|
||||||
fromNodeId: NODE_ID
|
fromNodeId: NODE_ID
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -458,19 +595,22 @@ const sendToClient = async (
|
|||||||
|
|
||||||
const broadcastToAllExcept = async (
|
const broadcastToAllExcept = async (
|
||||||
message: WSMessage,
|
message: WSMessage,
|
||||||
excludeClientId?: string
|
excludeClientId?: string,
|
||||||
|
options: SendMessageOptions = {}
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
// Broadcast locally
|
// Broadcast locally
|
||||||
await broadcastToAllExceptLocal(message, excludeClientId);
|
await broadcastToAllExceptLocal(message, excludeClientId, options);
|
||||||
|
|
||||||
// If Redis is enabled, also broadcast via Redis pub/sub to other nodes
|
// 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()) {
|
if (redisManager.isRedisEnabled()) {
|
||||||
try {
|
try {
|
||||||
const redisMessage: RedisMessage = {
|
const redisMessage: RedisMessage = {
|
||||||
type: "broadcast",
|
type: "broadcast",
|
||||||
excludeClientId,
|
excludeClientId,
|
||||||
message,
|
message,
|
||||||
fromNodeId: NODE_ID
|
fromNodeId: NODE_ID,
|
||||||
|
options
|
||||||
};
|
};
|
||||||
|
|
||||||
await redisManager.publish(
|
await redisManager.publish(
|
||||||
@@ -936,5 +1076,6 @@ export {
|
|||||||
getActiveNodes,
|
getActiveNodes,
|
||||||
disconnectClient,
|
disconnectClient,
|
||||||
NODE_ID,
|
NODE_ID,
|
||||||
cleanup
|
cleanup,
|
||||||
|
getClientConfigVersion
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -99,12 +99,13 @@ async function query(query: Q) {
|
|||||||
.where(and(baseConditions, not(isNull(requestAuditLog.location))))
|
.where(and(baseConditions, not(isNull(requestAuditLog.location))))
|
||||||
.groupBy(requestAuditLog.location)
|
.groupBy(requestAuditLog.location)
|
||||||
.orderBy(desc(totalQ))
|
.orderBy(desc(totalQ))
|
||||||
.limit(DISTINCT_LIMIT+1);
|
.limit(DISTINCT_LIMIT + 1);
|
||||||
|
|
||||||
if (requestsPerCountry.length > DISTINCT_LIMIT) {
|
if (requestsPerCountry.length > DISTINCT_LIMIT) {
|
||||||
// throw an error
|
// throw an error
|
||||||
throw createHttpError(
|
throw createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
|
// todo: is this even possible?
|
||||||
`Too many distinct countries. Please narrow your query.`
|
`Too many distinct countries. Please narrow your query.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,22 +189,22 @@ async function queryUniqueFilterAttributes(
|
|||||||
.selectDistinct({ actor: requestAuditLog.actor })
|
.selectDistinct({ actor: requestAuditLog.actor })
|
||||||
.from(requestAuditLog)
|
.from(requestAuditLog)
|
||||||
.where(baseConditions)
|
.where(baseConditions)
|
||||||
.limit(DISTINCT_LIMIT+1),
|
.limit(DISTINCT_LIMIT + 1),
|
||||||
primaryDb
|
primaryDb
|
||||||
.selectDistinct({ locations: requestAuditLog.location })
|
.selectDistinct({ locations: requestAuditLog.location })
|
||||||
.from(requestAuditLog)
|
.from(requestAuditLog)
|
||||||
.where(baseConditions)
|
.where(baseConditions)
|
||||||
.limit(DISTINCT_LIMIT+1),
|
.limit(DISTINCT_LIMIT + 1),
|
||||||
primaryDb
|
primaryDb
|
||||||
.selectDistinct({ hosts: requestAuditLog.host })
|
.selectDistinct({ hosts: requestAuditLog.host })
|
||||||
.from(requestAuditLog)
|
.from(requestAuditLog)
|
||||||
.where(baseConditions)
|
.where(baseConditions)
|
||||||
.limit(DISTINCT_LIMIT+1),
|
.limit(DISTINCT_LIMIT + 1),
|
||||||
primaryDb
|
primaryDb
|
||||||
.selectDistinct({ paths: requestAuditLog.path })
|
.selectDistinct({ paths: requestAuditLog.path })
|
||||||
.from(requestAuditLog)
|
.from(requestAuditLog)
|
||||||
.where(baseConditions)
|
.where(baseConditions)
|
||||||
.limit(DISTINCT_LIMIT+1),
|
.limit(DISTINCT_LIMIT + 1),
|
||||||
primaryDb
|
primaryDb
|
||||||
.selectDistinct({
|
.selectDistinct({
|
||||||
id: requestAuditLog.resourceId,
|
id: requestAuditLog.resourceId,
|
||||||
@@ -216,18 +216,20 @@ async function queryUniqueFilterAttributes(
|
|||||||
eq(requestAuditLog.resourceId, resources.resourceId)
|
eq(requestAuditLog.resourceId, resources.resourceId)
|
||||||
)
|
)
|
||||||
.where(baseConditions)
|
.where(baseConditions)
|
||||||
.limit(DISTINCT_LIMIT+1)
|
.limit(DISTINCT_LIMIT + 1)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (
|
// TODO: for stuff like the paths this is too restrictive so lets just show some of the paths and the user needs to
|
||||||
uniqueActors.length > DISTINCT_LIMIT ||
|
// refine the time range to see what they need to see
|
||||||
uniqueLocations.length > DISTINCT_LIMIT ||
|
// if (
|
||||||
uniqueHosts.length > DISTINCT_LIMIT ||
|
// uniqueActors.length > DISTINCT_LIMIT ||
|
||||||
uniquePaths.length > DISTINCT_LIMIT ||
|
// uniqueLocations.length > DISTINCT_LIMIT ||
|
||||||
uniqueResources.length > DISTINCT_LIMIT
|
// uniqueHosts.length > DISTINCT_LIMIT ||
|
||||||
) {
|
// uniquePaths.length > DISTINCT_LIMIT ||
|
||||||
throw new Error("Too many distinct filter attributes to retrieve. Please refine your time range.");
|
// uniqueResources.length > DISTINCT_LIMIT
|
||||||
}
|
// ) {
|
||||||
|
// throw new Error("Too many distinct filter attributes to retrieve. Please refine your time range.");
|
||||||
|
// }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
actors: uniqueActors
|
actors: uniqueActors
|
||||||
@@ -307,10 +309,12 @@ export async function queryRequestAuditLogs(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(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 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.") {
|
if (
|
||||||
return next(
|
error instanceof Error &&
|
||||||
createHttpError(HttpCode.BAD_REQUEST, error.message)
|
error.message ===
|
||||||
);
|
"Too many distinct filter attributes to retrieve. Please refine your time range."
|
||||||
|
) {
|
||||||
|
return next(createHttpError(HttpCode.BAD_REQUEST, error.message));
|
||||||
}
|
}
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ export * from "./securityKey";
|
|||||||
export * from "./startDeviceWebAuth";
|
export * from "./startDeviceWebAuth";
|
||||||
export * from "./verifyDeviceWebAuth";
|
export * from "./verifyDeviceWebAuth";
|
||||||
export * from "./pollDeviceWebAuth";
|
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 { createSession, generateSessionToken } from "@server/auth/sessions/app";
|
||||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||||
import { sha256 } from "@oslojs/crypto/sha2";
|
import { sha256 } from "@oslojs/crypto/sha2";
|
||||||
|
import { stripPortFromHost } from "@server/lib/ip";
|
||||||
|
|
||||||
const paramsSchema = z.object({
|
const paramsSchema = z.object({
|
||||||
code: z.string().min(1, "Code is required")
|
code: z.string().min(1, "Code is required")
|
||||||
@@ -27,30 +28,6 @@ export type PollDeviceWebAuthResponse = {
|
|||||||
token?: string;
|
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(
|
export async function pollDeviceWebAuth(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
@@ -70,7 +47,7 @@ export async function pollDeviceWebAuth(
|
|||||||
try {
|
try {
|
||||||
const { code } = parsedParams.data;
|
const { code } = parsedParams.data;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const requestIp = extractIpFromRequest(req);
|
const requestIp = req.ip ? stripPortFromHost(req.ip) : undefined;
|
||||||
|
|
||||||
// Hash the code before querying
|
// Hash the code before querying
|
||||||
const hashedCode = hashDeviceCode(code);
|
const hashedCode = hashDeviceCode(code);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { TimeSpan } from "oslo";
|
|||||||
import { maxmindLookup } from "@server/db/maxmind";
|
import { maxmindLookup } from "@server/db/maxmind";
|
||||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||||
import { sha256 } from "@oslojs/crypto/sha2";
|
import { sha256 } from "@oslojs/crypto/sha2";
|
||||||
|
import { stripPortFromHost } from "@server/lib/ip";
|
||||||
|
|
||||||
const bodySchema = z
|
const bodySchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -39,30 +40,6 @@ function hashDeviceCode(code: string): string {
|
|||||||
return encodeHexLowerCase(sha256(new TextEncoder().encode(code)));
|
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)
|
// Helper function to get city from IP (if available)
|
||||||
async function getCityFromIp(ip: string): Promise<string | undefined> {
|
async function getCityFromIp(ip: string): Promise<string | undefined> {
|
||||||
try {
|
try {
|
||||||
@@ -112,7 +89,7 @@ export async function startDeviceWebAuth(
|
|||||||
const hashedCode = hashDeviceCode(code);
|
const hashedCode = hashDeviceCode(code);
|
||||||
|
|
||||||
// Extract IP from request
|
// Extract IP from request
|
||||||
const ip = extractIpFromRequest(req);
|
const ip = req.ip ? stripPortFromHost(req.ip) : undefined;
|
||||||
|
|
||||||
// Get city (optional, may return undefined)
|
// Get city (optional, may return undefined)
|
||||||
const city = ip ? await getCityFromIp(ip) : 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 { encodeHexLowerCase } from "@oslojs/encoding";
|
||||||
import { sha256 } from "@oslojs/crypto/sha2";
|
import { sha256 } from "@oslojs/crypto/sha2";
|
||||||
import { unauthorized } from "@server/auth/unauthorizedResponse";
|
import { unauthorized } from "@server/auth/unauthorizedResponse";
|
||||||
|
import { getIosDeviceName, getMacDeviceName } from "@server/db/names";
|
||||||
|
|
||||||
const bodySchema = z
|
const bodySchema = z
|
||||||
.object({
|
.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 is false, just return metadata without verifying
|
||||||
if (!verify) {
|
if (!verify) {
|
||||||
return response<VerifyDeviceWebAuthResponse>(res, {
|
return response<VerifyDeviceWebAuthResponse>(res, {
|
||||||
@@ -129,7 +135,7 @@ export async function verifyDeviceWebAuth(
|
|||||||
metadata: {
|
metadata: {
|
||||||
ip: deviceCode.ip,
|
ip: deviceCode.ip,
|
||||||
city: deviceCode.city,
|
city: deviceCode.city,
|
||||||
deviceName: deviceCode.deviceName,
|
deviceName: deviceName,
|
||||||
applicationName: deviceCode.applicationName,
|
applicationName: deviceCode.applicationName,
|
||||||
createdAt: deviceCode.createdAt
|
createdAt: deviceCode.createdAt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import { SESSION_COOKIE_EXPIRES as RESOURCE_SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/resource";
|
import { SESSION_COOKIE_EXPIRES as RESOURCE_SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/resource";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { response } from "@server/lib/response";
|
import { response } from "@server/lib/response";
|
||||||
|
import { stripPortFromHost } from "@server/lib/ip";
|
||||||
|
|
||||||
const exchangeSessionBodySchema = z.object({
|
const exchangeSessionBodySchema = z.object({
|
||||||
requestToken: z.string(),
|
requestToken: z.string(),
|
||||||
@@ -62,7 +63,7 @@ export async function exchangeSession(
|
|||||||
cleanHost = cleanHost.slice(0, -1 * matched.length);
|
cleanHost = cleanHost.slice(0, -1 * matched.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientIp = requestIp?.split(":")[0];
|
const clientIp = requestIp ? stripPortFromHost(requestIp) : undefined;
|
||||||
|
|
||||||
const [resource] = await db
|
const [resource] = await db
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import logger from "@server/logger";
|
|||||||
import { and, eq, lt } from "drizzle-orm";
|
import { and, eq, lt } from "drizzle-orm";
|
||||||
import cache from "@server/lib/cache";
|
import cache from "@server/lib/cache";
|
||||||
import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs";
|
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_SIZE = 100; // Write to DB every 100 logs
|
||||||
const BATCH_INTERVAL_MS = 5000; // Or every 5 seconds, whichever comes first
|
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 flushTimer: NodeJS.Timeout | null = null;
|
||||||
|
let isFlushInProgress = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flush buffered logs to database
|
* Flush buffered logs to database
|
||||||
*/
|
*/
|
||||||
async function flushAuditLogs() {
|
async function flushAuditLogs() {
|
||||||
if (auditLogBuffer.length === 0) {
|
if (auditLogBuffer.length === 0 || isFlushInProgress) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isFlushInProgress = true;
|
||||||
|
|
||||||
// Take all current logs and clear buffer
|
// Take all current logs and clear buffer
|
||||||
const logsToWrite = auditLogBuffer.splice(0, auditLogBuffer.length);
|
const logsToWrite = auditLogBuffer.splice(0, auditLogBuffer.length);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Batch insert all logs at once
|
// Batch insert logs in groups of 25 to avoid overwhelming the database
|
||||||
await db.insert(requestAuditLog).values(logsToWrite);
|
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`);
|
logger.debug(`Flushed ${logsToWrite.length} audit logs to database`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error flushing audit logs:", error);
|
logger.error("Error flushing audit logs:", error);
|
||||||
// On error, we lose these logs - consider a fallback strategy if needed
|
// 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)
|
// (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);
|
clearTimeout(flushTimer);
|
||||||
flushTimer = null;
|
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();
|
await flushAuditLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,28 +229,17 @@ export async function logRequestAudit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const clientIp = body.requestIp
|
const clientIp = body.requestIp
|
||||||
? (() => {
|
? stripPortFromHost(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;
|
|
||||||
})()
|
|
||||||
: undefined;
|
: 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
|
// Add to buffer instead of writing directly to DB
|
||||||
auditLogBuffer.push({
|
auditLogBuffer.push({
|
||||||
timestamp,
|
timestamp,
|
||||||
|
|||||||
@@ -13,14 +13,15 @@ import {
|
|||||||
LoginPage,
|
LoginPage,
|
||||||
Org,
|
Org,
|
||||||
Resource,
|
Resource,
|
||||||
ResourceHeaderAuth, ResourceHeaderAuthExtendedCompatibility,
|
ResourceHeaderAuth,
|
||||||
|
ResourceHeaderAuthExtendedCompatibility,
|
||||||
ResourcePassword,
|
ResourcePassword,
|
||||||
ResourcePincode,
|
ResourcePincode,
|
||||||
ResourceRule,
|
ResourceRule,
|
||||||
resourceSessions
|
resourceSessions
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import config from "@server/lib/config";
|
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 { response } from "@server/lib/response";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -39,6 +40,8 @@ import {
|
|||||||
} from "#dynamic/lib/checkOrgAccessPolicy";
|
} from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
import { logRequestAudit } from "./logRequestAudit";
|
import { logRequestAudit } from "./logRequestAudit";
|
||||||
import cache from "@server/lib/cache";
|
import cache from "@server/lib/cache";
|
||||||
|
import semver from "semver";
|
||||||
|
import { APP_VERSION } from "@server/lib/consts";
|
||||||
|
|
||||||
const verifyResourceSessionSchema = z.object({
|
const verifyResourceSessionSchema = z.object({
|
||||||
sessions: z.record(z.string(), z.string()).optional(),
|
sessions: z.record(z.string(), z.string()).optional(),
|
||||||
@@ -50,7 +53,8 @@ const verifyResourceSessionSchema = z.object({
|
|||||||
path: z.string(),
|
path: z.string(),
|
||||||
method: z.string(),
|
method: z.string(),
|
||||||
tls: z.boolean(),
|
tls: z.boolean(),
|
||||||
requestIp: z.string().optional()
|
requestIp: z.string().optional(),
|
||||||
|
badgerVersion: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type VerifyResourceSessionSchema = z.infer<
|
export type VerifyResourceSessionSchema = z.infer<
|
||||||
@@ -69,6 +73,7 @@ export type VerifyUserResponse = {
|
|||||||
headerAuthChallenged?: boolean;
|
headerAuthChallenged?: boolean;
|
||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
userData?: BasicUserData;
|
userData?: BasicUserData;
|
||||||
|
pangolinVersion?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function verifyResourceSession(
|
export async function verifyResourceSession(
|
||||||
@@ -97,31 +102,15 @@ export async function verifyResourceSession(
|
|||||||
requestIp,
|
requestIp,
|
||||||
path,
|
path,
|
||||||
headers,
|
headers,
|
||||||
query
|
query,
|
||||||
|
badgerVersion
|
||||||
} = parsedBody.data;
|
} = parsedBody.data;
|
||||||
|
|
||||||
// Extract HTTP Basic Auth credentials if present
|
// Extract HTTP Basic Auth credentials if present
|
||||||
const clientHeaderAuth = extractBasicAuth(headers);
|
const clientHeaderAuth = extractBasicAuth(headers);
|
||||||
|
|
||||||
const clientIp = requestIp
|
const clientIp = requestIp
|
||||||
? (() => {
|
? stripPortFromHost(requestIp, badgerVersion)
|
||||||
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;
|
|
||||||
})()
|
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
logger.debug("Client IP:", { clientIp });
|
logger.debug("Client IP:", { clientIp });
|
||||||
@@ -130,9 +119,7 @@ export async function verifyResourceSession(
|
|||||||
? await getCountryCodeFromIp(clientIp)
|
? await getCountryCodeFromIp(clientIp)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const ipAsn = clientIp
|
const ipAsn = clientIp ? await getAsnFromIp(clientIp) : undefined;
|
||||||
? await getAsnFromIp(clientIp)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
let cleanHost = host;
|
let cleanHost = host;
|
||||||
// if the host ends with :port, strip it
|
// if the host ends with :port, strip it
|
||||||
@@ -178,7 +165,13 @@ export async function verifyResourceSession(
|
|||||||
cache.set(resourceCacheKey, resourceData, 5);
|
cache.set(resourceCacheKey, resourceData, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resource, pincode, password, headerAuth, headerAuthExtendedCompatibility } = resourceData;
|
const {
|
||||||
|
resource,
|
||||||
|
pincode,
|
||||||
|
password,
|
||||||
|
headerAuth,
|
||||||
|
headerAuthExtendedCompatibility
|
||||||
|
} = resourceData;
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
logger.debug(`Resource not found ${cleanHost}`);
|
logger.debug(`Resource not found ${cleanHost}`);
|
||||||
@@ -474,8 +467,7 @@ export async function verifyResourceSession(
|
|||||||
|
|
||||||
return notAllowed(res);
|
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 there are no other auth methods we need to return unauthorized if nothing is provided
|
||||||
if (
|
if (
|
||||||
!sso &&
|
!sso &&
|
||||||
@@ -713,7 +705,11 @@ export async function verifyResourceSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If headerAuthExtendedCompatibility is activated but no clientHeaderAuth provided, force client to challenge
|
// 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);
|
return headerAuthChallenged(res, redirectPath, resource.orgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -825,7 +821,7 @@ async function notAllowed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
data: { valid: false, redirectUrl },
|
data: { valid: false, redirectUrl, pangolinVersion: APP_VERSION },
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Access denied",
|
message: "Access denied",
|
||||||
@@ -839,8 +835,8 @@ function allowed(res: Response, userData?: BasicUserData) {
|
|||||||
const data = {
|
const data = {
|
||||||
data:
|
data:
|
||||||
userData !== undefined && userData !== null
|
userData !== undefined && userData !== null
|
||||||
? { valid: true, ...userData }
|
? { valid: true, ...userData, pangolinVersion: APP_VERSION }
|
||||||
: { valid: true },
|
: { valid: true, pangolinVersion: APP_VERSION },
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Access allowed",
|
message: "Access allowed",
|
||||||
@@ -879,7 +875,12 @@ async function headerAuthChallenged(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
data: { headerAuthChallenged: true, valid: false, redirectUrl },
|
data: {
|
||||||
|
headerAuthChallenged: true,
|
||||||
|
valid: false,
|
||||||
|
redirectUrl,
|
||||||
|
pangolinVersion: APP_VERSION
|
||||||
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Access denied",
|
message: "Access denied",
|
||||||
@@ -941,7 +942,7 @@ async function isUserAllowedToAccessResource(
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
role: user.role
|
role: userOrgRole.roleName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -955,7 +956,7 @@ async function isUserAllowedToAccessResource(
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
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 pattern parts: [${patternParts.join(", ")}]`);
|
||||||
logger.debug(`Normalized path parts: [${pathParts.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
|
// Recursive function to try different wildcard matches
|
||||||
function matchSegments(patternIndex: number, pathIndex: number): boolean {
|
function matchSegments(patternIndex: number, pathIndex: number, depth: number = 0): boolean {
|
||||||
const indent = " ".repeat(pathIndex); // Indent based on recursion depth
|
// 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 currentPatternPart = patternParts[patternIndex];
|
||||||
const currentPathPart = pathParts[pathIndex];
|
const currentPathPart = pathParts[pathIndex];
|
||||||
|
|
||||||
logger.debug(
|
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
|
// 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(
|
logger.debug(
|
||||||
`${indent}Trying to skip wildcard (consume 0 segments)`
|
`${indent}Trying to skip wildcard (consume 0 segments)`
|
||||||
);
|
);
|
||||||
if (matchSegments(patternIndex + 1, pathIndex)) {
|
if (matchSegments(patternIndex + 1, pathIndex, depth + 1)) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`${indent}Successfully matched by skipping wildcard`
|
`${indent}Successfully matched by skipping wildcard`
|
||||||
);
|
);
|
||||||
@@ -1085,7 +1097,7 @@ export function isPathAllowed(pattern: string, path: string): boolean {
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
`${indent}Trying to consume segment "${currentPathPart}" for wildcard`
|
`${indent}Trying to consume segment "${currentPathPart}" for wildcard`
|
||||||
);
|
);
|
||||||
if (matchSegments(patternIndex, pathIndex + 1)) {
|
if (matchSegments(patternIndex, pathIndex + 1, depth + 1)) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`${indent}Successfully matched by consuming segment for wildcard`
|
`${indent}Successfully matched by consuming segment for wildcard`
|
||||||
);
|
);
|
||||||
@@ -1113,7 +1125,7 @@ export function isPathAllowed(pattern: string, path: string): boolean {
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
`${indent}Segment with wildcard matches: "${currentPatternPart}" matches "${currentPathPart}"`
|
`${indent}Segment with wildcard matches: "${currentPatternPart}" matches "${currentPathPart}"`
|
||||||
);
|
);
|
||||||
return matchSegments(patternIndex + 1, pathIndex + 1);
|
return matchSegments(patternIndex + 1, pathIndex + 1, depth + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -1134,10 +1146,10 @@ export function isPathAllowed(pattern: string, path: string): boolean {
|
|||||||
`${indent}Segments match: "${currentPatternPart}" = "${currentPathPart}"`
|
`${indent}Segments match: "${currentPatternPart}" = "${currentPathPart}"`
|
||||||
);
|
);
|
||||||
// Move to next segments in both pattern and path
|
// 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}`);
|
logger.debug(`Final result: ${result}`);
|
||||||
return 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) {
|
if (client.userId) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
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 { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, olms } from "@server/db";
|
import { db, olms } from "@server/db";
|
||||||
import { clients } from "@server/db";
|
import { clients, fingerprints } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -10,6 +10,7 @@ import logger from "@server/logger";
|
|||||||
import stoi from "@server/lib/stoi";
|
import stoi from "@server/lib/stoi";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { getUserDeviceName } from "@server/db/names";
|
||||||
|
|
||||||
const getClientSchema = z.strictObject({
|
const getClientSchema = z.strictObject({
|
||||||
clientId: z
|
clientId: z
|
||||||
@@ -29,6 +30,7 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
|
|||||||
.from(clients)
|
.from(clients)
|
||||||
.where(eq(clients.clientId, clientId))
|
.where(eq(clients.clientId, clientId))
|
||||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||||
|
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
return res;
|
return res;
|
||||||
} else if (niceId && orgId) {
|
} else if (niceId && orgId) {
|
||||||
@@ -36,7 +38,8 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
|
|||||||
.select()
|
.select()
|
||||||
.from(clients)
|
.from(clients)
|
||||||
.where(and(eq(clients.niceId, niceId), eq(clients.orgId, orgId)))
|
.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);
|
.limit(1);
|
||||||
return res;
|
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 = {
|
const data: GetClientResponse = {
|
||||||
...client.clients,
|
...client.clients,
|
||||||
|
name: clientName,
|
||||||
olmId: client.olms ? client.olms.olmId : null
|
olmId: client.olms ? client.olms.olmId : null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
export * from "./pickClientDefaults";
|
export * from "./pickClientDefaults";
|
||||||
export * from "./createClient";
|
export * from "./createClient";
|
||||||
export * from "./deleteClient";
|
export * from "./deleteClient";
|
||||||
|
export * from "./archiveClient";
|
||||||
|
export * from "./unarchiveClient";
|
||||||
|
export * from "./blockClient";
|
||||||
|
export * from "./unblockClient";
|
||||||
export * from "./listClients";
|
export * from "./listClients";
|
||||||
export * from "./updateClient";
|
export * from "./updateClient";
|
||||||
export * from "./getClient";
|
export * from "./getClient";
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import {
|
|||||||
roleClients,
|
roleClients,
|
||||||
sites,
|
sites,
|
||||||
userClients,
|
userClients,
|
||||||
clientSitesAssociationsCache
|
clientSitesAssociationsCache,
|
||||||
|
fingerprints
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -27,6 +28,7 @@ import { fromError } from "zod-validation-error";
|
|||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import NodeCache from "node-cache";
|
import NodeCache from "node-cache";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
|
import { getUserDeviceName } from "@server/db/names";
|
||||||
|
|
||||||
const olmVersionCache = new NodeCache({ stdTTL: 3600 });
|
const olmVersionCache = new NodeCache({ stdTTL: 3600 });
|
||||||
|
|
||||||
@@ -56,12 +58,12 @@ async function getLatestOlmVersion(): Promise<string | null> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = await response.json();
|
let tags = await response.json();
|
||||||
if (!Array.isArray(tags) || tags.length === 0) {
|
if (!Array.isArray(tags) || tags.length === 0) {
|
||||||
logger.warn("No tags found for Olm repository");
|
logger.warn("No tags found for Olm repository");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
tags = tags.filter((version) => !version.name.includes("rc"));
|
||||||
const latestVersion = tags[0].name;
|
const latestVersion = tags[0].name;
|
||||||
|
|
||||||
olmVersionCache.set("latestOlmVersion", latestVersion);
|
olmVersionCache.set("latestOlmVersion", latestVersion);
|
||||||
@@ -136,12 +138,18 @@ function queryClients(
|
|||||||
username: users.username,
|
username: users.username,
|
||||||
userEmail: users.email,
|
userEmail: users.email,
|
||||||
niceId: clients.niceId,
|
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)
|
.from(clients)
|
||||||
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
||||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||||
.leftJoin(users, eq(clients.userId, users.userId))
|
.leftJoin(users, eq(clients.userId, users.userId))
|
||||||
|
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
|
||||||
.where(and(...conditions));
|
.where(and(...conditions));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,21 +168,22 @@ async function getSiteAssociations(clientIds: number[]) {
|
|||||||
.where(inArray(clientSitesAssociationsCache.clientId, clientIds));
|
.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;
|
olmUpdateAvailable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type OlmWithUpdateAvailable = ClientWithSites;
|
||||||
|
|
||||||
export type ListClientsResponse = {
|
export type ListClientsResponse = {
|
||||||
clients: Array<
|
clients: Array<ClientWithSites>;
|
||||||
Awaited<ReturnType<typeof queryClients>>[0] & {
|
|
||||||
sites: Array<{
|
|
||||||
siteId: number;
|
|
||||||
siteName: string | null;
|
|
||||||
siteNiceId: string | null;
|
|
||||||
}>;
|
|
||||||
olmUpdateAvailable?: boolean;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
pagination: { total: number; limit: number; offset: number };
|
pagination: { total: number; limit: number; offset: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -304,11 +313,17 @@ export async function listClients(
|
|||||||
>
|
>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Merge clients with their site associations
|
// Merge clients with their site associations and replace name with device name
|
||||||
const clientsWithSites = clientsList.map((client) => ({
|
const clientsWithSites = clientsList.map((client) => {
|
||||||
...client,
|
const model = client.deviceModel || null;
|
||||||
sites: sitesByClient[client.clientId] || []
|
const newName = getUserDeviceName(model, client.name);
|
||||||
}));
|
const { deviceModel, ...clientWithoutDeviceModel } = client;
|
||||||
|
return {
|
||||||
|
...clientWithoutDeviceModel,
|
||||||
|
name: newName,
|
||||||
|
sites: sitesByClient[client.clientId] || []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const latestOlVersionPromise = getLatestOlmVersion();
|
const latestOlVersionPromise = getLatestOlmVersion();
|
||||||
|
|
||||||
@@ -347,7 +362,7 @@ export async function listClients(
|
|||||||
|
|
||||||
return response<ListClientsResponse>(res, {
|
return response<ListClientsResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
clients: clientsWithSites,
|
clients: olmsWithUpdates,
|
||||||
pagination: {
|
pagination: {
|
||||||
total: totalCount,
|
total: totalCount,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export async function addTargets(newtId: string, targets: SubnetProxyTarget[]) {
|
|||||||
await sendToClient(newtId, {
|
await sendToClient(newtId, {
|
||||||
type: `newt/wg/targets/add`,
|
type: `newt/wg/targets/add`,
|
||||||
data: batches[i]
|
data: batches[i]
|
||||||
});
|
}, { incrementConfigVersion: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ export async function removeTargets(
|
|||||||
await sendToClient(newtId, {
|
await sendToClient(newtId, {
|
||||||
type: `newt/wg/targets/remove`,
|
type: `newt/wg/targets/remove`,
|
||||||
data: batches[i]
|
data: batches[i]
|
||||||
});
|
},{ incrementConfigVersion: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export async function updateTargets(
|
|||||||
oldTargets: oldBatches[i] || [],
|
oldTargets: oldBatches[i] || [],
|
||||||
newTargets: newBatches[i] || []
|
newTargets: newBatches[i] || []
|
||||||
}
|
}
|
||||||
}).catch((error) => {
|
}, { incrementConfigVersion: true }).catch((error) => {
|
||||||
logger.warn(`Error sending message:`, error);
|
logger.warn(`Error sending message:`, error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ export async function addPeerData(
|
|||||||
remoteSubnets: remoteSubnets,
|
remoteSubnets: remoteSubnets,
|
||||||
aliases: aliases
|
aliases: aliases
|
||||||
}
|
}
|
||||||
}).catch((error) => {
|
}, { incrementConfigVersion: true }).catch((error) => {
|
||||||
logger.warn(`Error sending message:`, error);
|
logger.warn(`Error sending message:`, error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -132,7 +132,7 @@ export async function removePeerData(
|
|||||||
remoteSubnets: remoteSubnets,
|
remoteSubnets: remoteSubnets,
|
||||||
aliases: aliases
|
aliases: aliases
|
||||||
}
|
}
|
||||||
}).catch((error) => {
|
}, { incrementConfigVersion: true }).catch((error) => {
|
||||||
logger.warn(`Error sending message:`, error);
|
logger.warn(`Error sending message:`, error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -173,7 +173,7 @@ export async function updatePeerData(
|
|||||||
...remoteSubnets,
|
...remoteSubnets,
|
||||||
...aliases
|
...aliases
|
||||||
}
|
}
|
||||||
}).catch((error) => {
|
}, { incrementConfigVersion: true }).catch((error) => {
|
||||||
logger.warn(`Error sending message:`, 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
|
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(
|
authenticated.post(
|
||||||
"/client/:clientId",
|
"/client/:clientId",
|
||||||
verifyClientAccess, // this will check if the user has access to the client
|
verifyClientAccess, // this will check if the user has access to the client
|
||||||
@@ -554,6 +586,14 @@ authenticated.get(
|
|||||||
verifyUserHasAction(ActionsEnum.listRoles),
|
verifyUserHasAction(ActionsEnum.listRoles),
|
||||||
role.listRoles
|
role.listRoles
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/role/:roleId",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.updateRole),
|
||||||
|
logActionAudit(ActionsEnum.updateRole),
|
||||||
|
role.updateRole
|
||||||
|
);
|
||||||
// authenticated.get(
|
// authenticated.get(
|
||||||
// "/role/:roleId",
|
// "/role/:roleId",
|
||||||
// verifyRoleAccess,
|
// verifyRoleAccess,
|
||||||
@@ -808,11 +848,18 @@ authenticated.put("/user/:userId/olm", verifyIsLoggedInUser, olm.createUserOlm);
|
|||||||
|
|
||||||
authenticated.get("/user/:userId/olms", verifyIsLoggedInUser, olm.listUserOlms);
|
authenticated.get("/user/:userId/olms", verifyIsLoggedInUser, olm.listUserOlms);
|
||||||
|
|
||||||
authenticated.delete(
|
authenticated.post(
|
||||||
"/user/:userId/olm/:olmId",
|
"/user/:userId/olm/:olmId/archive",
|
||||||
verifyIsLoggedInUser,
|
verifyIsLoggedInUser,
|
||||||
verifyOlmAccess,
|
verifyOlmAccess,
|
||||||
olm.deleteUserOlm
|
olm.archiveUserOlm
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/user/:userId/olm/:olmId/unarchive",
|
||||||
|
verifyIsLoggedInUser,
|
||||||
|
verifyOlmAccess,
|
||||||
|
olm.unarchiveUserOlm
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
@@ -822,6 +869,12 @@ authenticated.get(
|
|||||||
olm.getUserOlm
|
olm.getUserOlm
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/user/:userId/olm/recover",
|
||||||
|
verifyIsLoggedInUser,
|
||||||
|
olm.recoverOlmWithFingerprint
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.put(
|
authenticated.put(
|
||||||
"/idp/oidc",
|
"/idp/oidc",
|
||||||
verifyUserIsServerAdmin,
|
verifyUserIsServerAdmin,
|
||||||
@@ -1068,6 +1121,21 @@ authRouter.post(
|
|||||||
auth.login
|
auth.login
|
||||||
);
|
);
|
||||||
authRouter.post("/logout", auth.logout);
|
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(
|
authRouter.post(
|
||||||
"/newt/get-token",
|
"/newt/get-token",
|
||||||
rateLimit({
|
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, +, /, =)
|
// 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);
|
const exitNode = await createExitNode(cleanedPublicKey, reachableAt);
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ const bodySchema = z.strictObject({
|
|||||||
emailPath: z.string().optional(),
|
emailPath: z.string().optional(),
|
||||||
namePath: z.string().optional(),
|
namePath: z.string().optional(),
|
||||||
scopes: z.string().nonempty(),
|
scopes: z.string().nonempty(),
|
||||||
autoProvision: z.boolean().optional()
|
autoProvision: z.boolean().optional(),
|
||||||
|
tags: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreateIdpResponse = {
|
export type CreateIdpResponse = {
|
||||||
@@ -75,7 +76,8 @@ export async function createOidcIdp(
|
|||||||
emailPath,
|
emailPath,
|
||||||
namePath,
|
namePath,
|
||||||
name,
|
name,
|
||||||
autoProvision
|
autoProvision,
|
||||||
|
tags
|
||||||
} = parsedBody.data;
|
} = parsedBody.data;
|
||||||
|
|
||||||
const key = config.getRawConfig().server.secret!;
|
const key = config.getRawConfig().server.secret!;
|
||||||
@@ -90,7 +92,8 @@ export async function createOidcIdp(
|
|||||||
.values({
|
.values({
|
||||||
name,
|
name,
|
||||||
autoProvision,
|
autoProvision,
|
||||||
type: "oidc"
|
type: "oidc",
|
||||||
|
tags
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user