mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-25 22:36:38 +00:00
Compare commits
33 Commits
1.15.4-s.8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba3ab4362b | ||
|
|
a9b4a86c4a | ||
|
|
200ea502dd | ||
|
|
de36db97eb | ||
|
|
30283b044f | ||
|
|
055bed8a07 | ||
|
|
12b5c2ab34 | ||
|
|
dd78674888 | ||
|
|
0d0df63847 | ||
|
|
3ab00d9da8 | ||
|
|
3e6e72c5c7 | ||
|
|
5d8a55f08c | ||
|
|
81c569aae4 | ||
|
|
88fd3fc4da | ||
|
|
2ede0d498a | ||
|
|
978ac8f53c | ||
|
|
49a326cde7 | ||
|
|
63e208f4ec | ||
|
|
f50d1549b0 | ||
|
|
55e24df671 | ||
|
|
b37e1d0cc0 | ||
|
|
afa26c0dd4 | ||
|
|
2edebaddc2 | ||
|
|
119e1d4867 | ||
|
|
63e30d3378 | ||
|
|
d6fe04ec4e | ||
|
|
720d3a8135 | ||
|
|
9c42458fa5 | ||
|
|
bcd3475d17 | ||
|
|
d8b45396e3 | ||
|
|
971c375398 | ||
|
|
ac4439c5ae | ||
|
|
8c15855fc3 |
@@ -28,9 +28,9 @@ LICENSE
|
|||||||
CONTRIBUTING.md
|
CONTRIBUTING.md
|
||||||
dist
|
dist
|
||||||
.git
|
.git
|
||||||
migrations/
|
server/migrations/
|
||||||
config/
|
config/
|
||||||
build.ts
|
build.ts
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
Dockerfile*
|
Dockerfile*
|
||||||
migrations/
|
drizzle.config.ts
|
||||||
|
|||||||
20
.github/workflows/cicd.yml
vendored
20
.github/workflows/cicd.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v5
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Monitor storage space
|
- name: Monitor storage space
|
||||||
run: |
|
run: |
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Monitor storage space
|
- name: Monitor storage space
|
||||||
run: |
|
run: |
|
||||||
@@ -149,7 +149,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
@@ -201,10 +201,10 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
@@ -256,7 +256,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Extract tag name
|
- name: Extract tag name
|
||||||
id: get-tag
|
id: get-tag
|
||||||
@@ -415,7 +415,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry (for cosign)
|
- name: Login to GitHub Container Registry (for cosign)
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -578,7 +578,7 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v5
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
|
|||||||
2
.github/workflows/linting.yml
vendored
2
.github/workflows/linting.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||||
|
|||||||
2
.github/workflows/restart-runners.yml
vendored
2
.github/workflows/restart-runners.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v5
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
|
|||||||
8
.github/workflows/saas.yml
vendored
8
.github/workflows/saas.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v5
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Download MaxMind GeoLite2 databases
|
- name: Download MaxMind GeoLite2 databases
|
||||||
env:
|
env:
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v5
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
@@ -145,7 +145,7 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v5
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
|
|||||||
2
.github/workflows/stale-bot.yml
vendored
2
.github/workflows/stale-bot.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||||
with:
|
with:
|
||||||
days-before-stale: 14
|
days-before-stale: 14
|
||||||
days-before-close: 14
|
days-before-close: 14
|
||||||
|
|||||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Build Docker image sqlite
|
- name: Build Docker image sqlite
|
||||||
run: make dev-build-sqlite
|
run: make dev-build-sqlite
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Build Docker image pg
|
- name: Build Docker image pg
|
||||||
run: make dev-build-pg
|
run: make dev-build-pg
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
FROM node:24-alpine AS base
|
FROM node:24-slim AS base
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache python3 make g++
|
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
@@ -27,11 +27,11 @@ FROM base AS builder
|
|||||||
|
|
||||||
RUN npm ci --omit=dev
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
FROM node:24-alpine AS runner
|
FROM node:24-slim AS runner
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache curl tzdata
|
RUN apt-get update && apt-get install -y curl tzdata && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY --from=builder /app/node_modules ./node_modules
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
COPY --from=builder /app/package.json ./package.json
|
COPY --from=builder /app/package.json ./package.json
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ module installer
|
|||||||
go 1.24.0
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/term v0.39.0
|
golang.org/x/term v0.40.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/sys v0.40.0 // indirect
|
require golang.org/x/sys v0.41.0 // indirect
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
511
package-lock.json
generated
511
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -32,7 +32,7 @@
|
|||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asteasolutions/zod-to-openapi": "8.4.0",
|
"@asteasolutions/zod-to-openapi": "8.4.1",
|
||||||
"@aws-sdk/client-s3": "3.989.0",
|
"@aws-sdk/client-s3": "3.989.0",
|
||||||
"@faker-js/faker": "10.3.0",
|
"@faker-js/faker": "10.3.0",
|
||||||
"@headlessui/react": "2.2.9",
|
"@headlessui/react": "2.2.9",
|
||||||
@@ -59,11 +59,11 @@
|
|||||||
"@radix-ui/react-tabs": "1.1.13",
|
"@radix-ui/react-tabs": "1.1.13",
|
||||||
"@radix-ui/react-toast": "1.2.15",
|
"@radix-ui/react-toast": "1.2.15",
|
||||||
"@radix-ui/react-tooltip": "1.2.8",
|
"@radix-ui/react-tooltip": "1.2.8",
|
||||||
"@react-email/components": "1.0.7",
|
"@react-email/components": "1.0.8",
|
||||||
"@react-email/render": "2.0.4",
|
"@react-email/render": "2.0.4",
|
||||||
"@react-email/tailwind": "2.0.4",
|
"@react-email/tailwind": "2.0.5",
|
||||||
"@simplewebauthn/browser": "13.2.2",
|
"@simplewebauthn/browser": "13.2.2",
|
||||||
"@simplewebauthn/server": "13.2.2",
|
"@simplewebauthn/server": "13.2.3",
|
||||||
"@tailwindcss/forms": "0.5.11",
|
"@tailwindcss/forms": "0.5.11",
|
||||||
"@tanstack/react-query": "5.90.21",
|
"@tanstack/react-query": "5.90.21",
|
||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
"drizzle-orm": "0.45.1",
|
"drizzle-orm": "0.45.1",
|
||||||
"express": "5.2.1",
|
"express": "5.2.1",
|
||||||
"express-rate-limit": "8.2.1",
|
"express-rate-limit": "8.2.1",
|
||||||
"glob": "13.0.3",
|
"glob": "13.0.6",
|
||||||
"helmet": "8.1.0",
|
"helmet": "8.1.0",
|
||||||
"http-errors": "2.0.1",
|
"http-errors": "2.0.1",
|
||||||
"input-otp": "1.4.2",
|
"input-otp": "1.4.2",
|
||||||
@@ -93,20 +93,20 @@
|
|||||||
"maxmind": "5.0.5",
|
"maxmind": "5.0.5",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.5.12",
|
"next": "15.5.12",
|
||||||
"next-intl": "4.8.2",
|
"next-intl": "4.8.3",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"nextjs-toploader": "3.9.17",
|
"nextjs-toploader": "3.9.17",
|
||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
"nodemailer": "8.0.1",
|
"nodemailer": "8.0.1",
|
||||||
"oslo": "1.2.1",
|
"oslo": "1.2.1",
|
||||||
"pg": "8.18.0",
|
"pg": "8.19.0",
|
||||||
"posthog-node": "5.24.15",
|
"posthog-node": "5.26.0",
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-day-picker": "9.13.2",
|
"react-day-picker": "9.13.2",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"react-easy-sort": "1.8.0",
|
"react-easy-sort": "1.8.0",
|
||||||
"react-hook-form": "7.71.1",
|
"react-hook-form": "7.71.2",
|
||||||
"react-icons": "5.5.0",
|
"react-icons": "5.5.0",
|
||||||
"recharts": "2.15.4",
|
"recharts": "2.15.4",
|
||||||
"reodotdev": "1.0.0",
|
"reodotdev": "1.0.0",
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
"sshpk": "^1.18.0",
|
"sshpk": "^1.18.0",
|
||||||
"stripe": "20.3.1",
|
"stripe": "20.3.1",
|
||||||
"swagger-ui-express": "5.0.1",
|
"swagger-ui-express": "5.0.1",
|
||||||
"tailwind-merge": "3.4.0",
|
"tailwind-merge": "3.5.0",
|
||||||
"topojson-client": "3.1.0",
|
"topojson-client": "3.1.0",
|
||||||
"tw-animate-css": "1.4.0",
|
"tw-animate-css": "1.4.0",
|
||||||
"use-debounce": "^10.1.0",
|
"use-debounce": "^10.1.0",
|
||||||
@@ -147,7 +147,7 @@
|
|||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsonwebtoken": "9.0.10",
|
"@types/jsonwebtoken": "9.0.10",
|
||||||
"@types/node": "25.2.3",
|
"@types/node": "25.2.3",
|
||||||
"@types/nodemailer": "7.0.9",
|
"@types/nodemailer": "7.0.11",
|
||||||
"@types/nprogress": "0.2.3",
|
"@types/nprogress": "0.2.3",
|
||||||
"@types/pg": "8.16.0",
|
"@types/pg": "8.16.0",
|
||||||
"@types/react": "19.2.14",
|
"@types/react": "19.2.14",
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ const internalPort = config.getRawConfig().server.internal_port;
|
|||||||
export function createInternalServer() {
|
export function createInternalServer() {
|
||||||
const internalServer = express();
|
const internalServer = express();
|
||||||
|
|
||||||
|
const trustProxy = config.getRawConfig().server.trust_proxy;
|
||||||
|
if (trustProxy) {
|
||||||
|
internalServer.set("trust proxy", trustProxy);
|
||||||
|
}
|
||||||
|
|
||||||
internalServer.use(helmet());
|
internalServer.use(helmet());
|
||||||
internalServer.use(cors());
|
internalServer.use(cors());
|
||||||
internalServer.use(stripDuplicateSesions);
|
internalServer.use(stripDuplicateSesions);
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ export class UsageService {
|
|||||||
const orgIdToUse = await this.getBillingOrg(orgId);
|
const orgIdToUse = await this.getBillingOrg(orgId);
|
||||||
|
|
||||||
const cacheKey = `customer_${orgIdToUse}_${featureId}`;
|
const cacheKey = `customer_${orgIdToUse}_${featureId}`;
|
||||||
const cached = cache.get<string>(cacheKey);
|
const cached = await cache.get<string>(cacheKey);
|
||||||
|
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return cached;
|
return cached;
|
||||||
@@ -253,7 +253,7 @@ export class UsageService {
|
|||||||
const customerId = customer.customerId;
|
const customerId = customer.customerId;
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
cache.set(cacheKey, customerId, 300); // 5 minute TTL
|
await cache.set(cacheKey, customerId, 300); // 5 minute TTL
|
||||||
|
|
||||||
return customerId;
|
return customerId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import NodeCache from "node-cache";
|
import NodeCache from "node-cache";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
import { redisManager } from "@server/private/lib/redis";
|
||||||
|
|
||||||
// Create cache with maxKeys limit to prevent memory leaks
|
// Create local cache with maxKeys limit to prevent memory leaks
|
||||||
// With ~10k requests/day and 5min TTL, 10k keys should be more than sufficient
|
// With ~10k requests/day and 5min TTL, 10k keys should be more than sufficient
|
||||||
export const cache = new NodeCache({
|
export const localCache = new NodeCache({
|
||||||
stdTTL: 3600,
|
stdTTL: 3600,
|
||||||
checkperiod: 120,
|
checkperiod: 120,
|
||||||
maxKeys: 10000
|
maxKeys: 10000
|
||||||
@@ -11,10 +12,255 @@ export const cache = new NodeCache({
|
|||||||
|
|
||||||
// Log cache statistics periodically for monitoring
|
// Log cache statistics periodically for monitoring
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const stats = cache.getStats();
|
const stats = localCache.getStats();
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Cache stats - Keys: ${stats.keys}, Hits: ${stats.hits}, Misses: ${stats.misses}, Hit rate: ${stats.hits > 0 ? ((stats.hits / (stats.hits + stats.misses)) * 100).toFixed(2) : 0}%`
|
`Local cache stats - Keys: ${stats.keys}, Hits: ${stats.hits}, Misses: ${stats.misses}, Hit rate: ${stats.hits > 0 ? ((stats.hits / (stats.hits + stats.misses)) * 100).toFixed(2) : 0}%`
|
||||||
);
|
);
|
||||||
}, 300000); // Every 5 minutes
|
}, 300000); // Every 5 minutes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adaptive cache that uses Redis when available in multi-node environments,
|
||||||
|
* otherwise falls back to local memory cache for single-node deployments.
|
||||||
|
*/
|
||||||
|
class AdaptiveCache {
|
||||||
|
private useRedis(): boolean {
|
||||||
|
return redisManager.isRedisEnabled() && redisManager.getHealthStatus().isHealthy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a value in the cache
|
||||||
|
* @param key - Cache key
|
||||||
|
* @param value - Value to cache (will be JSON stringified for Redis)
|
||||||
|
* @param ttl - Time to live in seconds (0 = no expiration)
|
||||||
|
* @returns boolean indicating success
|
||||||
|
*/
|
||||||
|
async set(key: string, value: any, ttl?: number): Promise<boolean> {
|
||||||
|
const effectiveTtl = ttl === 0 ? undefined : ttl;
|
||||||
|
|
||||||
|
if (this.useRedis()) {
|
||||||
|
try {
|
||||||
|
const serialized = JSON.stringify(value);
|
||||||
|
const success = await redisManager.set(key, serialized, effectiveTtl);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
logger.debug(`Set key in Redis: ${key}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redis failed, fall through to local cache
|
||||||
|
logger.debug(`Redis set failed for key ${key}, falling back to local cache`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Redis set error for key ${key}:`, error);
|
||||||
|
// Fall through to local cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use local cache as fallback or primary
|
||||||
|
const success = localCache.set(key, value, effectiveTtl || 0);
|
||||||
|
if (success) {
|
||||||
|
logger.debug(`Set key in local cache: ${key}`);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a value from the cache
|
||||||
|
* @param key - Cache key
|
||||||
|
* @returns The cached value or undefined if not found
|
||||||
|
*/
|
||||||
|
async get<T = any>(key: string): Promise<T | undefined> {
|
||||||
|
if (this.useRedis()) {
|
||||||
|
try {
|
||||||
|
const value = await redisManager.get(key);
|
||||||
|
|
||||||
|
if (value !== null) {
|
||||||
|
logger.debug(`Cache hit in Redis: ${key}`);
|
||||||
|
return JSON.parse(value) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Cache miss in Redis: ${key}`);
|
||||||
|
return undefined;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Redis get error for key ${key}:`, error);
|
||||||
|
// Fall through to local cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use local cache as fallback or primary
|
||||||
|
const value = localCache.get<T>(key);
|
||||||
|
if (value !== undefined) {
|
||||||
|
logger.debug(`Cache hit in local cache: ${key}`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`Cache miss in local cache: ${key}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a value from the cache
|
||||||
|
* @param key - Cache key or array of keys
|
||||||
|
* @returns Number of deleted entries
|
||||||
|
*/
|
||||||
|
async del(key: string | string[]): Promise<number> {
|
||||||
|
const keys = Array.isArray(key) ? key : [key];
|
||||||
|
let deletedCount = 0;
|
||||||
|
|
||||||
|
if (this.useRedis()) {
|
||||||
|
try {
|
||||||
|
for (const k of keys) {
|
||||||
|
const success = await redisManager.del(k);
|
||||||
|
if (success) {
|
||||||
|
deletedCount++;
|
||||||
|
logger.debug(`Deleted key from Redis: ${k}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deletedCount === keys.length) {
|
||||||
|
return deletedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some Redis deletes failed, fall through to local cache
|
||||||
|
logger.debug(`Some Redis deletes failed, falling back to local cache`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Redis del error for keys ${keys.join(", ")}:`, error);
|
||||||
|
// Fall through to local cache
|
||||||
|
deletedCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use local cache as fallback or primary
|
||||||
|
for (const k of keys) {
|
||||||
|
const success = localCache.del(k);
|
||||||
|
if (success > 0) {
|
||||||
|
deletedCount++;
|
||||||
|
logger.debug(`Deleted key from local cache: ${k}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a key exists in the cache
|
||||||
|
* @param key - Cache key
|
||||||
|
* @returns boolean indicating if key exists
|
||||||
|
*/
|
||||||
|
async has(key: string): Promise<boolean> {
|
||||||
|
if (this.useRedis()) {
|
||||||
|
try {
|
||||||
|
const value = await redisManager.get(key);
|
||||||
|
return value !== null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Redis has error for key ${key}:`, error);
|
||||||
|
// Fall through to local cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use local cache as fallback or primary
|
||||||
|
return localCache.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get multiple values from the cache
|
||||||
|
* @param keys - Array of cache keys
|
||||||
|
* @returns Array of values (undefined for missing keys)
|
||||||
|
*/
|
||||||
|
async mget<T = any>(keys: string[]): Promise<(T | undefined)[]> {
|
||||||
|
if (this.useRedis()) {
|
||||||
|
try {
|
||||||
|
const results: (T | undefined)[] = [];
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = await redisManager.get(key);
|
||||||
|
if (value !== null) {
|
||||||
|
results.push(JSON.parse(value) as T);
|
||||||
|
} else {
|
||||||
|
results.push(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Redis mget error:`, error);
|
||||||
|
// Fall through to local cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use local cache as fallback or primary
|
||||||
|
return keys.map((key) => localCache.get<T>(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flush all keys from the cache
|
||||||
|
*/
|
||||||
|
async flushAll(): Promise<void> {
|
||||||
|
if (this.useRedis()) {
|
||||||
|
logger.warn("Adaptive cache flushAll called - Redis flush not implemented, only local cache will be flushed");
|
||||||
|
}
|
||||||
|
|
||||||
|
localCache.flushAll();
|
||||||
|
logger.debug("Flushed local cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache statistics
|
||||||
|
* Note: Only returns local cache stats, Redis stats are not included
|
||||||
|
*/
|
||||||
|
getStats() {
|
||||||
|
return localCache.getStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current cache backend being used
|
||||||
|
* @returns "redis" if Redis is available and healthy, "local" otherwise
|
||||||
|
*/
|
||||||
|
getCurrentBackend(): "redis" | "local" {
|
||||||
|
return this.useRedis() ? "redis" : "local";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take a key from the cache and delete it
|
||||||
|
* @param key - Cache key
|
||||||
|
* @returns The value or undefined if not found
|
||||||
|
*/
|
||||||
|
async take<T = any>(key: string): Promise<T | undefined> {
|
||||||
|
const value = await this.get<T>(key);
|
||||||
|
if (value !== undefined) {
|
||||||
|
await this.del(key);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get TTL (time to live) for a key
|
||||||
|
* @param key - Cache key
|
||||||
|
* @returns TTL in seconds, 0 if no expiration, -1 if key doesn't exist
|
||||||
|
*/
|
||||||
|
getTtl(key: string): number {
|
||||||
|
// Note: This only works for local cache, Redis TTL is not supported
|
||||||
|
if (this.useRedis()) {
|
||||||
|
logger.warn(`getTtl called for key ${key} but Redis TTL lookup is not implemented`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ttl = localCache.getTtl(key);
|
||||||
|
if (ttl === undefined) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return Math.max(0, Math.floor((ttl - Date.now()) / 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all keys from the cache
|
||||||
|
* Note: Only returns local cache keys, Redis keys are not included
|
||||||
|
*/
|
||||||
|
keys(): string[] {
|
||||||
|
if (this.useRedis()) {
|
||||||
|
logger.warn("keys() called but Redis keys are not included, only local cache keys returned");
|
||||||
|
}
|
||||||
|
return localCache.keys();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export const cache = new AdaptiveCache();
|
||||||
export default cache;
|
export default cache;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export async function getValidCertificatesForDomains(
|
|||||||
if (useCache) {
|
if (useCache) {
|
||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
const cacheKey = `cert:${domain}`;
|
const cacheKey = `cert:${domain}`;
|
||||||
const cachedCert = cache.get<CertificateResult>(cacheKey);
|
const cachedCert = await cache.get<CertificateResult>(cacheKey);
|
||||||
if (cachedCert) {
|
if (cachedCert) {
|
||||||
finalResults.push(cachedCert); // Valid cache hit
|
finalResults.push(cachedCert); // Valid cache hit
|
||||||
} else {
|
} else {
|
||||||
@@ -169,7 +169,7 @@ export async function getValidCertificatesForDomains(
|
|||||||
// Add to cache for future requests, using the *requested domain* as the key
|
// Add to cache for future requests, using the *requested domain* as the key
|
||||||
if (useCache) {
|
if (useCache) {
|
||||||
const cacheKey = `cert:${domain}`;
|
const cacheKey = `cert:${domain}`;
|
||||||
cache.set(cacheKey, resultCert, 180);
|
await cache.set(cacheKey, resultCert, 180);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ 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
|
||||||
const cached = cache.get<number>(`org_${orgId}_accessDays`);
|
const cached = await cache.get<number>(`org_${orgId}_accessDays`);
|
||||||
if (cached !== undefined) {
|
if (cached !== undefined) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ async function getAccessDays(orgId: string): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// store the result in cache
|
// store the result in cache
|
||||||
cache.set(
|
await cache.set(
|
||||||
`org_${orgId}_accessDays`,
|
`org_${orgId}_accessDays`,
|
||||||
org.settingsLogRetentionDaysAction,
|
org.settingsLogRetentionDaysAction,
|
||||||
300
|
300
|
||||||
@@ -146,14 +146,14 @@ export async function logAccessAudit(data: {
|
|||||||
async function getCountryCodeFromIp(ip: string): Promise<string | undefined> {
|
async function getCountryCodeFromIp(ip: string): Promise<string | undefined> {
|
||||||
const geoIpCacheKey = `geoip_access:${ip}`;
|
const geoIpCacheKey = `geoip_access:${ip}`;
|
||||||
|
|
||||||
let cachedCountryCode: string | undefined = cache.get(geoIpCacheKey);
|
let cachedCountryCode: string | undefined = await cache.get(geoIpCacheKey);
|
||||||
|
|
||||||
if (!cachedCountryCode) {
|
if (!cachedCountryCode) {
|
||||||
cachedCountryCode = await getCountryCodeForIp(ip); // do it locally
|
cachedCountryCode = await getCountryCodeForIp(ip); // do it locally
|
||||||
// Only cache successful lookups to avoid filling cache with undefined values
|
// Only cache successful lookups to avoid filling cache with undefined values
|
||||||
if (cachedCountryCode) {
|
if (cachedCountryCode) {
|
||||||
// Cache for longer since IP geolocation doesn't change frequently
|
// Cache for longer since IP geolocation doesn't change frequently
|
||||||
cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes
|
await cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs";
|
|||||||
|
|
||||||
async function getActionDays(orgId: string): Promise<number> {
|
async function getActionDays(orgId: string): Promise<number> {
|
||||||
// check cache first
|
// check cache first
|
||||||
const cached = cache.get<number>(`org_${orgId}_actionDays`);
|
const cached = await cache.get<number>(`org_${orgId}_actionDays`);
|
||||||
if (cached !== undefined) {
|
if (cached !== undefined) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ async function getActionDays(orgId: string): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// store the result in cache
|
// store the result in cache
|
||||||
cache.set(
|
await cache.set(
|
||||||
`org_${orgId}_actionDays`,
|
`org_${orgId}_actionDays`,
|
||||||
org.settingsLogRetentionDaysAction,
|
org.settingsLogRetentionDaysAction,
|
||||||
300
|
300
|
||||||
|
|||||||
@@ -480,9 +480,9 @@ 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(tierMatrix.rotateCredentials),
|
verifyValidSubscription(tierMatrix.rotateCredentials),
|
||||||
verifyClientAccess, // this is first to set the org id
|
|
||||||
verifyLimits,
|
verifyLimits,
|
||||||
verifyUserHasAction(ActionsEnum.reGenerateSecret),
|
verifyUserHasAction(ActionsEnum.reGenerateSecret),
|
||||||
reKey.reGenerateClientSecret
|
reKey.reGenerateClientSecret
|
||||||
@@ -490,9 +490,9 @@ authenticated.post(
|
|||||||
|
|
||||||
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(tierMatrix.rotateCredentials),
|
verifyValidSubscription(tierMatrix.rotateCredentials),
|
||||||
verifySiteAccess, // this is first to set the org id
|
|
||||||
verifyLimits,
|
verifyLimits,
|
||||||
verifyUserHasAction(ActionsEnum.reGenerateSecret),
|
verifyUserHasAction(ActionsEnum.reGenerateSecret),
|
||||||
reKey.reGenerateSiteSecret
|
reKey.reGenerateSiteSecret
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export async function shutdownAuditLogger() {
|
|||||||
|
|
||||||
async function getRetentionDays(orgId: string): Promise<number> {
|
async function getRetentionDays(orgId: string): Promise<number> {
|
||||||
// check cache first
|
// check cache first
|
||||||
const cached = cache.get<number>(`org_${orgId}_retentionDays`);
|
const cached = await cache.get<number>(`org_${orgId}_retentionDays`);
|
||||||
if (cached !== undefined) {
|
if (cached !== undefined) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
@@ -149,7 +149,7 @@ async function getRetentionDays(orgId: string): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// store the result in cache
|
// store the result in cache
|
||||||
cache.set(
|
await cache.set(
|
||||||
`org_${orgId}_retentionDays`,
|
`org_${orgId}_retentionDays`,
|
||||||
org.settingsLogRetentionDaysRequest,
|
org.settingsLogRetentionDaysRequest,
|
||||||
300
|
300
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
enforceResourceSessionLength
|
enforceResourceSessionLength
|
||||||
} from "#dynamic/lib/checkOrgAccessPolicy";
|
} from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
import { logRequestAudit } from "./logRequestAudit";
|
import { logRequestAudit } from "./logRequestAudit";
|
||||||
import cache from "@server/lib/cache";
|
import { localCache } from "@server/lib/cache";
|
||||||
import { APP_VERSION } from "@server/lib/consts";
|
import { APP_VERSION } from "@server/lib/consts";
|
||||||
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
@@ -137,7 +137,7 @@ export async function verifyResourceSession(
|
|||||||
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
||||||
org: Org;
|
org: Org;
|
||||||
}
|
}
|
||||||
| undefined = cache.get(resourceCacheKey);
|
| undefined = localCache.get(resourceCacheKey);
|
||||||
|
|
||||||
if (!resourceData) {
|
if (!resourceData) {
|
||||||
const result = await getResourceByDomain(cleanHost);
|
const result = await getResourceByDomain(cleanHost);
|
||||||
@@ -161,7 +161,7 @@ export async function verifyResourceSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
resourceData = result;
|
resourceData = result;
|
||||||
cache.set(resourceCacheKey, resourceData, 5);
|
localCache.set(resourceCacheKey, resourceData, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -405,7 +405,7 @@ export async function verifyResourceSession(
|
|||||||
// check for HTTP Basic Auth header
|
// check for HTTP Basic Auth header
|
||||||
const clientHeaderAuthKey = `headerAuth:${clientHeaderAuth}`;
|
const clientHeaderAuthKey = `headerAuth:${clientHeaderAuth}`;
|
||||||
if (headerAuth && clientHeaderAuth) {
|
if (headerAuth && clientHeaderAuth) {
|
||||||
if (cache.get(clientHeaderAuthKey)) {
|
if (localCache.get(clientHeaderAuthKey)) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Resource allowed because header auth is valid (cached)"
|
"Resource allowed because header auth is valid (cached)"
|
||||||
);
|
);
|
||||||
@@ -428,7 +428,7 @@ export async function verifyResourceSession(
|
|||||||
headerAuth.headerAuthHash
|
headerAuth.headerAuthHash
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
cache.set(clientHeaderAuthKey, clientHeaderAuth, 5);
|
localCache.set(clientHeaderAuthKey, clientHeaderAuth, 5);
|
||||||
logger.debug("Resource allowed because header auth is valid");
|
logger.debug("Resource allowed because header auth is valid");
|
||||||
|
|
||||||
logRequestAudit(
|
logRequestAudit(
|
||||||
@@ -520,7 +520,7 @@ export async function verifyResourceSession(
|
|||||||
|
|
||||||
if (resourceSessionToken) {
|
if (resourceSessionToken) {
|
||||||
const sessionCacheKey = `session:${resourceSessionToken}`;
|
const sessionCacheKey = `session:${resourceSessionToken}`;
|
||||||
let resourceSession: any = cache.get(sessionCacheKey);
|
let resourceSession: any = localCache.get(sessionCacheKey);
|
||||||
|
|
||||||
if (!resourceSession) {
|
if (!resourceSession) {
|
||||||
const result = await validateResourceSessionToken(
|
const result = await validateResourceSessionToken(
|
||||||
@@ -529,7 +529,7 @@ export async function verifyResourceSession(
|
|||||||
);
|
);
|
||||||
|
|
||||||
resourceSession = result?.resourceSession;
|
resourceSession = result?.resourceSession;
|
||||||
cache.set(sessionCacheKey, resourceSession, 5);
|
localCache.set(sessionCacheKey, resourceSession, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resourceSession?.isRequestToken) {
|
if (resourceSession?.isRequestToken) {
|
||||||
@@ -662,7 +662,7 @@ export async function verifyResourceSession(
|
|||||||
}:${resource.resourceId}`;
|
}:${resource.resourceId}`;
|
||||||
|
|
||||||
let allowedUserData: BasicUserData | null | undefined =
|
let allowedUserData: BasicUserData | null | undefined =
|
||||||
cache.get(userAccessCacheKey);
|
localCache.get(userAccessCacheKey);
|
||||||
|
|
||||||
if (allowedUserData === undefined) {
|
if (allowedUserData === undefined) {
|
||||||
allowedUserData = await isUserAllowedToAccessResource(
|
allowedUserData = await isUserAllowedToAccessResource(
|
||||||
@@ -671,7 +671,7 @@ export async function verifyResourceSession(
|
|||||||
resourceData.org
|
resourceData.org
|
||||||
);
|
);
|
||||||
|
|
||||||
cache.set(userAccessCacheKey, allowedUserData, 5);
|
localCache.set(userAccessCacheKey, allowedUserData, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -974,11 +974,11 @@ async function checkRules(
|
|||||||
): Promise<"ACCEPT" | "DROP" | "PASS" | undefined> {
|
): Promise<"ACCEPT" | "DROP" | "PASS" | undefined> {
|
||||||
const ruleCacheKey = `rules:${resourceId}`;
|
const ruleCacheKey = `rules:${resourceId}`;
|
||||||
|
|
||||||
let rules: ResourceRule[] | undefined = cache.get(ruleCacheKey);
|
let rules: ResourceRule[] | undefined = localCache.get(ruleCacheKey);
|
||||||
|
|
||||||
if (!rules) {
|
if (!rules) {
|
||||||
rules = await getResourceRules(resourceId);
|
rules = await getResourceRules(resourceId);
|
||||||
cache.set(ruleCacheKey, rules, 5);
|
localCache.set(ruleCacheKey, rules, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rules.length === 0) {
|
if (rules.length === 0) {
|
||||||
@@ -1208,13 +1208,13 @@ async function isIpInAsn(
|
|||||||
async function getAsnFromIp(ip: string): Promise<number | undefined> {
|
async function getAsnFromIp(ip: string): Promise<number | undefined> {
|
||||||
const asnCacheKey = `asn:${ip}`;
|
const asnCacheKey = `asn:${ip}`;
|
||||||
|
|
||||||
let cachedAsn: number | undefined = cache.get(asnCacheKey);
|
let cachedAsn: number | undefined = localCache.get(asnCacheKey);
|
||||||
|
|
||||||
if (!cachedAsn) {
|
if (!cachedAsn) {
|
||||||
cachedAsn = await getAsnForIp(ip); // do it locally
|
cachedAsn = await getAsnForIp(ip); // do it locally
|
||||||
// Cache for longer since IP ASN doesn't change frequently
|
// Cache for longer since IP ASN doesn't change frequently
|
||||||
if (cachedAsn) {
|
if (cachedAsn) {
|
||||||
cache.set(asnCacheKey, cachedAsn, 300); // 5 minutes
|
localCache.set(asnCacheKey, cachedAsn, 300); // 5 minutes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1224,14 +1224,14 @@ async function getAsnFromIp(ip: string): Promise<number | undefined> {
|
|||||||
async function getCountryCodeFromIp(ip: string): Promise<string | undefined> {
|
async function getCountryCodeFromIp(ip: string): Promise<string | undefined> {
|
||||||
const geoIpCacheKey = `geoip:${ip}`;
|
const geoIpCacheKey = `geoip:${ip}`;
|
||||||
|
|
||||||
let cachedCountryCode: string | undefined = cache.get(geoIpCacheKey);
|
let cachedCountryCode: string | undefined = localCache.get(geoIpCacheKey);
|
||||||
|
|
||||||
if (!cachedCountryCode) {
|
if (!cachedCountryCode) {
|
||||||
cachedCountryCode = await getCountryCodeForIp(ip); // do it locally
|
cachedCountryCode = await getCountryCodeForIp(ip); // do it locally
|
||||||
// Only cache successful lookups to avoid filling cache with undefined values
|
// Only cache successful lookups to avoid filling cache with undefined values
|
||||||
if (cachedCountryCode) {
|
if (cachedCountryCode) {
|
||||||
// Cache for longer since IP geolocation doesn't change frequently
|
// Cache for longer since IP geolocation doesn't change frequently
|
||||||
cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes
|
localCache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export const handleDockerStatusMessage: MessageHandler = async (context) => {
|
|||||||
|
|
||||||
if (available) {
|
if (available) {
|
||||||
logger.info(`Newt ${newt.newtId} has Docker socket access`);
|
logger.info(`Newt ${newt.newtId} has Docker socket access`);
|
||||||
cache.set(`${newt.newtId}:socketPath`, socketPath, 0);
|
await cache.set(`${newt.newtId}:socketPath`, socketPath, 0);
|
||||||
cache.set(`${newt.newtId}:isAvailable`, available, 0);
|
await cache.set(`${newt.newtId}:isAvailable`, available, 0);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`Newt ${newt.newtId} does not have Docker socket access`);
|
logger.warn(`Newt ${newt.newtId} does not have Docker socket access`);
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ export const handleDockerContainersMessage: MessageHandler = async (
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (containers && containers.length > 0) {
|
if (containers && containers.length > 0) {
|
||||||
cache.set(`${newt.newtId}:dockerContainers`, containers, 0);
|
await cache.set(`${newt.newtId}:dockerContainers`, containers, 0);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`Newt ${newt.newtId} does not have Docker containers`);
|
logger.warn(`Newt ${newt.newtId} does not have Docker containers`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
import {
|
||||||
|
generateSessionToken,
|
||||||
|
validateSessionToken
|
||||||
|
} from "@server/auth/sessions/app";
|
||||||
import {
|
import {
|
||||||
clients,
|
clients,
|
||||||
db,
|
db,
|
||||||
@@ -26,8 +29,9 @@ import { APP_VERSION } from "@server/lib/consts";
|
|||||||
|
|
||||||
export const olmGetTokenBodySchema = z.object({
|
export const olmGetTokenBodySchema = z.object({
|
||||||
olmId: z.string(),
|
olmId: z.string(),
|
||||||
secret: z.string(),
|
secret: z.string().optional(),
|
||||||
token: z.string().optional(),
|
userToken: z.string().optional(),
|
||||||
|
token: z.string().optional(), // this is the olm token
|
||||||
orgId: z.string().optional()
|
orgId: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,7 +53,7 @@ export async function getOlmToken(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { olmId, secret, token, orgId } = parsedBody.data;
|
const { olmId, secret, token, orgId, userToken } = parsedBody.data;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (token) {
|
if (token) {
|
||||||
@@ -84,19 +88,45 @@ export async function getOlmToken(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const validSecret = await verifyPassword(
|
if (userToken) {
|
||||||
secret,
|
const { session: userSession, user } =
|
||||||
existingOlm.secretHash
|
await validateSessionToken(userToken);
|
||||||
);
|
if (!userSession || !user) {
|
||||||
|
return next(
|
||||||
if (!validSecret) {
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid user token")
|
||||||
if (config.getRawConfig().app.log_failed_attempts) {
|
|
||||||
logger.info(
|
|
||||||
`Olm id or secret is incorrect. Olm: ID ${olmId}. IP: ${req.ip}.`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (user.userId !== existingOlm.userId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"User token does not match olm"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (secret) {
|
||||||
|
// this is for backward compatibility, we want to move towards userToken but some old clients may still be using secret so we will support both for now
|
||||||
|
const validSecret = await verifyPassword(
|
||||||
|
secret,
|
||||||
|
existingOlm.secretHash
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!validSecret) {
|
||||||
|
if (config.getRawConfig().app.log_failed_attempts) {
|
||||||
|
logger.info(
|
||||||
|
`Olm id or secret is incorrect. Olm: ID ${olmId}. IP: ${req.ip}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Secret is incorrect")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "Secret is incorrect")
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Either secret or userToken is required"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -194,9 +194,9 @@ export async function updateOrg(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// invalidate the cache for all of the orgs retention days
|
// invalidate the cache for all of the orgs retention days
|
||||||
cache.del(`org_${orgId}_retentionDays`);
|
await cache.del(`org_${orgId}_retentionDays`);
|
||||||
cache.del(`org_${orgId}_actionDays`);
|
await cache.del(`org_${orgId}_actionDays`);
|
||||||
cache.del(`org_${orgId}_accessDays`);
|
await cache.del(`org_${orgId}_accessDays`);
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: updatedOrg[0],
|
data: updatedOrg[0],
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { fromError } from "zod-validation-error";
|
|||||||
|
|
||||||
async function getLatestNewtVersion(): Promise<string | null> {
|
async function getLatestNewtVersion(): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const cachedVersion = cache.get<string>("latestNewtVersion");
|
const cachedVersion = await cache.get<string>("latestNewtVersion");
|
||||||
if (cachedVersion) {
|
if (cachedVersion) {
|
||||||
return cachedVersion;
|
return cachedVersion;
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ async function getLatestNewtVersion(): Promise<string | null> {
|
|||||||
tags = tags.filter((version) => !version.name.includes("rc"));
|
tags = tags.filter((version) => !version.name.includes("rc"));
|
||||||
const latestVersion = tags[0].name;
|
const latestVersion = tags[0].name;
|
||||||
|
|
||||||
cache.set("latestNewtVersion", latestVersion);
|
await cache.set("latestNewtVersion", latestVersion);
|
||||||
|
|
||||||
return latestVersion;
|
return latestVersion;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ async function triggerFetch(siteId: number) {
|
|||||||
|
|
||||||
// clear the cache for this Newt ID so that the site has to keep asking for the containers
|
// clear the cache for this Newt ID so that the site has to keep asking for the containers
|
||||||
// this is to ensure that the site always gets the latest data
|
// this is to ensure that the site always gets the latest data
|
||||||
cache.del(`${newt.newtId}:dockerContainers`);
|
await cache.del(`${newt.newtId}:dockerContainers`);
|
||||||
|
|
||||||
return { siteId, newtId: newt.newtId };
|
return { siteId, newtId: newt.newtId };
|
||||||
}
|
}
|
||||||
@@ -158,7 +158,7 @@ async function triggerFetch(siteId: number) {
|
|||||||
async function queryContainers(siteId: number) {
|
async function queryContainers(siteId: number) {
|
||||||
const { newt } = await getSiteAndNewt(siteId);
|
const { newt } = await getSiteAndNewt(siteId);
|
||||||
|
|
||||||
const result = cache.get(`${newt.newtId}:dockerContainers`) as Container[];
|
const result = await cache.get<Container[]>(`${newt.newtId}:dockerContainers`);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw createHttpError(
|
throw createHttpError(
|
||||||
HttpCode.TOO_EARLY,
|
HttpCode.TOO_EARLY,
|
||||||
@@ -173,7 +173,7 @@ async function isDockerAvailable(siteId: number): Promise<boolean> {
|
|||||||
const { newt } = await getSiteAndNewt(siteId);
|
const { newt } = await getSiteAndNewt(siteId);
|
||||||
|
|
||||||
const key = `${newt.newtId}:isAvailable`;
|
const key = `${newt.newtId}:isAvailable`;
|
||||||
const isAvailable = cache.get(key);
|
const isAvailable = await cache.get(key);
|
||||||
|
|
||||||
return !!isAvailable;
|
return !!isAvailable;
|
||||||
}
|
}
|
||||||
@@ -186,9 +186,11 @@ async function getDockerStatus(
|
|||||||
const keys = ["isAvailable", "socketPath"];
|
const keys = ["isAvailable", "socketPath"];
|
||||||
const mappedKeys = keys.map((x) => `${newt.newtId}:${x}`);
|
const mappedKeys = keys.map((x) => `${newt.newtId}:${x}`);
|
||||||
|
|
||||||
|
const values = await cache.mget<boolean | string>(mappedKeys);
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
isAvailable: cache.get(mappedKeys[0]) as boolean,
|
isAvailable: values[0] as boolean,
|
||||||
socketPath: cache.get(mappedKeys[1]) as string | undefined
|
socketPath: values[1] as string | undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export async function inviteUser(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (existingInvite.length) {
|
if (existingInvite.length) {
|
||||||
const attempts = cache.get<number>(email) || 0;
|
const attempts = (await cache.get<number>(email)) || 0;
|
||||||
if (attempts >= 3) {
|
if (attempts >= 3) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -201,7 +201,7 @@ export async function inviteUser(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
cache.set(email, attempts + 1);
|
await cache.set(email, attempts + 1);
|
||||||
|
|
||||||
const inviteId = existingInvite[0].inviteId; // Retrieve the original inviteId
|
const inviteId = existingInvite[0].inviteId; // Retrieve the original inviteId
|
||||||
const token = generateRandomString(
|
const token = generateRandomString(
|
||||||
|
|||||||
@@ -2,31 +2,20 @@ import { headers } from "next/headers";
|
|||||||
|
|
||||||
export async function authCookieHeader() {
|
export async function authCookieHeader() {
|
||||||
const otherHeaders = await headers();
|
const otherHeaders = await headers();
|
||||||
const otherHeadersObject = Object.fromEntries(otherHeaders.entries());
|
const otherHeadersObject = Object.fromEntries(
|
||||||
|
Array.from(otherHeaders.entries()).map(([k, v]) => [k.toLowerCase(), v])
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
headers: {
|
headers: {
|
||||||
cookie:
|
cookie: otherHeadersObject["cookie"],
|
||||||
otherHeadersObject["cookie"] || otherHeadersObject["Cookie"],
|
host: otherHeadersObject["host"],
|
||||||
host: otherHeadersObject["host"] || otherHeadersObject["Host"],
|
"user-agent": otherHeadersObject["user-agent"],
|
||||||
"user-agent":
|
"x-forwarded-for": otherHeadersObject["x-forwarded-for"],
|
||||||
otherHeadersObject["user-agent"] ||
|
"x-forwarded-host": otherHeadersObject["x-forwarded-host"],
|
||||||
otherHeadersObject["User-Agent"],
|
"x-forwarded-port": otherHeadersObject["x-forwarded-port"],
|
||||||
"x-forwarded-for":
|
"x-forwarded-proto": otherHeadersObject["x-forwarded-proto"],
|
||||||
otherHeadersObject["x-forwarded-for"] ||
|
"x-real-ip": otherHeadersObject["x-real-ip"]
|
||||||
otherHeadersObject["X-Forwarded-For"],
|
|
||||||
"x-forwarded-host":
|
|
||||||
otherHeadersObject["fx-forwarded-host"] ||
|
|
||||||
otherHeadersObject["Fx-Forwarded-Host"],
|
|
||||||
"x-forwarded-port":
|
|
||||||
otherHeadersObject["x-forwarded-port"] ||
|
|
||||||
otherHeadersObject["X-Forwarded-Port"],
|
|
||||||
"x-forwarded-proto":
|
|
||||||
otherHeadersObject["x-forwarded-proto"] ||
|
|
||||||
otherHeadersObject["X-Forwarded-Proto"],
|
|
||||||
"x-real-ip":
|
|
||||||
otherHeadersObject["x-real-ip"] ||
|
|
||||||
otherHeadersObject["X-Real-IP"]
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user