mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-09 03:16:37 +00:00
Compare commits
2 Commits
dependabot
...
breakout-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48abb9e98c | ||
|
|
28ef5238c9 |
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @oschwartz10612 @miloschwartz
|
||||
172
package-lock.json
generated
172
package-lock.json
generated
@@ -12,7 +12,7 @@
|
||||
"@asteasolutions/zod-to-openapi": "8.4.1",
|
||||
"@aws-sdk/client-s3": "3.1011.0",
|
||||
"@faker-js/faker": "10.3.0",
|
||||
"@headlessui/react": "2.2.10",
|
||||
"@headlessui/react": "2.2.9",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
"@node-rs/argon2": "2.0.2",
|
||||
@@ -36,9 +36,9 @@
|
||||
"@radix-ui/react-tabs": "1.1.13",
|
||||
"@radix-ui/react-toast": "1.2.15",
|
||||
"@radix-ui/react-tooltip": "1.2.8",
|
||||
"@react-email/components": "1.0.11",
|
||||
"@react-email/render": "2.0.5",
|
||||
"@react-email/tailwind": "2.0.7",
|
||||
"@react-email/components": "1.0.8",
|
||||
"@react-email/render": "2.0.4",
|
||||
"@react-email/tailwind": "2.0.5",
|
||||
"@simplewebauthn/browser": "13.3.0",
|
||||
"@simplewebauthn/server": "13.3.0",
|
||||
"@tailwindcss/forms": "0.5.11",
|
||||
@@ -55,33 +55,33 @@
|
||||
"cors": "2.8.6",
|
||||
"crypto-js": "4.2.0",
|
||||
"d3": "7.9.0",
|
||||
"drizzle-orm": "0.45.2",
|
||||
"drizzle-orm": "0.45.1",
|
||||
"express": "5.2.1",
|
||||
"express-rate-limit": "8.3.2",
|
||||
"express-rate-limit": "8.3.0",
|
||||
"glob": "13.0.6",
|
||||
"helmet": "8.1.0",
|
||||
"http-errors": "2.0.1",
|
||||
"input-otp": "1.4.2",
|
||||
"ioredis": "5.10.1",
|
||||
"ioredis": "5.10.0",
|
||||
"jmespath": "0.16.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"jsonwebtoken": "9.0.3",
|
||||
"lucide-react": "0.577.0",
|
||||
"maxmind": "5.0.6",
|
||||
"maxmind": "5.0.5",
|
||||
"moment": "2.30.1",
|
||||
"next": "15.5.14",
|
||||
"next-intl": "4.8.3",
|
||||
"next-themes": "0.4.6",
|
||||
"nextjs-toploader": "3.9.17",
|
||||
"node-cache": "5.1.2",
|
||||
"nodemailer": "8.0.5",
|
||||
"nodemailer": "8.0.4",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "8.20.0",
|
||||
"posthog-node": "5.28.0",
|
||||
"qrcode.react": "4.2.0",
|
||||
"react": "19.2.5",
|
||||
"react": "19.2.4",
|
||||
"react-day-picker": "9.14.0",
|
||||
"react-dom": "19.2.5",
|
||||
"react-dom": "19.2.4",
|
||||
"react-easy-sort": "1.8.0",
|
||||
"react-hook-form": "7.71.2",
|
||||
"react-icons": "5.6.0",
|
||||
@@ -89,13 +89,13 @@
|
||||
"reodotdev": "1.1.0",
|
||||
"resend": "6.9.2",
|
||||
"semver": "7.7.4",
|
||||
"sshpk": "1.18.0",
|
||||
"sshpk": "^1.18.0",
|
||||
"stripe": "20.4.1",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"tailwind-merge": "3.5.0",
|
||||
"topojson-client": "3.1.0",
|
||||
"tw-animate-css": "1.4.0",
|
||||
"use-debounce": "10.1.1",
|
||||
"use-debounce": "^10.1.0",
|
||||
"uuid": "13.0.0",
|
||||
"vaul": "1.1.2",
|
||||
"visionscarto-world-atlas": "1.0.0",
|
||||
@@ -124,13 +124,13 @@
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsonwebtoken": "9.0.10",
|
||||
"@types/node": "25.3.5",
|
||||
"@types/nodemailer": "8.0.0",
|
||||
"@types/nodemailer": "7.0.11",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@types/pg": "8.18.0",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/sshpk": "1.17.4",
|
||||
"@types/sshpk": "^1.17.4",
|
||||
"@types/swagger-ui-express": "4.1.8",
|
||||
"@types/topojson-client": "3.1.5",
|
||||
"@types/ws": "8.18.1",
|
||||
@@ -2247,9 +2247,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@headlessui/react": {
|
||||
"version": "2.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.10.tgz",
|
||||
"integrity": "sha512-5pVLNK9wlpxTUTy9GpgbX/SdcRh+HBnPktjM2wbiLTH4p+2EPHBO1aoSryUCuKUIItdDWO9ITlhUL8UnUN/oIA==",
|
||||
"version": "2.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.9.tgz",
|
||||
"integrity": "sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.26.16",
|
||||
@@ -6386,6 +6386,18 @@
|
||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-email/body": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.2.1.tgz",
|
||||
"integrity": "sha512-ljDiQiJDu/Fq//vSIIP0z5Nuvt4+DX1RqGasstChDGJB/14ogd4VdNS9aacoede/ZjGy3o3Qb+cxyS+XgM6SwQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-email/button": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.2.1.tgz",
|
||||
@@ -6438,12 +6450,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-email/components": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.11.tgz",
|
||||
"integrity": "sha512-s0CX31+S/u1MhBWYFAuZru0NHNExTY+OeZC9OrGyzl8PGQ0Iz/4gq3O4rHUVuA1D7FjAcPbwG1Up0yey/Xh6dw==",
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.8.tgz",
|
||||
"integrity": "sha512-zY81ED6o5MWMzBkr9uZFuT24lWarT+xIbOZxI6C9dsFmCWBczM8IE1BgOI8rhpUK4JcYVDy1uKxYAFqsx2Bc4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-email/body": "0.3.0",
|
||||
"@react-email/body": "0.2.1",
|
||||
"@react-email/button": "0.2.1",
|
||||
"@react-email/code-block": "0.2.1",
|
||||
"@react-email/code-inline": "0.0.6",
|
||||
@@ -6458,10 +6470,10 @@
|
||||
"@react-email/link": "0.0.13",
|
||||
"@react-email/markdown": "0.0.18",
|
||||
"@react-email/preview": "0.0.14",
|
||||
"@react-email/render": "2.0.5",
|
||||
"@react-email/render": "2.0.4",
|
||||
"@react-email/row": "0.0.13",
|
||||
"@react-email/section": "0.0.17",
|
||||
"@react-email/tailwind": "2.0.7",
|
||||
"@react-email/tailwind": "2.0.5",
|
||||
"@react-email/text": "0.1.6"
|
||||
},
|
||||
"engines": {
|
||||
@@ -6471,18 +6483,6 @@
|
||||
"react": "^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-email/components/node_modules/@react-email/body": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.3.0.tgz",
|
||||
"integrity": "sha512-uGo0BOOzjbMUo3lu+BIDWayvn5o6Xyfmnlla5VGf05n8gHMvO1ll7U4FtzWe3hxMLwt53pmc4iE0M+B5slG+Ug==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-email/container": {
|
||||
"version": "0.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.16.tgz",
|
||||
@@ -6854,9 +6854,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-email/render": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-2.0.5.tgz",
|
||||
"integrity": "sha512-oAsSpY/vYt9ReDcRQDBLxENwCNAklkE6bvP5Kl9ZlmVr/RZpfhloJp8xc/OZki/YF2nisRRX50aEy8P9v3R5GA==",
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-2.0.4.tgz",
|
||||
"integrity": "sha512-kht2oTFQ1SwrLpd882ahTvUtNa9s53CERHstiTbzhm6aR2Hbykp/mQ4tpPvsBGkKAEvKRlDEoooh60Uk6nHK1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"html-to-text": "^9.0.5",
|
||||
@@ -6895,9 +6895,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-email/tailwind": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.7.tgz",
|
||||
"integrity": "sha512-kGw80weVFXikcnCXbigTGXGWQ0MRCSYNCudcdkWxebkWYd0FG6/NPoN3V1p/u68/4+NxZwYPVi2fhnp0x23HdA==",
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.5.tgz",
|
||||
"integrity": "sha512-7Ey+kiWliJdxPMCLYsdDts8ffp4idlP//w4Ui3q/A5kokVaLSNKG8DOg/8qAuzWmRiGwNQVOKBk7PXNlK5W+sg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tailwindcss": "^4.1.18"
|
||||
@@ -6906,17 +6906,17 @@
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-email/body": ">=0",
|
||||
"@react-email/button": ">=0",
|
||||
"@react-email/code-block": ">=0",
|
||||
"@react-email/code-inline": ">=0",
|
||||
"@react-email/container": ">=0",
|
||||
"@react-email/heading": ">=0",
|
||||
"@react-email/hr": ">=0",
|
||||
"@react-email/img": ">=0",
|
||||
"@react-email/link": ">=0",
|
||||
"@react-email/preview": ">=0",
|
||||
"@react-email/text": ">=0",
|
||||
"@react-email/body": "0.2.1",
|
||||
"@react-email/button": "0.2.1",
|
||||
"@react-email/code-block": "0.2.1",
|
||||
"@react-email/code-inline": "0.0.6",
|
||||
"@react-email/container": "0.0.16",
|
||||
"@react-email/heading": "0.0.16",
|
||||
"@react-email/hr": "0.0.12",
|
||||
"@react-email/img": "0.0.12",
|
||||
"@react-email/link": "0.0.13",
|
||||
"@react-email/preview": "0.0.14",
|
||||
"@react-email/text": "0.1.6",
|
||||
"react": "^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -8979,9 +8979,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nodemailer": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.0.tgz",
|
||||
"integrity": "sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==",
|
||||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz",
|
||||
"integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -11731,9 +11731,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/drizzle-orm": {
|
||||
"version": "0.45.2",
|
||||
"resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz",
|
||||
"integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==",
|
||||
"version": "0.45.1",
|
||||
"resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.1.tgz",
|
||||
"integrity": "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/client-rds-data": ">=3",
|
||||
@@ -12951,9 +12951,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
|
||||
"integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz",
|
||||
"integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.1.0"
|
||||
@@ -13950,9 +13950,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ioredis": {
|
||||
"version": "5.10.1",
|
||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz",
|
||||
"integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==",
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.0.tgz",
|
||||
"integrity": "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ioredis/commands": "1.5.1",
|
||||
@@ -15098,13 +15098,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/maxmind": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-5.0.6.tgz",
|
||||
"integrity": "sha512-5bvd/u+kIaTqaGM+xkXjatzQw1dQfSmlLggr2W1EKMyMxSgx2woZyusLpNpZ4DdPmL+1bbJWeo4LXsi6bC0Iew==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-5.0.5.tgz",
|
||||
"integrity": "sha512-1lcH2kMjbBpCFhuHaMU32vz8CuOsKttRcWMQyXvtlklopCzN7NNHSVR/h9RYa8JPuFTGmkn2vYARm+7cIGuqDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mmdb-lib": "3.0.2",
|
||||
"tiny-lru": "13.0.0"
|
||||
"tiny-lru": "11.4.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
@@ -15646,9 +15646,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz",
|
||||
"integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==",
|
||||
"version": "8.0.4",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.4.tgz",
|
||||
"integrity": "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -16888,9 +16888,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
|
||||
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -16919,15 +16919,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
|
||||
"integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.5"
|
||||
"react": "^19.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/react-easy-sort": {
|
||||
@@ -18733,12 +18733,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-lru": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-13.0.0.tgz",
|
||||
"integrity": "sha512-xDHxKKS1FdF0Tv2P+QT7IeSEg74K/8cEDzbv3Tv6UyHHUgBOjOiQiBp818MGj66dhurQus/IBcoAbwIKtSGc6Q==",
|
||||
"version": "11.4.7",
|
||||
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.7.tgz",
|
||||
"integrity": "sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
@@ -19329,9 +19329,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/use-debounce": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.1.1.tgz",
|
||||
"integrity": "sha512-kvds8BHR2k28cFsxW8k3nc/tRga2rs1RHYCqmmGqb90MEeE++oALwzh2COiuBLO1/QXiOuShXoSN2ZpWnMmvuQ==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.1.0.tgz",
|
||||
"integrity": "sha512-lu87Za35V3n/MyMoEpD5zJv0k7hCn0p+V/fK2kWD+3k2u3kOCwO593UArbczg1fhfs2rqPEnHpULJ3KmGdDzvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
|
||||
26
package.json
26
package.json
@@ -35,7 +35,7 @@
|
||||
"@asteasolutions/zod-to-openapi": "8.4.1",
|
||||
"@aws-sdk/client-s3": "3.1011.0",
|
||||
"@faker-js/faker": "10.3.0",
|
||||
"@headlessui/react": "2.2.10",
|
||||
"@headlessui/react": "2.2.9",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
"@node-rs/argon2": "2.0.2",
|
||||
@@ -59,9 +59,9 @@
|
||||
"@radix-ui/react-tabs": "1.1.13",
|
||||
"@radix-ui/react-toast": "1.2.15",
|
||||
"@radix-ui/react-tooltip": "1.2.8",
|
||||
"@react-email/components": "1.0.11",
|
||||
"@react-email/render": "2.0.5",
|
||||
"@react-email/tailwind": "2.0.7",
|
||||
"@react-email/components": "1.0.8",
|
||||
"@react-email/render": "2.0.4",
|
||||
"@react-email/tailwind": "2.0.5",
|
||||
"@simplewebauthn/browser": "13.3.0",
|
||||
"@simplewebauthn/server": "13.3.0",
|
||||
"@tailwindcss/forms": "0.5.11",
|
||||
@@ -78,33 +78,33 @@
|
||||
"cors": "2.8.6",
|
||||
"crypto-js": "4.2.0",
|
||||
"d3": "7.9.0",
|
||||
"drizzle-orm": "0.45.2",
|
||||
"drizzle-orm": "0.45.1",
|
||||
"express": "5.2.1",
|
||||
"express-rate-limit": "8.3.2",
|
||||
"express-rate-limit": "8.3.0",
|
||||
"glob": "13.0.6",
|
||||
"helmet": "8.1.0",
|
||||
"http-errors": "2.0.1",
|
||||
"input-otp": "1.4.2",
|
||||
"ioredis": "5.10.1",
|
||||
"ioredis": "5.10.0",
|
||||
"jmespath": "0.16.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"jsonwebtoken": "9.0.3",
|
||||
"lucide-react": "0.577.0",
|
||||
"maxmind": "5.0.6",
|
||||
"maxmind": "5.0.5",
|
||||
"moment": "2.30.1",
|
||||
"next": "15.5.14",
|
||||
"next-intl": "4.8.3",
|
||||
"next-themes": "0.4.6",
|
||||
"nextjs-toploader": "3.9.17",
|
||||
"node-cache": "5.1.2",
|
||||
"nodemailer": "8.0.5",
|
||||
"nodemailer": "8.0.4",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "8.20.0",
|
||||
"posthog-node": "5.28.0",
|
||||
"qrcode.react": "4.2.0",
|
||||
"react": "19.2.5",
|
||||
"react": "19.2.4",
|
||||
"react-day-picker": "9.14.0",
|
||||
"react-dom": "19.2.5",
|
||||
"react-dom": "19.2.4",
|
||||
"react-easy-sort": "1.8.0",
|
||||
"react-hook-form": "7.71.2",
|
||||
"react-icons": "5.6.0",
|
||||
@@ -118,7 +118,7 @@
|
||||
"tailwind-merge": "3.5.0",
|
||||
"topojson-client": "3.1.0",
|
||||
"tw-animate-css": "1.4.0",
|
||||
"use-debounce": "10.1.1",
|
||||
"use-debounce": "10.1.0",
|
||||
"uuid": "13.0.0",
|
||||
"vaul": "1.1.2",
|
||||
"visionscarto-world-atlas": "1.0.0",
|
||||
@@ -147,7 +147,7 @@
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsonwebtoken": "9.0.10",
|
||||
"@types/node": "25.3.5",
|
||||
"@types/nodemailer": "8.0.0",
|
||||
"@types/nodemailer": "7.0.11",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@types/pg": "8.18.0",
|
||||
"@types/react": "19.2.14",
|
||||
|
||||
@@ -89,12 +89,8 @@ export const sites = pgTable("sites", {
|
||||
name: varchar("name").notNull(),
|
||||
pubKey: varchar("pubKey"),
|
||||
subnet: varchar("subnet"),
|
||||
megabytesIn: real("bytesIn").default(0),
|
||||
megabytesOut: real("bytesOut").default(0),
|
||||
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
||||
type: varchar("type").notNull(), // "newt" or "wireguard"
|
||||
online: boolean("online").notNull().default(false),
|
||||
lastPing: integer("lastPing"),
|
||||
address: varchar("address"),
|
||||
endpoint: varchar("endpoint"),
|
||||
publicKey: varchar("publicKey"),
|
||||
@@ -729,10 +725,7 @@ export const clients = pgTable("clients", {
|
||||
name: varchar("name").notNull(),
|
||||
pubKey: varchar("pubKey"),
|
||||
subnet: varchar("subnet").notNull(),
|
||||
megabytesIn: real("bytesIn"),
|
||||
megabytesOut: real("bytesOut"),
|
||||
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
||||
lastPing: integer("lastPing"),
|
||||
|
||||
type: varchar("type").notNull(), // "olm"
|
||||
online: boolean("online").notNull().default(false),
|
||||
// endpoint: varchar("endpoint"),
|
||||
@@ -745,6 +738,42 @@ export const clients = pgTable("clients", {
|
||||
>()
|
||||
});
|
||||
|
||||
export const sitePing = pgTable("sitePing", {
|
||||
siteId: integer("siteId")
|
||||
.primaryKey()
|
||||
.references(() => sites.siteId, { onDelete: "cascade" })
|
||||
.notNull(),
|
||||
lastPing: integer("lastPing")
|
||||
});
|
||||
|
||||
export const siteBandwidth = pgTable("siteBandwidth", {
|
||||
siteId: integer("siteId")
|
||||
.primaryKey()
|
||||
.references(() => sites.siteId, { onDelete: "cascade" })
|
||||
.notNull(),
|
||||
megabytesIn: real("bytesIn").default(0),
|
||||
megabytesOut: real("bytesOut").default(0),
|
||||
lastBandwidthUpdate: integer("lastBandwidthUpdate") // unix epoch
|
||||
});
|
||||
|
||||
export const clientPing = pgTable("clientPing", {
|
||||
clientId: integer("clientId")
|
||||
.primaryKey()
|
||||
.references(() => clients.clientId, { onDelete: "cascade" })
|
||||
.notNull(),
|
||||
lastPing: integer("lastPing")
|
||||
});
|
||||
|
||||
export const clientBandwidth = pgTable("clientBandwidth", {
|
||||
clientId: integer("clientId")
|
||||
.primaryKey()
|
||||
.references(() => clients.clientId, { onDelete: "cascade" })
|
||||
.notNull(),
|
||||
megabytesIn: real("bytesIn"),
|
||||
megabytesOut: real("bytesOut"),
|
||||
lastBandwidthUpdate: integer("lastBandwidthUpdate") // unix epoch
|
||||
});
|
||||
|
||||
export const clientSitesAssociationsCache = pgTable(
|
||||
"clientSitesAssociationsCache",
|
||||
{
|
||||
@@ -1106,3 +1135,7 @@ export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
||||
export type RoundTripMessageTracker = InferSelectModel<
|
||||
typeof roundTripMessageTracker
|
||||
>;
|
||||
export type SitePing = typeof sitePing.$inferSelect;
|
||||
export type SiteBandwidth = typeof siteBandwidth.$inferSelect;
|
||||
export type ClientPing = typeof clientPing.$inferSelect;
|
||||
export type ClientBandwidth = typeof clientBandwidth.$inferSelect;
|
||||
|
||||
@@ -95,12 +95,8 @@ export const sites = sqliteTable("sites", {
|
||||
name: text("name").notNull(),
|
||||
pubKey: text("pubKey"),
|
||||
subnet: text("subnet"),
|
||||
megabytesIn: integer("bytesIn").default(0),
|
||||
megabytesOut: integer("bytesOut").default(0),
|
||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||
type: text("type").notNull(), // "newt" or "wireguard"
|
||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||
lastPing: integer("lastPing"),
|
||||
|
||||
// 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
|
||||
@@ -399,10 +395,7 @@ export const clients = sqliteTable("clients", {
|
||||
pubKey: text("pubKey"),
|
||||
olmId: text("olmId"), // to lock it to a specific olm optionally
|
||||
subnet: text("subnet").notNull(),
|
||||
megabytesIn: integer("bytesIn"),
|
||||
megabytesOut: integer("bytesOut"),
|
||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||
lastPing: integer("lastPing"),
|
||||
|
||||
type: text("type").notNull(), // "olm"
|
||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||
// endpoint: text("endpoint"),
|
||||
@@ -414,6 +407,42 @@ export const clients = sqliteTable("clients", {
|
||||
>()
|
||||
});
|
||||
|
||||
export const sitePing = sqliteTable("sitePing", {
|
||||
siteId: integer("siteId")
|
||||
.primaryKey()
|
||||
.references(() => sites.siteId, { onDelete: "cascade" })
|
||||
.notNull(),
|
||||
lastPing: integer("lastPing")
|
||||
});
|
||||
|
||||
export const siteBandwidth = sqliteTable("siteBandwidth", {
|
||||
siteId: integer("siteId")
|
||||
.primaryKey()
|
||||
.references(() => sites.siteId, { onDelete: "cascade" })
|
||||
.notNull(),
|
||||
megabytesIn: integer("bytesIn").default(0),
|
||||
megabytesOut: integer("bytesOut").default(0),
|
||||
lastBandwidthUpdate: integer("lastBandwidthUpdate") // unix epoch
|
||||
});
|
||||
|
||||
export const clientPing = sqliteTable("clientPing", {
|
||||
clientId: integer("clientId")
|
||||
.primaryKey()
|
||||
.references(() => clients.clientId, { onDelete: "cascade" })
|
||||
.notNull(),
|
||||
lastPing: integer("lastPing")
|
||||
});
|
||||
|
||||
export const clientBandwidth = sqliteTable("clientBandwidth", {
|
||||
clientId: integer("clientId")
|
||||
.primaryKey()
|
||||
.references(() => clients.clientId, { onDelete: "cascade" })
|
||||
.notNull(),
|
||||
megabytesIn: integer("bytesIn"),
|
||||
megabytesOut: integer("bytesOut"),
|
||||
lastBandwidthUpdate: integer("lastBandwidthUpdate") // unix epoch
|
||||
});
|
||||
|
||||
export const clientSitesAssociationsCache = sqliteTable(
|
||||
"clientSitesAssociationsCache",
|
||||
{
|
||||
@@ -1209,3 +1238,7 @@ export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
|
||||
export type RoundTripMessageTracker = InferSelectModel<
|
||||
typeof roundTripMessageTracker
|
||||
>;
|
||||
export type SitePing = typeof sitePing.$inferSelect;
|
||||
export type SiteBandwidth = typeof siteBandwidth.$inferSelect;
|
||||
export type ClientPing = typeof clientPing.$inferSelect;
|
||||
export type ClientBandwidth = typeof clientBandwidth.$inferSelect;
|
||||
|
||||
@@ -3,7 +3,7 @@ import config from "./config";
|
||||
import { getHostMeta } from "./hostMeta";
|
||||
import logger from "@server/logger";
|
||||
import { apiKeys, db, roles, siteResources } from "@server/db";
|
||||
import { sites, users, orgs, resources, clients, idp } from "@server/db";
|
||||
import { sites, users, orgs, resources, clients, idp, siteBandwidth } from "@server/db";
|
||||
import { eq, count, notInArray, and, isNotNull, isNull } from "drizzle-orm";
|
||||
import { APP_VERSION } from "./consts";
|
||||
import crypto from "crypto";
|
||||
@@ -150,12 +150,13 @@ class TelemetryClient {
|
||||
const siteDetails = await db
|
||||
.select({
|
||||
siteName: sites.name,
|
||||
megabytesIn: sites.megabytesIn,
|
||||
megabytesOut: sites.megabytesOut,
|
||||
megabytesIn: siteBandwidth.megabytesIn,
|
||||
megabytesOut: siteBandwidth.megabytesOut,
|
||||
type: sites.type,
|
||||
online: sites.online
|
||||
})
|
||||
.from(sites);
|
||||
.from(sites)
|
||||
.leftJoin(siteBandwidth, eq(siteBandwidth.siteId, sites.siteId));
|
||||
|
||||
const supporterKey = config.getSupporterData();
|
||||
|
||||
|
||||
@@ -18,10 +18,11 @@ import {
|
||||
subscriptionItems,
|
||||
usage,
|
||||
sites,
|
||||
siteBandwidth,
|
||||
customers,
|
||||
orgs
|
||||
} from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import { getFeatureIdByMetricId, getFeatureIdByPriceId } from "@server/lib/billing/features";
|
||||
import stripe from "#private/lib/stripe";
|
||||
@@ -253,14 +254,19 @@ export async function handleSubscriptionUpdated(
|
||||
);
|
||||
}
|
||||
|
||||
// Also reset the sites to 0
|
||||
// Also reset the site bandwidth to 0
|
||||
await trx
|
||||
.update(sites)
|
||||
.update(siteBandwidth)
|
||||
.set({
|
||||
megabytesIn: 0,
|
||||
megabytesOut: 0
|
||||
})
|
||||
.where(eq(sites.orgId, orgId));
|
||||
.where(
|
||||
inArray(
|
||||
siteBandwidth.siteId,
|
||||
trx.select({ siteId: sites.siteId }).from(sites).where(eq(sites.orgId, orgId))
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
clientBandwidth,
|
||||
clients,
|
||||
clientSitesAssociationsCache,
|
||||
currentFingerprint,
|
||||
@@ -180,8 +181,8 @@ function queryClientsBase() {
|
||||
name: clients.name,
|
||||
pubKey: clients.pubKey,
|
||||
subnet: clients.subnet,
|
||||
megabytesIn: clients.megabytesIn,
|
||||
megabytesOut: clients.megabytesOut,
|
||||
megabytesIn: clientBandwidth.megabytesIn,
|
||||
megabytesOut: clientBandwidth.megabytesOut,
|
||||
orgName: orgs.name,
|
||||
type: clients.type,
|
||||
online: clients.online,
|
||||
@@ -200,7 +201,8 @@ function queryClientsBase() {
|
||||
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||
.leftJoin(users, eq(clients.userId, users.userId))
|
||||
.leftJoin(currentFingerprint, eq(olms.olmId, currentFingerprint.olmId));
|
||||
.leftJoin(currentFingerprint, eq(olms.olmId, currentFingerprint.olmId))
|
||||
.leftJoin(clientBandwidth, eq(clientBandwidth.clientId, clients.clientId));
|
||||
}
|
||||
|
||||
async function getSiteAssociations(clientIds: number[]) {
|
||||
@@ -367,9 +369,15 @@ export async function listClients(
|
||||
.offset(pageSize * (page - 1))
|
||||
.orderBy(
|
||||
sort_by
|
||||
? order === "asc"
|
||||
? asc(clients[sort_by])
|
||||
: desc(clients[sort_by])
|
||||
? (() => {
|
||||
const field =
|
||||
sort_by === "megabytesIn"
|
||||
? clientBandwidth.megabytesIn
|
||||
: sort_by === "megabytesOut"
|
||||
? clientBandwidth.megabytesOut
|
||||
: clients.name;
|
||||
return order === "asc" ? asc(field) : desc(field);
|
||||
})()
|
||||
: asc(clients.name)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { build } from "@server/build";
|
||||
import {
|
||||
clientBandwidth,
|
||||
clients,
|
||||
currentFingerprint,
|
||||
db,
|
||||
@@ -211,8 +212,8 @@ function queryUserDevicesBase() {
|
||||
name: clients.name,
|
||||
pubKey: clients.pubKey,
|
||||
subnet: clients.subnet,
|
||||
megabytesIn: clients.megabytesIn,
|
||||
megabytesOut: clients.megabytesOut,
|
||||
megabytesIn: clientBandwidth.megabytesIn,
|
||||
megabytesOut: clientBandwidth.megabytesOut,
|
||||
orgName: orgs.name,
|
||||
type: clients.type,
|
||||
online: clients.online,
|
||||
@@ -239,7 +240,8 @@ function queryUserDevicesBase() {
|
||||
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||
.leftJoin(users, eq(clients.userId, users.userId))
|
||||
.leftJoin(currentFingerprint, eq(olms.olmId, currentFingerprint.olmId));
|
||||
.leftJoin(currentFingerprint, eq(olms.olmId, currentFingerprint.olmId))
|
||||
.leftJoin(clientBandwidth, eq(clientBandwidth.clientId, clients.clientId));
|
||||
}
|
||||
|
||||
type OlmWithUpdateAvailable = Awaited<
|
||||
@@ -427,9 +429,15 @@ export async function listUserDevices(
|
||||
.offset(pageSize * (page - 1))
|
||||
.orderBy(
|
||||
sort_by
|
||||
? order === "asc"
|
||||
? asc(clients[sort_by])
|
||||
: desc(clients[sort_by])
|
||||
? (() => {
|
||||
const field =
|
||||
sort_by === "megabytesIn"
|
||||
? clientBandwidth.megabytesIn
|
||||
: sort_by === "megabytesOut"
|
||||
? clientBandwidth.megabytesOut
|
||||
: clients.name;
|
||||
return order === "asc" ? asc(field) : desc(field);
|
||||
})()
|
||||
: asc(clients.clientId)
|
||||
);
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
|
||||
const snapshot = accumulator;
|
||||
accumulator = new Map<string, AccumulatorEntry>();
|
||||
|
||||
const currentTime = new Date().toISOString();
|
||||
const currentEpoch = Math.floor(Date.now() / 1000);
|
||||
|
||||
// Sort by publicKey for consistent lock ordering across concurrent
|
||||
// writers — deadlock-prevention strategy.
|
||||
@@ -157,33 +157,52 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
|
||||
orgId: string;
|
||||
pubKey: string;
|
||||
}>(sql`
|
||||
UPDATE sites
|
||||
SET
|
||||
"bytesOut" = COALESCE("bytesOut", 0) + ${bytesIn},
|
||||
"bytesIn" = COALESCE("bytesIn", 0) + ${bytesOut},
|
||||
"lastBandwidthUpdate" = ${currentTime}
|
||||
WHERE "pubKey" = ${publicKey}
|
||||
RETURNING "orgId", "pubKey"
|
||||
WITH upsert AS (
|
||||
INSERT INTO "siteBandwidth" ("siteId", "bytesIn", "bytesOut", "lastBandwidthUpdate")
|
||||
SELECT s."siteId", ${bytesIn}, ${bytesOut}, ${currentEpoch}
|
||||
FROM "sites" s WHERE s."pubKey" = ${publicKey}
|
||||
ON CONFLICT ("siteId") DO UPDATE SET
|
||||
"bytesIn" = COALESCE("siteBandwidth"."bytesIn", 0) + EXCLUDED."bytesIn",
|
||||
"bytesOut" = COALESCE("siteBandwidth"."bytesOut", 0) + EXCLUDED."bytesOut",
|
||||
"lastBandwidthUpdate" = EXCLUDED."lastBandwidthUpdate"
|
||||
RETURNING "siteId"
|
||||
)
|
||||
SELECT u."siteId", s."orgId", s."pubKey"
|
||||
FROM upsert u
|
||||
INNER JOIN "sites" s ON s."siteId" = u."siteId"
|
||||
`);
|
||||
results.push(...result);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// PostgreSQL: batch UPDATE … FROM (VALUES …) — single round-trip per chunk.
|
||||
// PostgreSQL: batch UPSERT via CTE — single round-trip per chunk.
|
||||
const valuesList = chunk.map(([publicKey, { bytesIn, bytesOut }]) =>
|
||||
sql`(${publicKey}::text, ${bytesIn}::real, ${bytesOut}::real)`
|
||||
);
|
||||
const valuesClause = sql.join(valuesList, sql`, `);
|
||||
return dbQueryRows<{ orgId: string; pubKey: string }>(sql`
|
||||
UPDATE sites
|
||||
SET
|
||||
"bytesOut" = COALESCE("bytesOut", 0) + v.bytes_in,
|
||||
"bytesIn" = COALESCE("bytesIn", 0) + v.bytes_out,
|
||||
"lastBandwidthUpdate" = ${currentTime}
|
||||
FROM (VALUES ${valuesClause}) AS v(pub_key, bytes_in, bytes_out)
|
||||
WHERE sites."pubKey" = v.pub_key
|
||||
RETURNING sites."orgId" AS "orgId", sites."pubKey" AS "pubKey"
|
||||
WITH vals(pub_key, bytes_in, bytes_out) AS (
|
||||
VALUES ${valuesClause}
|
||||
),
|
||||
site_lookup AS (
|
||||
SELECT s."siteId", s."orgId", s."pubKey", v.bytes_in, v.bytes_out
|
||||
FROM vals v
|
||||
INNER JOIN "sites" s ON s."pubKey" = v.pub_key
|
||||
),
|
||||
upsert AS (
|
||||
INSERT INTO "siteBandwidth" ("siteId", "bytesIn", "bytesOut", "lastBandwidthUpdate")
|
||||
SELECT sl."siteId", sl.bytes_in, sl.bytes_out, ${currentEpoch}::integer
|
||||
FROM site_lookup sl
|
||||
ON CONFLICT ("siteId") DO UPDATE SET
|
||||
"bytesIn" = COALESCE("siteBandwidth"."bytesIn", 0) + EXCLUDED."bytesIn",
|
||||
"bytesOut" = COALESCE("siteBandwidth"."bytesOut", 0) + EXCLUDED."bytesOut",
|
||||
"lastBandwidthUpdate" = EXCLUDED."lastBandwidthUpdate"
|
||||
RETURNING "siteId"
|
||||
)
|
||||
SELECT u."siteId", s."orgId", s."pubKey"
|
||||
FROM upsert u
|
||||
INNER JOIN "sites" s ON s."siteId" = u."siteId"
|
||||
`);
|
||||
}, `flush bandwidth chunk [${i}–${chunkEnd}]`);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { db, newts, sites, targetHealthCheck, targets } from "@server/db";
|
||||
import { db, newts, sites, targetHealthCheck, targets, sitePing, siteBandwidth } from "@server/db";
|
||||
import {
|
||||
hasActiveConnections,
|
||||
getClientConfigVersion
|
||||
} from "#dynamic/routers/ws";
|
||||
import { MessageHandler } from "@server/routers/ws";
|
||||
import { Newt } from "@server/db";
|
||||
import { eq, lt, isNull, and, or, ne, not } from "drizzle-orm";
|
||||
import { eq, lt, isNull, and, or, ne } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import { sendNewtSyncMessage } from "./sync";
|
||||
import { recordPing } from "./pingAccumulator";
|
||||
@@ -41,17 +41,18 @@ export const startNewtOfflineChecker = (): void => {
|
||||
.select({
|
||||
siteId: sites.siteId,
|
||||
newtId: newts.newtId,
|
||||
lastPing: sites.lastPing
|
||||
lastPing: sitePing.lastPing
|
||||
})
|
||||
.from(sites)
|
||||
.innerJoin(newts, eq(newts.siteId, sites.siteId))
|
||||
.leftJoin(sitePing, eq(sitePing.siteId, sites.siteId))
|
||||
.where(
|
||||
and(
|
||||
eq(sites.online, true),
|
||||
eq(sites.type, "newt"),
|
||||
or(
|
||||
lt(sites.lastPing, twoMinutesAgo),
|
||||
isNull(sites.lastPing)
|
||||
lt(sitePing.lastPing, twoMinutesAgo),
|
||||
isNull(sitePing.lastPing)
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -112,15 +113,11 @@ export const startNewtOfflineChecker = (): void => {
|
||||
.select({
|
||||
siteId: sites.siteId,
|
||||
online: sites.online,
|
||||
lastBandwidthUpdate: sites.lastBandwidthUpdate
|
||||
lastBandwidthUpdate: siteBandwidth.lastBandwidthUpdate
|
||||
})
|
||||
.from(sites)
|
||||
.where(
|
||||
and(
|
||||
eq(sites.type, "wireguard"),
|
||||
not(isNull(sites.lastBandwidthUpdate))
|
||||
)
|
||||
);
|
||||
.innerJoin(siteBandwidth, eq(siteBandwidth.siteId, sites.siteId))
|
||||
.where(eq(sites.type, "wireguard"));
|
||||
|
||||
const wireguardOfflineThreshold = Math.floor(
|
||||
(Date.now() - OFFLINE_THRESHOLD_BANDWIDTH_MS) / 1000
|
||||
@@ -128,12 +125,7 @@ export const startNewtOfflineChecker = (): void => {
|
||||
|
||||
// loop over each one. If its offline and there is a new update then mark it online. If its online and there is no update then mark it offline
|
||||
for (const site of allWireguardSites) {
|
||||
const lastBandwidthUpdate =
|
||||
new Date(site.lastBandwidthUpdate!).getTime() / 1000;
|
||||
if (
|
||||
lastBandwidthUpdate < wireguardOfflineThreshold &&
|
||||
site.online
|
||||
) {
|
||||
if ((site.lastBandwidthUpdate ?? 0) < wireguardOfflineThreshold && site.online) {
|
||||
logger.info(
|
||||
`Marking wireguard site ${site.siteId} offline: no bandwidth update in over ${OFFLINE_THRESHOLD_BANDWIDTH_MS / 60000} minutes`
|
||||
);
|
||||
@@ -142,10 +134,7 @@ export const startNewtOfflineChecker = (): void => {
|
||||
.update(sites)
|
||||
.set({ online: false })
|
||||
.where(eq(sites.siteId, site.siteId));
|
||||
} else if (
|
||||
lastBandwidthUpdate >= wireguardOfflineThreshold &&
|
||||
!site.online
|
||||
) {
|
||||
} else if ((site.lastBandwidthUpdate ?? 0) >= wireguardOfflineThreshold && !site.online) {
|
||||
logger.info(
|
||||
`Marking wireguard site ${site.siteId} online: recent bandwidth update`
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { db } from "@server/db";
|
||||
import { db, clients, clientBandwidth } from "@server/db";
|
||||
import { MessageHandler } from "@server/routers/ws";
|
||||
import { clients } from "@server/db";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
|
||||
@@ -85,7 +84,7 @@ export async function flushBandwidthToDb(): Promise<void> {
|
||||
const snapshot = accumulator;
|
||||
accumulator = new Map<string, BandwidthAccumulator>();
|
||||
|
||||
const currentTime = new Date().toISOString();
|
||||
const currentEpoch = Math.floor(Date.now() / 1000);
|
||||
|
||||
// Sort by publicKey for consistent lock ordering across concurrent
|
||||
// writers — this is the same deadlock-prevention strategy used in the
|
||||
@@ -101,19 +100,37 @@ export async function flushBandwidthToDb(): Promise<void> {
|
||||
for (const [publicKey, { bytesIn, bytesOut }] of sortedEntries) {
|
||||
try {
|
||||
await withDeadlockRetry(async () => {
|
||||
// Use atomic SQL increment to avoid the SELECT-then-UPDATE
|
||||
// anti-pattern and the races it would introduce.
|
||||
// Find clientId by pubKey
|
||||
const [clientRow] = await db
|
||||
.select({ clientId: clients.clientId })
|
||||
.from(clients)
|
||||
.where(eq(clients.pubKey, publicKey))
|
||||
.limit(1);
|
||||
|
||||
if (!clientRow) {
|
||||
logger.warn(`No client found for pubKey ${publicKey}, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
await db
|
||||
.update(clients)
|
||||
.set({
|
||||
.insert(clientBandwidth)
|
||||
.values({
|
||||
clientId: clientRow.clientId,
|
||||
// Note: bytesIn from peer goes to megabytesOut (data
|
||||
// sent to client) and bytesOut from peer goes to
|
||||
// megabytesIn (data received from client).
|
||||
megabytesOut: sql`COALESCE(${clients.megabytesOut}, 0) + ${bytesIn}`,
|
||||
megabytesIn: sql`COALESCE(${clients.megabytesIn}, 0) + ${bytesOut}`,
|
||||
lastBandwidthUpdate: currentTime
|
||||
megabytesOut: bytesIn,
|
||||
megabytesIn: bytesOut,
|
||||
lastBandwidthUpdate: currentEpoch
|
||||
})
|
||||
.where(eq(clients.pubKey, publicKey));
|
||||
.onConflictDoUpdate({
|
||||
target: clientBandwidth.clientId,
|
||||
set: {
|
||||
megabytesOut: sql`COALESCE(${clientBandwidth.megabytesOut}, 0) + ${bytesIn}`,
|
||||
megabytesIn: sql`COALESCE(${clientBandwidth.megabytesIn}, 0) + ${bytesOut}`,
|
||||
lastBandwidthUpdate: currentEpoch
|
||||
}
|
||||
});
|
||||
}, `flush bandwidth for client ${publicKey}`);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from "@server/db";
|
||||
import { sites, clients, olms } from "@server/db";
|
||||
import { inArray } from "drizzle-orm";
|
||||
import { sites, clients, olms, sitePing, clientPing } from "@server/db";
|
||||
import { inArray, sql } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
|
||||
/**
|
||||
@@ -81,11 +81,8 @@ export function recordClientPing(
|
||||
/**
|
||||
* Flush all accumulated site pings to the database.
|
||||
*
|
||||
* Each batch of up to BATCH_SIZE rows is written with a **single** UPDATE
|
||||
* statement. We use the maximum timestamp across the batch so that `lastPing`
|
||||
* reflects the most recent ping seen for any site in the group. This avoids
|
||||
* the multi-statement transaction that previously created additional
|
||||
* row-lock ordering hazards.
|
||||
* For each batch: first upserts individual per-site timestamps into
|
||||
* `sitePing`, then bulk-updates `sites.online = true`.
|
||||
*/
|
||||
async function flushSitePingsToDb(): Promise<void> {
|
||||
if (pendingSitePings.size === 0) {
|
||||
@@ -103,20 +100,25 @@ async function flushSitePingsToDb(): Promise<void> {
|
||||
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
|
||||
const batch = entries.slice(i, i + BATCH_SIZE);
|
||||
|
||||
// Use the latest timestamp in the batch so that `lastPing` always
|
||||
// moves forward. Using a single timestamp for the whole batch means
|
||||
// we only ever need one UPDATE statement (no transaction).
|
||||
const maxTimestamp = Math.max(...batch.map(([, ts]) => ts));
|
||||
const siteIds = batch.map(([id]) => id);
|
||||
|
||||
try {
|
||||
await withRetry(async () => {
|
||||
const rows = batch.map(([siteId, ts]) => ({ siteId, lastPing: ts }));
|
||||
|
||||
// Step 1: Upsert ping timestamps into sitePing
|
||||
await db
|
||||
.insert(sitePing)
|
||||
.values(rows)
|
||||
.onConflictDoUpdate({
|
||||
target: sitePing.siteId,
|
||||
set: { lastPing: sql`excluded."lastPing"` }
|
||||
});
|
||||
|
||||
// Step 2: Update online status on sites
|
||||
await db
|
||||
.update(sites)
|
||||
.set({
|
||||
online: true,
|
||||
lastPing: maxTimestamp
|
||||
})
|
||||
.set({ online: true })
|
||||
.where(inArray(sites.siteId, siteIds));
|
||||
}, "flushSitePingsToDb");
|
||||
} catch (error) {
|
||||
@@ -139,7 +141,8 @@ async function flushSitePingsToDb(): Promise<void> {
|
||||
/**
|
||||
* Flush all accumulated client (OLM) pings to the database.
|
||||
*
|
||||
* Same single-UPDATE-per-batch approach as `flushSitePingsToDb`.
|
||||
* For each batch: first upserts individual per-client timestamps into
|
||||
* `clientPing`, then bulk-updates `clients.online = true, archived = false`.
|
||||
*/
|
||||
async function flushClientPingsToDb(): Promise<void> {
|
||||
if (pendingClientPings.size === 0 && pendingOlmArchiveResets.size === 0) {
|
||||
@@ -161,18 +164,25 @@ async function flushClientPingsToDb(): Promise<void> {
|
||||
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
|
||||
const batch = entries.slice(i, i + BATCH_SIZE);
|
||||
|
||||
const maxTimestamp = Math.max(...batch.map(([, ts]) => ts));
|
||||
const clientIds = batch.map(([id]) => id);
|
||||
|
||||
try {
|
||||
await withRetry(async () => {
|
||||
const rows = batch.map(([clientId, ts]) => ({ clientId, lastPing: ts }));
|
||||
|
||||
// Step 1: Upsert ping timestamps into clientPing
|
||||
await db
|
||||
.insert(clientPing)
|
||||
.values(rows)
|
||||
.onConflictDoUpdate({
|
||||
target: clientPing.clientId,
|
||||
set: { lastPing: sql`excluded."lastPing"` }
|
||||
});
|
||||
|
||||
// Step 2: Update online + unarchive on clients
|
||||
await db
|
||||
.update(clients)
|
||||
.set({
|
||||
lastPing: maxTimestamp,
|
||||
online: true,
|
||||
archived: false
|
||||
})
|
||||
.set({ online: true, archived: false })
|
||||
.where(inArray(clients.clientId, clientIds));
|
||||
}, "flushClientPingsToDb");
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { disconnectClient, getClientConfigVersion } from "#dynamic/routers/ws";
|
||||
import { db } from "@server/db";
|
||||
import { MessageHandler } from "@server/routers/ws";
|
||||
import { clients, olms, Olm } from "@server/db";
|
||||
import { eq, lt, isNull, and, or } from "drizzle-orm";
|
||||
import { clients, olms, Olm, clientPing } from "@server/db";
|
||||
import { eq, lt, isNull, and, or, inArray } from "drizzle-orm";
|
||||
import { recordClientPing } from "@server/routers/newt/pingAccumulator";
|
||||
import logger from "@server/logger";
|
||||
import { validateSessionToken } from "@server/auth/sessions/app";
|
||||
@@ -37,21 +37,33 @@ export const startOlmOfflineChecker = (): void => {
|
||||
// TODO: WE NEED TO MAKE SURE THIS WORKS WITH DISTRIBUTED NODES ALL DOING THE SAME THING
|
||||
|
||||
// Find clients that haven't pinged in the last 2 minutes and mark them as offline
|
||||
const offlineClients = await db
|
||||
.update(clients)
|
||||
.set({ online: false })
|
||||
const staleClientRows = await db
|
||||
.select({
|
||||
clientId: clients.clientId,
|
||||
olmId: clients.olmId,
|
||||
lastPing: clientPing.lastPing
|
||||
})
|
||||
.from(clients)
|
||||
.leftJoin(clientPing, eq(clientPing.clientId, clients.clientId))
|
||||
.where(
|
||||
and(
|
||||
eq(clients.online, true),
|
||||
or(
|
||||
lt(clients.lastPing, twoMinutesAgo),
|
||||
isNull(clients.lastPing)
|
||||
lt(clientPing.lastPing, twoMinutesAgo),
|
||||
isNull(clientPing.lastPing)
|
||||
)
|
||||
)
|
||||
)
|
||||
.returning();
|
||||
);
|
||||
|
||||
for (const offlineClient of offlineClients) {
|
||||
if (staleClientRows.length > 0) {
|
||||
const staleClientIds = staleClientRows.map((c) => c.clientId);
|
||||
await db
|
||||
.update(clients)
|
||||
.set({ online: false })
|
||||
.where(inArray(clients.clientId, staleClientIds));
|
||||
}
|
||||
|
||||
for (const offlineClient of staleClientRows) {
|
||||
logger.info(
|
||||
`Kicking offline olm client ${offlineClient.clientId} due to inactivity`
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, sites } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db, sites, siteBandwidth } from "@server/db";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -60,12 +60,17 @@ export async function resetOrgBandwidth(
|
||||
}
|
||||
|
||||
await db
|
||||
.update(sites)
|
||||
.update(siteBandwidth)
|
||||
.set({
|
||||
megabytesIn: 0,
|
||||
megabytesOut: 0
|
||||
})
|
||||
.where(eq(sites.orgId, orgId));
|
||||
.where(
|
||||
inArray(
|
||||
siteBandwidth.siteId,
|
||||
db.select({ siteId: sites.siteId }).from(sites).where(eq(sites.orgId, orgId))
|
||||
)
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
data: {},
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
remoteExitNodes,
|
||||
roleSites,
|
||||
sites,
|
||||
siteBandwidth,
|
||||
userSites
|
||||
} from "@server/db";
|
||||
import cache from "#dynamic/lib/cache";
|
||||
@@ -155,8 +156,8 @@ function querySitesBase() {
|
||||
name: sites.name,
|
||||
pubKey: sites.pubKey,
|
||||
subnet: sites.subnet,
|
||||
megabytesIn: sites.megabytesIn,
|
||||
megabytesOut: sites.megabytesOut,
|
||||
megabytesIn: siteBandwidth.megabytesIn,
|
||||
megabytesOut: siteBandwidth.megabytesOut,
|
||||
orgName: orgs.name,
|
||||
type: sites.type,
|
||||
online: sites.online,
|
||||
@@ -175,7 +176,8 @@ function querySitesBase() {
|
||||
.leftJoin(
|
||||
remoteExitNodes,
|
||||
eq(remoteExitNodes.exitNodeId, sites.exitNodeId)
|
||||
);
|
||||
)
|
||||
.leftJoin(siteBandwidth, eq(siteBandwidth.siteId, sites.siteId));
|
||||
}
|
||||
|
||||
type SiteWithUpdateAvailable = Awaited<ReturnType<typeof querySitesBase>>[0] & {
|
||||
@@ -299,9 +301,15 @@ export async function listSites(
|
||||
.offset(pageSize * (page - 1))
|
||||
.orderBy(
|
||||
sort_by
|
||||
? order === "asc"
|
||||
? asc(sites[sort_by])
|
||||
: desc(sites[sort_by])
|
||||
? (() => {
|
||||
const field =
|
||||
sort_by === "megabytesIn"
|
||||
? siteBandwidth.megabytesIn
|
||||
: sort_by === "megabytesOut"
|
||||
? siteBandwidth.megabytesOut
|
||||
: sites.name;
|
||||
return order === "asc" ? asc(field) : desc(field);
|
||||
})()
|
||||
: asc(sites.name)
|
||||
);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import m13 from "./scriptsPg/1.15.3";
|
||||
import m14 from "./scriptsPg/1.15.4";
|
||||
import m15 from "./scriptsPg/1.16.0";
|
||||
import m16 from "./scriptsPg/1.17.0";
|
||||
import m17 from "./scriptsPg/1.18.0";
|
||||
|
||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||
@@ -43,7 +44,8 @@ const migrations = [
|
||||
{ version: "1.15.3", run: m13 },
|
||||
{ version: "1.15.4", run: m14 },
|
||||
{ version: "1.16.0", run: m15 },
|
||||
{ version: "1.17.0", run: m16 }
|
||||
{ version: "1.17.0", run: m16 },
|
||||
{ version: "1.18.0", run: m17 }
|
||||
// Add new migrations here as they are created
|
||||
] as {
|
||||
version: string;
|
||||
|
||||
@@ -40,6 +40,7 @@ import m34 from "./scriptsSqlite/1.15.3";
|
||||
import m35 from "./scriptsSqlite/1.15.4";
|
||||
import m36 from "./scriptsSqlite/1.16.0";
|
||||
import m37 from "./scriptsSqlite/1.17.0";
|
||||
import m38 from "./scriptsSqlite/1.18.0";
|
||||
|
||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||
@@ -77,7 +78,8 @@ const migrations = [
|
||||
{ version: "1.15.3", run: m34 },
|
||||
{ version: "1.15.4", run: m35 },
|
||||
{ version: "1.16.0", run: m36 },
|
||||
{ version: "1.17.0", run: m37 }
|
||||
{ version: "1.17.0", run: m37 },
|
||||
{ version: "1.18.0", run: m38 }
|
||||
// Add new migrations here as they are created
|
||||
] as const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user