mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-29 15:31:25 +02:00
Compare commits
50 Commits
babd90ae71
...
bc63747efe
| Author | SHA1 | Date | |
|---|---|---|---|
| bc63747efe | |||
| 2a8ceeec1b | |||
| 91ef0d0153 | |||
| b8101402cd | |||
| 7731849a2f | |||
| c11d24e10a | |||
| a9b7cce49b | |||
| d78223b94f | |||
| 963e9da7dd | |||
| 2cbc88fa05 | |||
| 65bad456cb | |||
| f48a4f7bc0 | |||
| ce3c2f7583 | |||
| 51c357e6c7 | |||
| 7ae29612d4 | |||
| da794adb7d | |||
| 8004ae6870 | |||
| 1bff7bbc2f | |||
| 50db5695fc | |||
| cfb63f9742 | |||
| c76b4555e1 | |||
| c25bfbad27 | |||
| 44782f8963 | |||
| e6f7cd6da9 | |||
| 19faa3a29c | |||
| c284dc2e83 | |||
| 1b634955d8 | |||
| be888c3fc1 | |||
| 3f2bb42221 | |||
| 5dc3ae4c7f | |||
| ffb6c64de0 | |||
| 2cbc6fb128 | |||
| 75084028d7 | |||
| f44a7c55dd | |||
| 753358a17d | |||
| d747b45f0b | |||
| a24091257a | |||
| 1c60041390 | |||
| 95c3f74a33 | |||
| cedccd8cdb | |||
| 7a275c86c2 | |||
| 4b703b5c11 | |||
| 1b6e9e8cfe | |||
| fe55956079 | |||
| 4cd0b9a0bb | |||
| ab4d567af9 | |||
| 38203e522b | |||
| 13b691fd7d | |||
| 89f3f3c8cd | |||
| 44c16d69af |
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Monitor storage space
|
||||
run: |
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Monitor storage space
|
||||
run: |
|
||||
@@ -201,7 +201,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Extract tag name
|
||||
id: get-tag
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Build Docker image sqlite
|
||||
run: make dev-build-sqlite
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Build Docker image pg
|
||||
run: make dev-build-pg
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ go 1.25.0
|
||||
require (
|
||||
github.com/charmbracelet/huh v1.0.0
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
golang.org/x/term v0.43.0
|
||||
golang.org/x/term v0.44.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -33,6 +33,6 @@ require (
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
)
|
||||
|
||||
+4
-4
@@ -69,10 +69,10 @@ golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
|
||||
@@ -2338,6 +2338,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "The CIDR range of the resource on the site's network.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "An optional internal DNS alias for this resource.",
|
||||
"internalResourceAliasLocalWarning": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "Scheme is required for HTTP resources",
|
||||
"internalResourceHttpPortRequired": "Destination port is required for HTTP resources",
|
||||
"siteConfiguration": "Configuration",
|
||||
@@ -2967,6 +2968,7 @@
|
||||
"orgOrDomainIdMissing": "Organization or Domain ID is missing",
|
||||
"loadingDNSRecords": "Loading DNS records...",
|
||||
"olmUpdateAvailableInfo": "An updated version of Olm is available. Please update to the latest version for the best experience.",
|
||||
"updateAvailableInfo": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"client": "Client",
|
||||
"proxyProtocol": "Proxy Protocol Settings",
|
||||
"proxyProtocolDescription": "Configure Proxy Protocol to preserve client IP addresses for TCP services.",
|
||||
|
||||
Generated
+200
-238
@@ -70,7 +70,7 @@
|
||||
"input-otp": "1.4.2",
|
||||
"ioredis": "5.11.0",
|
||||
"jmespath": "0.16.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"js-yaml": "4.2.0",
|
||||
"jsonwebtoken": "9.0.3",
|
||||
"lucide-react": "1.17.0",
|
||||
"maxmind": "5.0.6",
|
||||
@@ -80,7 +80,7 @@
|
||||
"next-themes": "0.4.6",
|
||||
"nextjs-toploader": "3.9.17",
|
||||
"node-cache": "5.1.2",
|
||||
"nodemailer": "8.0.9",
|
||||
"nodemailer": "9.0.1",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "8.21.0",
|
||||
"posthog-node": "5.35.6",
|
||||
@@ -142,7 +142,7 @@
|
||||
"@types/yargs": "17.0.35",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"drizzle-kit": "0.31.10",
|
||||
"esbuild": "0.28.0",
|
||||
"esbuild": "0.28.1",
|
||||
"esbuild-node-externals": "1.22.0",
|
||||
"eslint": "10.4.0",
|
||||
"eslint-config-next": "16.2.6",
|
||||
@@ -1248,9 +1248,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1265,9 +1265,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1282,9 +1282,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1299,9 +1299,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1316,9 +1316,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1333,9 +1333,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1350,9 +1350,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1367,9 +1367,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1384,9 +1384,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1401,9 +1401,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1418,9 +1418,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1435,9 +1435,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
|
||||
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
||||
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -1452,9 +1452,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
|
||||
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||||
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -1469,9 +1469,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1486,9 +1486,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
|
||||
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||||
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1503,9 +1503,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
|
||||
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||||
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -1520,9 +1520,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1537,9 +1537,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1554,9 +1554,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1571,9 +1571,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1588,9 +1588,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1605,9 +1605,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1622,9 +1622,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1639,9 +1639,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1656,9 +1656,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1673,9 +1673,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2076,9 +2076,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2095,9 +2092,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2114,9 +2108,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2133,9 +2124,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2152,9 +2140,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2187,9 +2172,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2222,9 +2204,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2247,9 +2226,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2272,9 +2248,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2297,9 +2270,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2322,9 +2292,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2369,9 +2336,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2664,9 +2628,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2683,9 +2644,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2941,9 +2899,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2960,9 +2915,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3200,9 +3152,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3219,9 +3168,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3582,9 +3528,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3605,9 +3548,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3628,9 +3568,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3651,9 +3588,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6873,9 +6807,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6892,9 +6823,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6911,9 +6839,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6930,9 +6855,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7200,9 +7122,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7220,9 +7139,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7296,6 +7212,72 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.10.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.4",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz",
|
||||
@@ -8448,9 +8430,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8465,9 +8444,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8482,9 +8458,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8499,9 +8472,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8516,9 +8486,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8533,9 +8500,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11261,9 +11225,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
|
||||
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -11274,32 +11238,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.0",
|
||||
"@esbuild/android-arm": "0.28.0",
|
||||
"@esbuild/android-arm64": "0.28.0",
|
||||
"@esbuild/android-x64": "0.28.0",
|
||||
"@esbuild/darwin-arm64": "0.28.0",
|
||||
"@esbuild/darwin-x64": "0.28.0",
|
||||
"@esbuild/freebsd-arm64": "0.28.0",
|
||||
"@esbuild/freebsd-x64": "0.28.0",
|
||||
"@esbuild/linux-arm": "0.28.0",
|
||||
"@esbuild/linux-arm64": "0.28.0",
|
||||
"@esbuild/linux-ia32": "0.28.0",
|
||||
"@esbuild/linux-loong64": "0.28.0",
|
||||
"@esbuild/linux-mips64el": "0.28.0",
|
||||
"@esbuild/linux-ppc64": "0.28.0",
|
||||
"@esbuild/linux-riscv64": "0.28.0",
|
||||
"@esbuild/linux-s390x": "0.28.0",
|
||||
"@esbuild/linux-x64": "0.28.0",
|
||||
"@esbuild/netbsd-arm64": "0.28.0",
|
||||
"@esbuild/netbsd-x64": "0.28.0",
|
||||
"@esbuild/openbsd-arm64": "0.28.0",
|
||||
"@esbuild/openbsd-x64": "0.28.0",
|
||||
"@esbuild/openharmony-arm64": "0.28.0",
|
||||
"@esbuild/sunos-x64": "0.28.0",
|
||||
"@esbuild/win32-arm64": "0.28.0",
|
||||
"@esbuild/win32-ia32": "0.28.0",
|
||||
"@esbuild/win32-x64": "0.28.0"
|
||||
"@esbuild/aix-ppc64": "0.28.1",
|
||||
"@esbuild/android-arm": "0.28.1",
|
||||
"@esbuild/android-arm64": "0.28.1",
|
||||
"@esbuild/android-x64": "0.28.1",
|
||||
"@esbuild/darwin-arm64": "0.28.1",
|
||||
"@esbuild/darwin-x64": "0.28.1",
|
||||
"@esbuild/freebsd-arm64": "0.28.1",
|
||||
"@esbuild/freebsd-x64": "0.28.1",
|
||||
"@esbuild/linux-arm": "0.28.1",
|
||||
"@esbuild/linux-arm64": "0.28.1",
|
||||
"@esbuild/linux-ia32": "0.28.1",
|
||||
"@esbuild/linux-loong64": "0.28.1",
|
||||
"@esbuild/linux-mips64el": "0.28.1",
|
||||
"@esbuild/linux-ppc64": "0.28.1",
|
||||
"@esbuild/linux-riscv64": "0.28.1",
|
||||
"@esbuild/linux-s390x": "0.28.1",
|
||||
"@esbuild/linux-x64": "0.28.1",
|
||||
"@esbuild/netbsd-arm64": "0.28.1",
|
||||
"@esbuild/netbsd-x64": "0.28.1",
|
||||
"@esbuild/openbsd-arm64": "0.28.1",
|
||||
"@esbuild/openbsd-x64": "0.28.1",
|
||||
"@esbuild/openharmony-arm64": "0.28.1",
|
||||
"@esbuild/sunos-x64": "0.28.1",
|
||||
"@esbuild/win32-arm64": "0.28.1",
|
||||
"@esbuild/win32-ia32": "0.28.1",
|
||||
"@esbuild/win32-x64": "0.28.1"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-node-externals": {
|
||||
@@ -12191,16 +12155,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -12629,9 +12593,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
|
||||
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
@@ -13425,9 +13389,19 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
|
||||
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodeca"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
@@ -13767,9 +13741,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -13791,9 +13762,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -14574,9 +14542,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "8.0.9",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.9.tgz",
|
||||
"integrity": "sha512-5ofa7BUN8+C+Hckh5V2GjeeOGRQBx0CJQA6KxrvuZfC8iU4/q7sLn8XrtEEhJkjV6HdyIiQs7Bba6bTao8JhkA==",
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz",
|
||||
"integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -15001,9 +14969,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -15020,9 +14985,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
+4
-4
@@ -93,7 +93,7 @@
|
||||
"input-otp": "1.4.2",
|
||||
"ioredis": "5.11.0",
|
||||
"jmespath": "0.16.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"js-yaml": "4.2.0",
|
||||
"jsonwebtoken": "9.0.3",
|
||||
"lucide-react": "1.17.0",
|
||||
"maxmind": "5.0.6",
|
||||
@@ -103,7 +103,7 @@
|
||||
"next-themes": "0.4.6",
|
||||
"nextjs-toploader": "3.9.17",
|
||||
"node-cache": "5.1.2",
|
||||
"nodemailer": "8.0.9",
|
||||
"nodemailer": "9.0.1",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "8.21.0",
|
||||
"posthog-node": "5.35.6",
|
||||
@@ -165,7 +165,7 @@
|
||||
"@types/yargs": "17.0.35",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"drizzle-kit": "0.31.10",
|
||||
"esbuild": "0.28.0",
|
||||
"esbuild": "0.28.1",
|
||||
"esbuild-node-externals": "1.22.0",
|
||||
"eslint": "10.4.0",
|
||||
"eslint-config-next": "16.2.6",
|
||||
@@ -179,7 +179,7 @@
|
||||
"typescript-eslint": "8.60.0"
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "0.28.0",
|
||||
"esbuild": "0.28.1",
|
||||
"dompurify": "3.4.0",
|
||||
"postcss": "8.5.15"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
newts,
|
||||
blueprints,
|
||||
Blueprint,
|
||||
Site,
|
||||
siteResources,
|
||||
roleSiteResources,
|
||||
userSiteResources,
|
||||
@@ -60,30 +59,26 @@ export async function applyBlueprint({
|
||||
|
||||
const config: Config = validationResult.data;
|
||||
|
||||
let proxyResourcesResults: PublicResourcesResults = [];
|
||||
let clientResourcesResults: ClientResourcesResults = [];
|
||||
let publicResourcesResults: PublicResourcesResults = [];
|
||||
let privateResourcesResults: ClientResourcesResults = [];
|
||||
await db.transaction(async (trx) => {
|
||||
await updateResourcePolicies(orgId, config, trx);
|
||||
|
||||
proxyResourcesResults = await updatePublicResources(
|
||||
publicResourcesResults = await updatePublicResources(
|
||||
orgId,
|
||||
config,
|
||||
trx,
|
||||
siteId
|
||||
);
|
||||
clientResourcesResults = await updatePrivateResources(
|
||||
privateResourcesResults = await updatePrivateResources(
|
||||
orgId,
|
||||
config,
|
||||
trx,
|
||||
siteId
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`Successfully updated proxy resources for org ${orgId}: ${JSON.stringify(proxyResourcesResults)}`
|
||||
);
|
||||
|
||||
// We need to update the targets on the newts from the successfully updated information
|
||||
for (const result of proxyResourcesResults) {
|
||||
for (const result of publicResourcesResults) {
|
||||
for (const target of result.targetsToUpdate) {
|
||||
const [site] = await trx
|
||||
.select()
|
||||
@@ -136,166 +131,38 @@ export async function applyBlueprint({
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Successfully updated client resources for org ${orgId}: ${JSON.stringify(clientResourcesResults)}`
|
||||
`Successfully updated public resources for org ${orgId}: ${JSON.stringify(publicResourcesResults)}`
|
||||
);
|
||||
|
||||
// We need to update the targets on the newts from the successfully updated information
|
||||
for (const result of clientResourcesResults) {
|
||||
if (
|
||||
result.oldSiteResource &&
|
||||
JSON.stringify(result.newSites?.sort()) !==
|
||||
JSON.stringify(result.oldSites?.sort())
|
||||
) {
|
||||
// query existing associations
|
||||
const existingRoleIds = await trx
|
||||
.select()
|
||||
.from(roleSiteResources)
|
||||
.where(
|
||||
eq(
|
||||
roleSiteResources.siteResourceId,
|
||||
result.oldSiteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
.then((rows) => rows.map((row) => row.roleId));
|
||||
|
||||
const existingUserIds = await trx
|
||||
.select()
|
||||
.from(userSiteResources)
|
||||
.where(
|
||||
eq(
|
||||
userSiteResources.siteResourceId,
|
||||
result.oldSiteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
.then((rows) => rows.map((row) => row.userId));
|
||||
|
||||
const existingClientIds = await trx
|
||||
.select()
|
||||
.from(clientSiteResources)
|
||||
.where(
|
||||
eq(
|
||||
clientSiteResources.siteResourceId,
|
||||
result.oldSiteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
.then((rows) => rows.map((row) => row.clientId));
|
||||
|
||||
// delete the existing site resource
|
||||
await trx
|
||||
.delete(siteResources)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
siteResources.siteResourceId,
|
||||
result.oldSiteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(
|
||||
result.oldSiteResource,
|
||||
trx
|
||||
for (const result of privateResourcesResults) {
|
||||
rebuildClientAssociationsFromSiteResource(
|
||||
result.newSiteResource
|
||||
).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for site resource ${result.newSiteResource.siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
const [insertedSiteResource] = await trx
|
||||
.insert(siteResources)
|
||||
.values({
|
||||
...result.newSiteResource
|
||||
})
|
||||
.returning();
|
||||
|
||||
// wait some time to allow for messages to be handled
|
||||
await new Promise((resolve) => setTimeout(resolve, 750));
|
||||
|
||||
//////////////////// update the associations ////////////////////
|
||||
|
||||
if (existingRoleIds.length > 0) {
|
||||
await trx.insert(roleSiteResources).values(
|
||||
existingRoleIds.map((roleId) => ({
|
||||
roleId,
|
||||
siteResourceId:
|
||||
insertedSiteResource!.siteResourceId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
if (existingUserIds.length > 0) {
|
||||
await trx.insert(userSiteResources).values(
|
||||
existingUserIds.map((userId) => ({
|
||||
userId,
|
||||
siteResourceId:
|
||||
insertedSiteResource!.siteResourceId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
if (existingClientIds.length > 0) {
|
||||
await trx.insert(clientSiteResources).values(
|
||||
existingClientIds.map((clientId) => ({
|
||||
clientId,
|
||||
siteResourceId:
|
||||
insertedSiteResource!.siteResourceId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(
|
||||
insertedSiteResource,
|
||||
trx
|
||||
handleMessagingForUpdatedSiteResource(
|
||||
result.oldSiteResource,
|
||||
result.newSiteResource,
|
||||
result.oldSites.map((site) => ({
|
||||
// only need to run this on the old sites because the new sites are added above
|
||||
siteId: site.siteId,
|
||||
orgId: result.newSiteResource.orgId
|
||||
}))
|
||||
).catch((err) => {
|
||||
logger.error(
|
||||
`Error handling messaging for updated site resource ${result.newSiteResource.siteResourceId}:`,
|
||||
err
|
||||
);
|
||||
} else {
|
||||
let good = true;
|
||||
for (const newSite of result.newSites) {
|
||||
const [site] = await trx
|
||||
.select()
|
||||
.from(sites)
|
||||
.innerJoin(newts, eq(sites.siteId, newts.siteId))
|
||||
.where(
|
||||
and(
|
||||
eq(sites.siteId, newSite.siteId),
|
||||
eq(sites.orgId, orgId),
|
||||
eq(sites.type, "newt"),
|
||||
isNotNull(sites.pubKey)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!site) {
|
||||
logger.debug(
|
||||
`No newt sites found for client resource ${result.newSiteResource.siteResourceId}, skipping target update`
|
||||
);
|
||||
good = false;
|
||||
break;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Updating client resource ${result.newSiteResource.siteResourceId} on site ${newSite.siteId}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!good) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await handleMessagingForUpdatedSiteResource(
|
||||
result.oldSiteResource,
|
||||
result.newSiteResource,
|
||||
result.newSites.map((site) => ({
|
||||
siteId: site.siteId,
|
||||
orgId: result.newSiteResource.orgId
|
||||
})),
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
// await addClientTargets(
|
||||
// site.newt.newtId,
|
||||
// result.resource.destination,
|
||||
// result.resource.destinationPort,
|
||||
// result.resource.protocol,
|
||||
// result.resource.proxyPort
|
||||
// );
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Successfully updated private resources for org ${orgId}: ${JSON.stringify(privateResourcesResults)}`
|
||||
);
|
||||
});
|
||||
|
||||
blueprintSucceeded = true;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
db,
|
||||
olms,
|
||||
orgs,
|
||||
primaryDb,
|
||||
roleClients,
|
||||
roles,
|
||||
Transaction,
|
||||
@@ -23,10 +24,44 @@ import { rebuildClientAssociationsFromClient } from "./rebuildClientAssociations
|
||||
import { OlmErrorCodes } from "@server/routers/olm/error";
|
||||
import { tierMatrix } from "./billing/tierMatrix";
|
||||
|
||||
export async function calculateUserClientsForOrgs(
|
||||
type ClientRow = typeof clients.$inferSelect;
|
||||
|
||||
function runQueuedClientAssociationRebuilds(
|
||||
userId: string,
|
||||
trx: Transaction | typeof db = db
|
||||
queuedClients: ClientRow[]
|
||||
): void {
|
||||
if (queuedClients.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uniqueClientsById = new Map<number, ClientRow>();
|
||||
for (const client of queuedClients) {
|
||||
uniqueClientsById.set(client.clientId, client);
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
for (const client of uniqueClientsById.values()) {
|
||||
try {
|
||||
await rebuildClientAssociationsFromClient(client);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed rebuilding associations for client ${client.clientId} (user ${userId}): ${String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Queued association rebuild completed for ${uniqueClientsById.size} client(s) (user ${userId})`
|
||||
);
|
||||
})();
|
||||
}
|
||||
|
||||
export async function calculateUserClientsForOrgs(
|
||||
userId: string
|
||||
): Promise<void> {
|
||||
const trx = primaryDb;
|
||||
const queuedAssociationRebuilds: ClientRow[] = [];
|
||||
|
||||
const execute = async (transaction: Transaction | typeof db) => {
|
||||
const orgCache = new Map<string, typeof orgs.$inferSelect | null>();
|
||||
const adminRoleCache = new Map<
|
||||
@@ -189,7 +224,12 @@ export async function calculateUserClientsForOrgs(
|
||||
|
||||
if (userOlms.length === 0) {
|
||||
// No OLMs for this user, but we should still clean up any orphaned clients
|
||||
await cleanupOrphanedClients(userId, transaction);
|
||||
await cleanupOrphanedClients(
|
||||
userId,
|
||||
transaction,
|
||||
[],
|
||||
queuedAssociationRebuilds
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -382,10 +422,7 @@ export async function calculateUserClientsForOrgs(
|
||||
.returning();
|
||||
}
|
||||
|
||||
await rebuildClientAssociationsFromClient(
|
||||
newClient,
|
||||
transaction
|
||||
);
|
||||
queuedAssociationRebuilds.push(newClient);
|
||||
|
||||
// Grant admin role access to the client
|
||||
await transaction.insert(roleClients).values({
|
||||
@@ -414,24 +451,22 @@ export async function calculateUserClientsForOrgs(
|
||||
}
|
||||
|
||||
// Clean up clients in orgs the user is no longer in
|
||||
await cleanupOrphanedClients(userId, transaction, userOrgIds);
|
||||
await cleanupOrphanedClients(
|
||||
userId,
|
||||
transaction,
|
||||
userOrgIds,
|
||||
queuedAssociationRebuilds
|
||||
);
|
||||
};
|
||||
|
||||
if (trx) {
|
||||
// Use provided transaction
|
||||
await execute(trx);
|
||||
} else {
|
||||
// Create new transaction
|
||||
await db.transaction(async (transaction) => {
|
||||
await execute(transaction);
|
||||
});
|
||||
}
|
||||
runQueuedClientAssociationRebuilds(userId, queuedAssociationRebuilds);
|
||||
}
|
||||
|
||||
async function cleanupOrphanedClients(
|
||||
userId: string,
|
||||
trx: Transaction | typeof db,
|
||||
userOrgIds: string[] = []
|
||||
userOrgIds: string[] = [],
|
||||
queuedAssociationRebuilds: ClientRow[] = []
|
||||
): Promise<void> {
|
||||
// Find all OLM clients for this user that should be deleted
|
||||
// If userOrgIds is empty, delete all OLM clients (user has no orgs)
|
||||
@@ -461,9 +496,9 @@ async function cleanupOrphanedClients(
|
||||
)
|
||||
.returning();
|
||||
|
||||
// Rebuild associations for each deleted client to clean up related data
|
||||
// Queue deleted clients for post-transaction association cleanup.
|
||||
for (const deletedClient of deletedClients) {
|
||||
await rebuildClientAssociationsFromClient(deletedClient, trx);
|
||||
queuedAssociationRebuilds.push(deletedClient);
|
||||
|
||||
if (deletedClient.olmId) {
|
||||
await sendTerminateClient(
|
||||
|
||||
@@ -160,9 +160,9 @@ export async function getClientSiteResourceAccess(
|
||||
}
|
||||
|
||||
export async function rebuildClientAssociationsFromSiteResource(
|
||||
siteResource: SiteResource,
|
||||
trx: Transaction | typeof db = db
|
||||
siteResource: SiteResource
|
||||
) {
|
||||
const trx = primaryDb;
|
||||
try {
|
||||
return await lockManager.withLock(
|
||||
`rebuild-client-associations:site-resource:${siteResource.siteResourceId}`,
|
||||
@@ -1043,9 +1043,9 @@ async function handleSubnetProxyTargetUpdates(
|
||||
}
|
||||
|
||||
export async function rebuildClientAssociationsFromClient(
|
||||
client: Client,
|
||||
trx: Transaction | typeof db = db
|
||||
client: Client
|
||||
): Promise<void> {
|
||||
const trx = primaryDb;
|
||||
try {
|
||||
return await lockManager.withLock(
|
||||
`rebuild-client-associations:client:${client.clientId}`,
|
||||
@@ -2119,10 +2119,7 @@ export function startRebuildQueueProcessor(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(
|
||||
siteResource,
|
||||
primaryDb
|
||||
);
|
||||
await rebuildClientAssociationsFromSiteResource(siteResource);
|
||||
},
|
||||
onClient: async (clientId: number) => {
|
||||
const [client] = await primaryDb
|
||||
@@ -2137,7 +2134,7 @@ export function startRebuildQueueProcessor(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
await rebuildClientAssociationsFromClient(client, primaryDb);
|
||||
await rebuildClientAssociationsFromClient(client);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
import { redis } from "#private/lib/redis";
|
||||
import { lockManager } from "#dynamic/lib/lock";
|
||||
import { lockManager } from "#private/lib/lock";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export type RebuildJobType = "site-resource" | "client";
|
||||
|
||||
@@ -29,26 +29,40 @@ const paramsSchema = z.strictObject({
|
||||
orgId: z.string().nonempty()
|
||||
});
|
||||
|
||||
const bodySchema = z.strictObject({
|
||||
name: z.string().nonempty(),
|
||||
siteId: z.number().int().positive(),
|
||||
hcEnabled: z.boolean().default(false),
|
||||
hcMode: z.string().default("http"),
|
||||
hcHostname: z.string().optional(),
|
||||
hcPort: z.number().int().min(1).max(65535).optional(),
|
||||
hcPath: z.string().optional(),
|
||||
hcScheme: z.string().optional(),
|
||||
hcMethod: z.string().default("GET"),
|
||||
hcInterval: z.number().int().positive().default(30),
|
||||
hcUnhealthyInterval: z.number().int().positive().default(30),
|
||||
hcTimeout: z.number().int().positive().default(1),
|
||||
hcHeaders: z.string().optional().nullable(),
|
||||
hcFollowRedirects: z.boolean().default(true),
|
||||
hcStatus: z.number().int().optional().nullable(),
|
||||
hcTlsServerName: z.string().optional(),
|
||||
hcHealthyThreshold: z.number().int().positive().default(1),
|
||||
hcUnhealthyThreshold: z.number().int().positive().default(1)
|
||||
});
|
||||
const bodySchema = z
|
||||
.strictObject({
|
||||
name: z.string().nonempty(),
|
||||
siteId: z.number().int().positive(),
|
||||
hcEnabled: z.boolean().default(false),
|
||||
hcMode: z.string().default("http"),
|
||||
hcHostname: z.string().optional(),
|
||||
hcPort: z.number().int().min(1).max(65535).optional(),
|
||||
hcPath: z.string().optional(),
|
||||
hcScheme: z.string().optional(),
|
||||
hcMethod: z.string().default("GET"),
|
||||
hcInterval: z.number().int().positive().default(30),
|
||||
hcUnhealthyInterval: z.number().int().positive().default(30),
|
||||
hcTimeout: z.number().int().positive().default(1),
|
||||
hcHeaders: z.string().optional().nullable(),
|
||||
hcFollowRedirects: z.boolean().default(true),
|
||||
hcStatus: z.number().int().optional().nullable(),
|
||||
hcTlsServerName: z.string().optional(),
|
||||
hcHealthyThreshold: z.number().int().positive().default(1),
|
||||
hcUnhealthyThreshold: z.number().int().positive().default(1)
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
const hcHostnameMissing =
|
||||
data.hcHostname === undefined ||
|
||||
data.hcHostname.trim().length === 0;
|
||||
|
||||
if (data.hcEnabled === true && hcHostnameMissing) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["hcHostname"],
|
||||
message: "hcHostname is required when hcEnabled is true"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type CreateHealthCheckResponse = {
|
||||
targetHealthCheckId: number;
|
||||
@@ -57,7 +71,6 @@ const CreateHealthCheckResponseDataSchema = z.object({
|
||||
targetHealthCheckId: z.number()
|
||||
});
|
||||
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/health-check",
|
||||
@@ -78,7 +91,9 @@ registry.registerPath({
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createApiResponseSchema(CreateHealthCheckResponseDataSchema)
|
||||
schema: createApiResponseSchema(
|
||||
CreateHealthCheckResponseDataSchema
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,6 @@ const UpdateHealthCheckResponseDataSchema = z.object({
|
||||
hcUnhealthyThreshold: z.number().nullable()
|
||||
});
|
||||
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/org/{orgId}/health-check/{healthCheckId}",
|
||||
@@ -126,7 +125,9 @@ registry.registerPath({
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createApiResponseSchema(UpdateHealthCheckResponseDataSchema)
|
||||
schema: createApiResponseSchema(
|
||||
UpdateHealthCheckResponseDataSchema
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,6 +216,32 @@ export async function updateHealthCheck(
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!existingHealthCheck) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"Standalone health check not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const nextHcEnabled = hcEnabled ?? existingHealthCheck.hcEnabled;
|
||||
const nextHcHostname =
|
||||
hcHostname !== undefined
|
||||
? hcHostname
|
||||
: existingHealthCheck.hcHostname;
|
||||
const hcHostnameMissing =
|
||||
!nextHcHostname || nextHcHostname.trim().length === 0;
|
||||
|
||||
if (nextHcEnabled && hcHostnameMissing) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"hcHostname is required when hcEnabled is true"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (name !== undefined) updateData.name = name;
|
||||
if (siteId !== undefined) updateData.siteId = siteId;
|
||||
if (hcEnabled !== undefined) updateData.hcEnabled = hcEnabled;
|
||||
|
||||
@@ -121,7 +121,7 @@ export async function unassociateOrgIdp(
|
||||
});
|
||||
|
||||
for (const userId of userIdsToRemove) {
|
||||
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||
calculateUserClientsForOrgs(userId).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to calculate user clients after removing user ${userId} from org ${orgId} during IdP unassociation: ${e}`
|
||||
);
|
||||
|
||||
@@ -163,13 +163,11 @@ export async function addUserRole(
|
||||
});
|
||||
|
||||
for (const orgClient of orgClientsToRebuild) {
|
||||
rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for client ${orgClient.clientId} after adding role: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
rebuildClientAssociationsFromClient(orgClient).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for client ${orgClient.clientId} after adding role: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -170,13 +170,11 @@ export async function removeUserRole(
|
||||
});
|
||||
|
||||
for (const orgClient of orgClientsToRebuild) {
|
||||
rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for client ${orgClient.clientId} after removing role: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
rebuildClientAssociationsFromClient(orgClient).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for client ${orgClient.clientId} after removing role: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -150,13 +150,11 @@ export async function setUserOrgRoles(
|
||||
});
|
||||
|
||||
for (const orgClient of orgClientsToRebuild) {
|
||||
rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for client ${orgClient.clientId} after setting roles: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
rebuildClientAssociationsFromClient(orgClient).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for client ${orgClient.clientId} after setting roles: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -30,7 +30,7 @@ const listAccessTokensParamsSchema = z
|
||||
error: "Either resourceId or orgId must be provided, but not both"
|
||||
});
|
||||
|
||||
const listAccessTokensSchema = z.object({
|
||||
const listAccessTokensSchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -15,7 +15,7 @@ const paramsSchema = z.object({
|
||||
apiKeyId: z.string().nonempty()
|
||||
});
|
||||
|
||||
const querySchema = z.object({
|
||||
const querySchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -11,7 +11,7 @@ import { eq, and } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
|
||||
|
||||
const querySchema = z.object({
|
||||
const querySchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -9,7 +9,7 @@ import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
const querySchema = z.object({
|
||||
const querySchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -20,7 +20,7 @@ import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||
|
||||
export const queryAccessAuditLogsQuery = z.object({
|
||||
export const queryAccessAuditLogsQuery = z.strictObject({
|
||||
// iso string just validate its a parseable date
|
||||
timeStart: z
|
||||
.string()
|
||||
|
||||
@@ -224,7 +224,7 @@ export async function deleteMyAccount(
|
||||
}
|
||||
});
|
||||
|
||||
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||
calculateUserClientsForOrgs(userId).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to calculate user clients after deleting account for user ${userId}: ${e}`
|
||||
);
|
||||
|
||||
@@ -280,13 +280,11 @@ export async function createClient(
|
||||
});
|
||||
|
||||
if (newClient) {
|
||||
rebuildClientAssociationsFromClient(newClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations after creating client: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
rebuildClientAssociationsFromClient(newClient).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations after creating client: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return response<CreateClientResponse>(res, {
|
||||
|
||||
@@ -255,13 +255,11 @@ export async function createUserClient(
|
||||
});
|
||||
|
||||
if (newClient) {
|
||||
rebuildClientAssociationsFromClient(newClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations after creating user client: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
rebuildClientAssociationsFromClient(newClient).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations after creating user client: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return response<CreateClientAndOlmResponse>(res, {
|
||||
|
||||
@@ -109,13 +109,11 @@ export async function deleteClient(
|
||||
});
|
||||
|
||||
if (deletedClient) {
|
||||
rebuildClientAssociationsFromClient(deletedClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations after deleting client ${clientId}: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
rebuildClientAssociationsFromClient(deletedClient).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations after deleting client ${clientId}: ${e}`
|
||||
);
|
||||
});
|
||||
if (olm) {
|
||||
sendTerminateClient(
|
||||
deletedClient.clientId,
|
||||
|
||||
@@ -41,7 +41,7 @@ const listClientsParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const listClientsSchema = z.object({
|
||||
const listClientsSchema = z.strictObject({
|
||||
pageSize: z.coerce
|
||||
.number<string>() // for prettier formatting
|
||||
.int()
|
||||
|
||||
@@ -40,7 +40,7 @@ const listUserDevicesParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const listUserDevicesSchema = z.object({
|
||||
const listUserDevicesSchema = z.strictObject({
|
||||
pageSize: z.coerce
|
||||
.number<string>() // for prettier formatting
|
||||
.int()
|
||||
@@ -420,31 +420,6 @@ export async function listUserDevices(
|
||||
}
|
||||
);
|
||||
|
||||
// REMOVING THIS BECAUSE WE HAVE DIFFERENT TYPES OF CLIENTS NOW
|
||||
// // Try to get the latest version, but don't block if it fails
|
||||
// try {
|
||||
// const latestOlmVersion = await getLatestOlmVersion();
|
||||
|
||||
// if (latestOlmVersion) {
|
||||
// olmsWithUpdates.forEach((client) => {
|
||||
// try {
|
||||
// client.olmUpdateAvailable = semver.lt(
|
||||
// client.olmVersion ? client.olmVersion : "",
|
||||
// latestOlmVersion
|
||||
// );
|
||||
// } catch (error) {
|
||||
// client.olmUpdateAvailable = false;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// } catch (error) {
|
||||
// // Log the error but don't let it block the response
|
||||
// logger.warn(
|
||||
// "Failed to check for OLM updates, continuing without update info:",
|
||||
// error
|
||||
// );
|
||||
// }
|
||||
|
||||
return response<ListUserDevicesResponse>(res, {
|
||||
data: {
|
||||
devices: olmsWithUpdates,
|
||||
|
||||
@@ -60,13 +60,17 @@ export async function rebuildClientAssociationsCacheRoute(
|
||||
);
|
||||
}
|
||||
|
||||
await rebuildClientAssociationsFromClient(client);
|
||||
rebuildClientAssociationsFromClient(client).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for client ${clientId}: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Client association cache rebuilt successfully",
|
||||
message: "Client association cache queued successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -635,7 +635,7 @@ export async function validateOidcCallback(
|
||||
}
|
||||
});
|
||||
|
||||
calculateUserClientsForOrgs(userId!, primaryDb).catch((err) => {
|
||||
calculateUserClientsForOrgs(userId!).catch((err) => {
|
||||
logger.error(
|
||||
"Error calculating user clients after syncing orgs and roles for OIDC user",
|
||||
{ error: err }
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
verifyApiKey,
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction,
|
||||
verifyApiKeyCanSetUserOrgRoles,
|
||||
verifyApiKeySiteAccess,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyTargetAccess,
|
||||
@@ -974,6 +973,13 @@ authenticated.get(
|
||||
idp.getIdp
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/idp/:idpId",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteIdp),
|
||||
idp.deleteIdp
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyApiKeyIsRoot,
|
||||
|
||||
@@ -104,7 +104,7 @@ export async function createUserOlm(
|
||||
dateCreated: moment().toISOString()
|
||||
});
|
||||
|
||||
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||
calculateUserClientsForOrgs(userId).catch((e) => {
|
||||
console.error(
|
||||
"Error calculating user clients after creating olm:",
|
||||
e
|
||||
|
||||
@@ -86,13 +86,11 @@ export async function deleteUserOlm(
|
||||
});
|
||||
|
||||
if (deletedClient) {
|
||||
rebuildClientAssociationsFromClient(deletedClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client-site associations after deleting OLM ${olmId}: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
rebuildClientAssociationsFromClient(deletedClient).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client-site associations after deleting OLM ${olmId}: ${e}`
|
||||
);
|
||||
});
|
||||
sendTerminateClient(
|
||||
deletedClient.clientId,
|
||||
OlmErrorCodes.TERMINATED_DELETED,
|
||||
|
||||
@@ -11,7 +11,7 @@ import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { getUserDeviceName } from "@server/db/names";
|
||||
|
||||
const querySchema = z.object({
|
||||
const querySchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { db, primaryDb } from "@server/db";
|
||||
import { and, count, eq } from "drizzle-orm";
|
||||
import {
|
||||
domains,
|
||||
@@ -233,6 +233,7 @@ export async function createOrg(
|
||||
let error = "";
|
||||
let org: Org | null = null;
|
||||
let numOrgs: number | null = null;
|
||||
let ownerUserId: string | null = null;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
const allDomains = await trx
|
||||
@@ -326,7 +327,6 @@ export async function createOrg(
|
||||
);
|
||||
}
|
||||
|
||||
let ownerUserId: string | null = null;
|
||||
if (req.user) {
|
||||
await trx.insert(userOrgs).values({
|
||||
userId: req.user!.userId,
|
||||
@@ -382,8 +382,6 @@ export async function createOrg(
|
||||
}))
|
||||
);
|
||||
|
||||
await calculateUserClientsForOrgs(ownerUserId, trx);
|
||||
|
||||
if (billingOrgIdForNewOrg) {
|
||||
const [numOrgsResult] = await trx
|
||||
.select({ count: count() })
|
||||
@@ -396,6 +394,14 @@ export async function createOrg(
|
||||
}
|
||||
});
|
||||
|
||||
if (ownerUserId) {
|
||||
calculateUserClientsForOrgs(ownerUserId).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to calculate user clients after creating org ${orgId} for user ${ownerUserId}: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (!org) {
|
||||
return next(
|
||||
createHttpError(
|
||||
|
||||
@@ -11,7 +11,7 @@ import { fromZodError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
|
||||
|
||||
const listOrgsSchema = z.object({
|
||||
const listOrgsSchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -14,7 +14,7 @@ const listOrgsParamsSchema = z.object({
|
||||
userId: z.string()
|
||||
});
|
||||
|
||||
const listOrgsSchema = z.object({
|
||||
const listOrgsSchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -9,6 +9,7 @@ import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { applyInlinePolicyFields } from "./inlinePolicyFields";
|
||||
|
||||
const getResourceSchema = z.strictObject({
|
||||
resourceId: z
|
||||
@@ -151,13 +152,7 @@ export async function getResource(
|
||||
const policy = await queryInlinePolicy(
|
||||
resource.defaultResourcePolicyId!
|
||||
);
|
||||
returnData = {
|
||||
...returnData,
|
||||
sso: policy?.sso || null,
|
||||
emailWhitelistEnabled: policy?.emailWhitelistEnabled || null,
|
||||
applyRules: policy?.applyRules || null,
|
||||
skipToIdpId: policy?.idpId || null
|
||||
};
|
||||
returnData = applyInlinePolicyFields(returnData, policy);
|
||||
}
|
||||
|
||||
return response<GetResourceResponse>(res, {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { assertEquals } from "../../../test/assert";
|
||||
import { applyInlinePolicyFields } from "./inlinePolicyFields";
|
||||
|
||||
function runTests() {
|
||||
const resource = {
|
||||
resourceId: 1,
|
||||
name: "dashboard",
|
||||
sso: null,
|
||||
emailWhitelistEnabled: null,
|
||||
applyRules: null,
|
||||
skipToIdpId: null
|
||||
} as any;
|
||||
|
||||
const enabledPolicy = {
|
||||
sso: true,
|
||||
emailWhitelistEnabled: true,
|
||||
applyRules: true,
|
||||
idpId: 42
|
||||
};
|
||||
|
||||
const enabledResult = applyInlinePolicyFields(resource, enabledPolicy);
|
||||
assertEquals(enabledResult.sso, true, "sso should mirror policy true");
|
||||
assertEquals(
|
||||
enabledResult.emailWhitelistEnabled,
|
||||
true,
|
||||
"email whitelist should mirror policy true"
|
||||
);
|
||||
assertEquals(
|
||||
enabledResult.applyRules,
|
||||
true,
|
||||
"applyRules should mirror policy true"
|
||||
);
|
||||
assertEquals(
|
||||
enabledResult.skipToIdpId,
|
||||
42,
|
||||
"skipToIdpId should use policy idpId"
|
||||
);
|
||||
|
||||
const disabledPolicy = {
|
||||
sso: false,
|
||||
emailWhitelistEnabled: false,
|
||||
applyRules: false,
|
||||
idpId: null
|
||||
};
|
||||
|
||||
const disabledResult = applyInlinePolicyFields(resource, disabledPolicy);
|
||||
assertEquals(disabledResult.sso, false, "sso false must not become null");
|
||||
assertEquals(
|
||||
disabledResult.emailWhitelistEnabled,
|
||||
false,
|
||||
"email whitelist false must not become null"
|
||||
);
|
||||
assertEquals(
|
||||
disabledResult.applyRules,
|
||||
false,
|
||||
"applyRules false must not become null"
|
||||
);
|
||||
assertEquals(
|
||||
disabledResult.skipToIdpId,
|
||||
null,
|
||||
"missing idp should stay null"
|
||||
);
|
||||
|
||||
const missingPolicyResult = applyInlinePolicyFields(resource, null);
|
||||
assertEquals(
|
||||
missingPolicyResult.sso,
|
||||
null,
|
||||
"missing policy should return nullable resource fields"
|
||||
);
|
||||
|
||||
console.log("PASS: inline policy fields mirror policy values");
|
||||
}
|
||||
|
||||
runTests();
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { Resource, ResourcePolicy } from "@server/db";
|
||||
|
||||
type InlinePolicyFields = Pick<
|
||||
ResourcePolicy,
|
||||
"sso" | "emailWhitelistEnabled" | "applyRules" | "idpId"
|
||||
>;
|
||||
|
||||
export function applyInlinePolicyFields<T extends Resource>(
|
||||
resource: T,
|
||||
policy: InlinePolicyFields | null | undefined
|
||||
): T {
|
||||
return {
|
||||
...resource,
|
||||
sso: policy?.sso ?? null,
|
||||
emailWhitelistEnabled: policy?.emailWhitelistEnabled ?? null,
|
||||
applyRules: policy?.applyRules ?? null,
|
||||
skipToIdpId: policy?.idpId ?? null
|
||||
};
|
||||
}
|
||||
@@ -14,7 +14,7 @@ const listResourceRulesParamsSchema = z.strictObject({
|
||||
resourceId: z.coerce.number().int().positive()
|
||||
});
|
||||
|
||||
const listResourceRulesSchema = z.object({
|
||||
const listResourceRulesSchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -48,7 +48,7 @@ const listResourcesParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const listResourcesSchema = z.object({
|
||||
const listResourcesSchema = z.strictObject({
|
||||
pageSize: z.coerce
|
||||
.number<string>() // for prettier formatting
|
||||
.int()
|
||||
|
||||
@@ -32,7 +32,7 @@ const listUserResourceAliasesParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const listUserResourceAliasesQuerySchema = z.object({
|
||||
const listUserResourceAliasesQuerySchema = z.strictObject({
|
||||
pageSize: z.coerce
|
||||
.number<string>()
|
||||
.int()
|
||||
|
||||
@@ -47,6 +47,7 @@ import { build } from "@server/build";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||
import { applyInlinePolicyFields } from "./inlinePolicyFields";
|
||||
|
||||
const updateResourceParamsSchema = z.strictObject({
|
||||
resourceId: z.coerce.number().int().positive()
|
||||
@@ -682,6 +683,12 @@ async function updateHttpResource(
|
||||
.where(eq(resourcePolicies.resourcePolicyId, policyId));
|
||||
}
|
||||
|
||||
const [inlinePolicy] = await db
|
||||
.select()
|
||||
.from(resourcePolicies)
|
||||
.where(eq(resourcePolicies.resourcePolicyId, policyId))
|
||||
.limit(1);
|
||||
|
||||
const updatedResource = await db
|
||||
.update(resources)
|
||||
.set({ ...resourceOnlyData, headers })
|
||||
@@ -698,7 +705,7 @@ async function updateHttpResource(
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: updatedResource[0],
|
||||
data: applyInlinePolicyFields(updatedResource[0], inlinePolicy),
|
||||
success: true,
|
||||
error: false,
|
||||
message: "HTTP resource updated successfully",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resourceRules, resources } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { resourcePolicyRules, resourceRules, resources } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -22,13 +22,20 @@ const updateResourceRuleParamsSchema = z.strictObject({
|
||||
resourceId: z.coerce.number().int().positive()
|
||||
});
|
||||
|
||||
const resourceRuleMatchSchema = z.enum([
|
||||
"CIDR",
|
||||
"IP",
|
||||
"PATH",
|
||||
"COUNTRY",
|
||||
"ASN",
|
||||
"REGION"
|
||||
]);
|
||||
|
||||
// Define Zod schema for request body validation
|
||||
const updateResourceRuleSchema = z
|
||||
.strictObject({
|
||||
action: z.enum(["ACCEPT", "DROP", "PASS"]).optional(),
|
||||
match: z
|
||||
.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN", "REGION"])
|
||||
.optional(),
|
||||
match: resourceRuleMatchSchema.optional(),
|
||||
value: z.string().min(1).optional(),
|
||||
priority: z.int(),
|
||||
enabled: z.boolean().optional()
|
||||
@@ -123,37 +130,102 @@ export async function updateResourceRule(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Cannot create rule for non-http resource"
|
||||
"Cannot update rule for non-http resource"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that the rule exists and belongs to the specified resource
|
||||
const [existingRule] = await db
|
||||
.select()
|
||||
.from(resourceRules)
|
||||
.where(eq(resourceRules.ruleId, ruleId))
|
||||
.limit(1);
|
||||
const isInlinePolicy =
|
||||
resource.resourcePolicyId === null &&
|
||||
resource.defaultResourcePolicyId !== null;
|
||||
|
||||
if (!existingRule) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource rule with ID ${ruleId} not found`
|
||||
)
|
||||
let existingMatch:
|
||||
| "CIDR"
|
||||
| "IP"
|
||||
| "PATH"
|
||||
| "COUNTRY"
|
||||
| "ASN"
|
||||
| "REGION";
|
||||
|
||||
if (isInlinePolicy) {
|
||||
const policyId = resource.defaultResourcePolicyId!;
|
||||
const [existingRule] = await db
|
||||
.select()
|
||||
.from(resourcePolicyRules)
|
||||
.where(eq(resourcePolicyRules.ruleId, ruleId))
|
||||
.limit(1);
|
||||
|
||||
if (!existingRule) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource rule with ID ${ruleId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (existingRule.resourcePolicyId !== policyId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
`Resource rule ${ruleId} does not belong to resource ${resourceId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedExistingMatch = resourceRuleMatchSchema.safeParse(
|
||||
existingRule.match
|
||||
);
|
||||
if (!parsedExistingMatch.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Resource rule has invalid match type"
|
||||
)
|
||||
);
|
||||
}
|
||||
existingMatch = parsedExistingMatch.data;
|
||||
} else {
|
||||
// Verify that the rule exists and belongs to the specified resource
|
||||
const [existingRule] = await db
|
||||
.select()
|
||||
.from(resourceRules)
|
||||
.where(eq(resourceRules.ruleId, ruleId))
|
||||
.limit(1);
|
||||
|
||||
if (!existingRule) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource rule with ID ${ruleId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (existingRule.resourceId !== resourceId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
`Resource rule ${ruleId} does not belong to resource ${resourceId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedExistingMatch = resourceRuleMatchSchema.safeParse(
|
||||
existingRule.match
|
||||
);
|
||||
if (!parsedExistingMatch.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Resource rule has invalid match type"
|
||||
)
|
||||
);
|
||||
}
|
||||
existingMatch = parsedExistingMatch.data;
|
||||
}
|
||||
|
||||
if (existingRule.resourceId !== resourceId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
`Resource rule ${ruleId} does not belong to resource ${resourceId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const match = updateData.match || existingRule.match;
|
||||
const match = updateData.match || existingMatch;
|
||||
const { value } = updateData;
|
||||
|
||||
if (value !== undefined) {
|
||||
@@ -197,11 +269,30 @@ export async function updateResourceRule(
|
||||
}
|
||||
|
||||
// Update the rule
|
||||
const [updatedRule] = await db
|
||||
.update(resourceRules)
|
||||
.set(updateData)
|
||||
.where(eq(resourceRules.ruleId, ruleId))
|
||||
.returning();
|
||||
const [updatedRule] = isInlinePolicy
|
||||
? await db
|
||||
.update(resourcePolicyRules)
|
||||
.set(updateData)
|
||||
.where(
|
||||
and(
|
||||
eq(resourcePolicyRules.ruleId, ruleId),
|
||||
eq(
|
||||
resourcePolicyRules.resourcePolicyId,
|
||||
resource.defaultResourcePolicyId!
|
||||
)
|
||||
)
|
||||
)
|
||||
.returning()
|
||||
: await db
|
||||
.update(resourceRules)
|
||||
.set(updateData)
|
||||
.where(
|
||||
and(
|
||||
eq(resourceRules.ruleId, ruleId),
|
||||
eq(resourceRules.resourceId, resourceId)
|
||||
)
|
||||
)
|
||||
.returning();
|
||||
|
||||
return response(res, {
|
||||
data: updatedRule,
|
||||
|
||||
@@ -15,7 +15,7 @@ const listRolesParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const listRolesSchema = z.object({
|
||||
const listRolesSchema = z.strictObject({
|
||||
pageSize: z.coerce
|
||||
.number<string>() // for prettier formatting
|
||||
.int()
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import {
|
||||
db,
|
||||
exitNodes,
|
||||
labels,
|
||||
newts,
|
||||
orgs,
|
||||
remoteExitNodes,
|
||||
roleSites,
|
||||
siteLabels,
|
||||
siteNetworks,
|
||||
siteResources,
|
||||
targets,
|
||||
sites,
|
||||
targets,
|
||||
userSites,
|
||||
labels,
|
||||
siteLabels,
|
||||
type Label
|
||||
} from "@server/db";
|
||||
import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
@@ -23,108 +24,15 @@ import type { PaginatedResponse } from "@server/types/Pagination";
|
||||
import { and, asc, desc, eq, inArray, like, or, sql } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import semver from "semver";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
// Stale-while-revalidate: keeps the last successfully fetched version so that
|
||||
// a transient network failure / timeout does not flip every site back to
|
||||
// newtUpdateAvailable: false.
|
||||
let staleNewtVersion: string | null = null;
|
||||
|
||||
async function getLatestNewtVersion(): Promise<string | null> {
|
||||
try {
|
||||
const cachedVersion = await cache.get<string>(
|
||||
"cache:latestNewtVersion"
|
||||
);
|
||||
if (cachedVersion) {
|
||||
return cachedVersion;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 1500);
|
||||
|
||||
const response = await fetch(
|
||||
"https://api.github.com/repos/fosrl/newt/tags",
|
||||
{
|
||||
signal: controller.signal
|
||||
}
|
||||
);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
logger.warn(
|
||||
`Failed to fetch latest Newt version from GitHub: ${response.status} ${response.statusText}`
|
||||
);
|
||||
return staleNewtVersion;
|
||||
}
|
||||
|
||||
let tags = await response.json();
|
||||
if (!Array.isArray(tags) || tags.length === 0) {
|
||||
logger.warn("No tags found for Newt repository");
|
||||
return staleNewtVersion;
|
||||
}
|
||||
|
||||
// Remove release-candidates, then sort descending by semver so that
|
||||
// duplicate tags (e.g. "1.10.3" and "v1.10.3") and any ordering quirks
|
||||
// from the GitHub API do not cause an older tag to be selected.
|
||||
tags = tags.filter((tag: any) => !tag.name.includes("rc"));
|
||||
tags.sort((a: any, b: any) => {
|
||||
const va = semver.coerce(a.name);
|
||||
const vb = semver.coerce(b.name);
|
||||
if (!va && !vb) return 0;
|
||||
if (!va) return 1;
|
||||
if (!vb) return -1;
|
||||
return semver.rcompare(va, vb);
|
||||
});
|
||||
|
||||
// Deduplicate: keep only the first (highest) entry per normalised version
|
||||
const seen = new Set<string>();
|
||||
tags = tags.filter((tag: any) => {
|
||||
const normalised = semver.coerce(tag.name)?.version;
|
||||
if (!normalised || seen.has(normalised)) return false;
|
||||
seen.add(normalised);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (tags.length === 0) {
|
||||
logger.warn("No valid semver tags found for Newt repository");
|
||||
return staleNewtVersion;
|
||||
}
|
||||
|
||||
const latestVersion = tags[0].name;
|
||||
|
||||
staleNewtVersion = latestVersion;
|
||||
await cache.set("cache:latestNewtVersion", latestVersion, 3600);
|
||||
|
||||
return latestVersion;
|
||||
} catch (error: any) {
|
||||
if (error.name === "AbortError") {
|
||||
logger.warn(
|
||||
"Request to fetch latest Newt version timed out (1.5s)"
|
||||
);
|
||||
} else if (error.cause?.code === "UND_ERR_CONNECT_TIMEOUT") {
|
||||
logger.warn(
|
||||
"Connection timeout while fetching latest Newt version"
|
||||
);
|
||||
} else {
|
||||
logger.warn(
|
||||
"Error fetching latest Newt version:",
|
||||
error.message || error
|
||||
);
|
||||
}
|
||||
return staleNewtVersion;
|
||||
}
|
||||
}
|
||||
|
||||
const listSitesParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const listSitesSchema = z.object({
|
||||
const listSitesSchema = z.strictObject({
|
||||
pageSize: z.coerce
|
||||
.number<string>() // for prettier formatting
|
||||
.int()
|
||||
@@ -449,9 +357,6 @@ export async function listSites(
|
||||
|
||||
const totalCount = Number(countRows[0]?.count ?? 0);
|
||||
|
||||
// Get latest version asynchronously without blocking the response
|
||||
const latestNewtVersionPromise = getLatestNewtVersion();
|
||||
|
||||
const siteIds = rows.map((site) => site.siteId);
|
||||
|
||||
let labelsForSites: Array<{
|
||||
@@ -494,36 +399,6 @@ export async function listSites(
|
||||
return { ...siteWithUpdate, labels: labelsForSite };
|
||||
});
|
||||
|
||||
// Try to get the latest version, but don't block if it fails
|
||||
try {
|
||||
const latestNewtVersion = await latestNewtVersionPromise;
|
||||
|
||||
if (latestNewtVersion) {
|
||||
sitesWithUpdates.forEach((site) => {
|
||||
if (
|
||||
site.type === "newt" &&
|
||||
site.newtVersion &&
|
||||
latestNewtVersion
|
||||
) {
|
||||
try {
|
||||
site.newtUpdateAvailable = semver.lt(
|
||||
site.newtVersion,
|
||||
latestNewtVersion
|
||||
);
|
||||
} catch (error) {
|
||||
site.newtUpdateAvailable = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Log the error but don't let it block the response
|
||||
logger.warn(
|
||||
"Failed to check for Newt updates, continuing without update info:",
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
const sitesPayload = sitesWithUpdates.map((site) =>
|
||||
site.type === "local" ? { ...site, online: undefined } : site
|
||||
);
|
||||
|
||||
@@ -148,13 +148,15 @@ export async function addClientToSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx.insert(clientSiteResources).values({
|
||||
clientId,
|
||||
siteResourceId
|
||||
});
|
||||
await db.insert(clientSiteResources).values({
|
||||
clientId,
|
||||
siteResourceId
|
||||
});
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||
rebuildClientAssociationsFromSiteResource(siteResource).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for site resource ${siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -155,13 +155,15 @@ export async function addRoleToSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx.insert(roleSiteResources).values({
|
||||
roleId,
|
||||
siteResourceId
|
||||
});
|
||||
await db.insert(roleSiteResources).values({
|
||||
roleId,
|
||||
siteResourceId
|
||||
});
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||
rebuildClientAssociationsFromSiteResource(siteResource).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for site resource ${siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -124,13 +124,15 @@ export async function addUserToSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx.insert(userSiteResources).values({
|
||||
userId,
|
||||
siteResourceId
|
||||
});
|
||||
await db.insert(userSiteResources).values({
|
||||
userId,
|
||||
siteResourceId
|
||||
});
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||
rebuildClientAssociationsFromSiteResource(siteResource).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for site resource ${siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -235,7 +235,7 @@ export async function batchAddClientToSiteResources(
|
||||
}
|
||||
});
|
||||
|
||||
rebuildClientAssociationsFromClient(client, primaryDb).catch((e) => {
|
||||
rebuildClientAssociationsFromClient(client).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations after batch adding site resources for client ${clientId}: ${e}`
|
||||
);
|
||||
|
||||
@@ -625,15 +625,14 @@ export async function createSiteResource(
|
||||
// own transaction so it always executes on the primary — avoiding any
|
||||
// replica-lag issues while still allowing the HTTP response to return
|
||||
// early.
|
||||
rebuildClientAssociationsFromSiteResource(
|
||||
newSiteResource!,
|
||||
primaryDb
|
||||
).catch((err) => {
|
||||
logger.error(
|
||||
`Error rebuilding client associations for site resource ${newSiteResource!.siteResourceId}:`,
|
||||
err
|
||||
);
|
||||
});
|
||||
rebuildClientAssociationsFromSiteResource(newSiteResource!).catch(
|
||||
(err) => {
|
||||
logger.error(
|
||||
`Error rebuilding client associations for site resource ${newSiteResource!.siteResourceId}:`,
|
||||
err
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
data: newSiteResource,
|
||||
|
||||
@@ -88,15 +88,14 @@ export async function deleteSiteResource(
|
||||
// own transaction so it always executes on the primary — avoiding any
|
||||
// replica-lag issues while still allowing the HTTP response to return
|
||||
// early.
|
||||
rebuildClientAssociationsFromSiteResource(
|
||||
removedSiteResource,
|
||||
primaryDb
|
||||
).catch((err) => {
|
||||
logger.error(
|
||||
`Error rebuilding client associations for site resource ${removedSiteResource!.siteResourceId}:`,
|
||||
err
|
||||
);
|
||||
});
|
||||
rebuildClientAssociationsFromSiteResource(removedSiteResource).catch(
|
||||
(err) => {
|
||||
logger.error(
|
||||
`Error rebuilding client associations for site resource ${removedSiteResource!.siteResourceId}:`,
|
||||
err
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
logger.info(`Deleted site resource ${siteResourceId}`);
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ const listAllSiteResourcesByOrgParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const listAllSiteResourcesByOrgQuerySchema = z.object({
|
||||
const listAllSiteResourcesByOrgQuerySchema = z.strictObject({
|
||||
pageSize: z.coerce
|
||||
.number<string>() // for prettier formatting
|
||||
.int()
|
||||
|
||||
@@ -15,7 +15,7 @@ const listSiteResourcesParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const listSiteResourcesQuerySchema = z.object({
|
||||
const listSiteResourcesQuerySchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -148,17 +148,19 @@ export async function removeClientFromSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.delete(clientSiteResources)
|
||||
.where(
|
||||
and(
|
||||
eq(clientSiteResources.siteResourceId, siteResourceId),
|
||||
eq(clientSiteResources.clientId, clientId)
|
||||
)
|
||||
);
|
||||
await db
|
||||
.delete(clientSiteResources)
|
||||
.where(
|
||||
and(
|
||||
eq(clientSiteResources.siteResourceId, siteResourceId),
|
||||
eq(clientSiteResources.clientId, clientId)
|
||||
)
|
||||
);
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||
rebuildClientAssociationsFromSiteResource(siteResource).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for site resource ${siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -156,17 +156,19 @@ export async function removeRoleFromSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.delete(roleSiteResources)
|
||||
.where(
|
||||
and(
|
||||
eq(roleSiteResources.siteResourceId, siteResourceId),
|
||||
eq(roleSiteResources.roleId, roleId)
|
||||
)
|
||||
);
|
||||
await db
|
||||
.delete(roleSiteResources)
|
||||
.where(
|
||||
and(
|
||||
eq(roleSiteResources.siteResourceId, siteResourceId),
|
||||
eq(roleSiteResources.roleId, roleId)
|
||||
)
|
||||
);
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||
rebuildClientAssociationsFromSiteResource(siteResource).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for site resource ${siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -126,17 +126,19 @@ export async function removeUserFromSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.delete(userSiteResources)
|
||||
.where(
|
||||
and(
|
||||
eq(userSiteResources.siteResourceId, siteResourceId),
|
||||
eq(userSiteResources.userId, userId)
|
||||
)
|
||||
);
|
||||
await db
|
||||
.delete(userSiteResources)
|
||||
.where(
|
||||
and(
|
||||
eq(userSiteResources.siteResourceId, siteResourceId),
|
||||
eq(userSiteResources.userId, userId)
|
||||
)
|
||||
);
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||
rebuildClientAssociationsFromSiteResource(siteResource).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for site resource ${siteResourceId} after removing user ${userId}: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -141,8 +141,12 @@ export async function setSiteResourceClients(
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||
rebuildClientAssociationsFromSiteResource(siteResource).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for site resource ${siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -165,8 +165,12 @@ export async function setSiteResourceRoles(
|
||||
roleIds.map((roleId) => ({ roleId, siteResourceId }))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||
rebuildClientAssociationsFromSiteResource(siteResource).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for site resource ${siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { fromError } from "zod-validation-error";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||
import { error } from "node:console";
|
||||
|
||||
const setSiteResourceUsersBodySchema = z
|
||||
.object({
|
||||
@@ -120,8 +121,12 @@ export async function setSiteResourceUsers(
|
||||
userIds.map((userId) => ({ userId, siteResourceId }))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||
rebuildClientAssociationsFromSiteResource(siteResource).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for site resource ${siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
sites,
|
||||
networks,
|
||||
Transaction,
|
||||
userSiteResources
|
||||
userSiteResources,
|
||||
primaryDb
|
||||
} from "@server/db";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
@@ -389,7 +390,6 @@ export async function updateSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
let sitesChanged = false;
|
||||
const existingSiteIds = existingSiteResource.networkId
|
||||
? await db
|
||||
.select()
|
||||
@@ -398,16 +398,7 @@ export async function updateSiteResource(
|
||||
eq(siteNetworks.networkId, existingSiteResource.networkId)
|
||||
)
|
||||
: [];
|
||||
|
||||
const existingSiteIdSet = new Set(existingSiteIds.map((s) => s.siteId));
|
||||
const newSiteIdSet = new Set(siteIds);
|
||||
|
||||
if (
|
||||
existingSiteIdSet.size !== newSiteIdSet.size ||
|
||||
![...existingSiteIdSet].every((id) => newSiteIdSet.has(id))
|
||||
) {
|
||||
sitesChanged = true;
|
||||
}
|
||||
|
||||
let fullDomain: string | null = null;
|
||||
let finalSubdomain: string | null = null;
|
||||
@@ -474,345 +465,165 @@ export async function updateSiteResource(
|
||||
|
||||
let updatedSiteResource: SiteResource | undefined;
|
||||
await db.transaction(async (trx) => {
|
||||
// if the site is changed we need to delete and recreate the resource to avoid complications with the rebuild function otherwise we can just update in place
|
||||
if (sitesChanged) {
|
||||
// delete the existing site resource
|
||||
await trx
|
||||
.delete(siteResources)
|
||||
.where(
|
||||
and(eq(siteResources.siteResourceId, siteResourceId))
|
||||
);
|
||||
// Update the site resource
|
||||
const sshPamSet =
|
||||
isLicensedSshPam &&
|
||||
(authDaemonPort !== undefined ||
|
||||
authDaemonMode !== undefined ||
|
||||
pamMode !== undefined)
|
||||
? {
|
||||
...(authDaemonPort !== undefined && {
|
||||
authDaemonPort
|
||||
}),
|
||||
...(authDaemonMode !== undefined && {
|
||||
authDaemonMode
|
||||
}),
|
||||
...(pamMode !== undefined && {
|
||||
pamMode
|
||||
})
|
||||
}
|
||||
: {};
|
||||
let tcpPortRangeStringAdjusted = tcpPortRangeString;
|
||||
if (mode === "http") {
|
||||
tcpPortRangeStringAdjusted = "443,80";
|
||||
} else if (mode === "ssh") {
|
||||
tcpPortRangeStringAdjusted = destinationPort
|
||||
? destinationPort.toString()
|
||||
: "22";
|
||||
}
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(
|
||||
existingSiteResource,
|
||||
trx
|
||||
[updatedSiteResource] = await trx
|
||||
.update(siteResources)
|
||||
.set({
|
||||
name: name,
|
||||
niceId: niceId,
|
||||
mode: mode,
|
||||
scheme,
|
||||
ssl,
|
||||
destination: destination,
|
||||
destinationPort: destinationPort,
|
||||
enabled: enabled,
|
||||
alias: alias ? alias.trim() : null,
|
||||
tcpPortRangeString: tcpPortRangeStringAdjusted,
|
||||
udpPortRangeString:
|
||||
mode == "http" || mode == "ssh"
|
||||
? ""
|
||||
: udpPortRangeString,
|
||||
disableIcmp:
|
||||
disableIcmp ||
|
||||
(mode == "http" || mode == "ssh" ? true : false),
|
||||
domainId,
|
||||
subdomain: finalSubdomain,
|
||||
fullDomain,
|
||||
...sshPamSet
|
||||
})
|
||||
.where(and(eq(siteResources.siteResourceId, siteResourceId)))
|
||||
.returning();
|
||||
|
||||
//////////////////// update the associations ////////////////////
|
||||
|
||||
// delete the site - site resources associations
|
||||
await trx
|
||||
.delete(siteNetworks)
|
||||
.where(
|
||||
eq(siteNetworks.networkId, updatedSiteResource.networkId!)
|
||||
);
|
||||
|
||||
// create the new site resource from the removed one - the ID should stay the same
|
||||
const [insertedSiteResource] = await trx
|
||||
.insert(siteResources)
|
||||
.values({
|
||||
...existingSiteResource
|
||||
})
|
||||
.returning();
|
||||
|
||||
const sshPamSet =
|
||||
isLicensedSshPam &&
|
||||
(authDaemonPort !== undefined ||
|
||||
authDaemonMode !== undefined ||
|
||||
pamMode !== undefined)
|
||||
? {
|
||||
...(authDaemonPort !== undefined && {
|
||||
authDaemonPort
|
||||
}),
|
||||
...(authDaemonMode !== undefined && {
|
||||
authDaemonMode
|
||||
}),
|
||||
...(pamMode !== undefined && {
|
||||
pamMode
|
||||
})
|
||||
}
|
||||
: {};
|
||||
|
||||
let tcpPortRangeStringAdjusted = tcpPortRangeString;
|
||||
if (mode === "http") {
|
||||
tcpPortRangeStringAdjusted = "443,80";
|
||||
} else if (mode === "ssh") {
|
||||
tcpPortRangeStringAdjusted = destinationPort
|
||||
? destinationPort.toString()
|
||||
: "22";
|
||||
}
|
||||
|
||||
[updatedSiteResource] = await trx
|
||||
.update(siteResources)
|
||||
.set({
|
||||
name,
|
||||
niceId,
|
||||
mode,
|
||||
scheme,
|
||||
ssl,
|
||||
destination,
|
||||
destinationPort,
|
||||
enabled,
|
||||
alias: alias ? alias.trim() : null,
|
||||
tcpPortRangeString: tcpPortRangeStringAdjusted,
|
||||
udpPortRangeString:
|
||||
mode == "http" || mode == "ssh"
|
||||
? ""
|
||||
: udpPortRangeString,
|
||||
disableIcmp:
|
||||
disableIcmp ||
|
||||
(mode == "http" || mode == "ssh" ? true : false), // default to true for http resources, otherwise false
|
||||
domainId,
|
||||
subdomain: finalSubdomain,
|
||||
fullDomain,
|
||||
...sshPamSet
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
siteResources.siteResourceId,
|
||||
insertedSiteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
)
|
||||
.returning();
|
||||
|
||||
if (!updatedSiteResource) {
|
||||
throw new Error(
|
||||
"Failed to create updated site resource after site change"
|
||||
);
|
||||
}
|
||||
|
||||
//////////////////// update the associations ////////////////////
|
||||
|
||||
// delete the site - site resources associations
|
||||
await trx
|
||||
.delete(siteNetworks)
|
||||
.where(
|
||||
eq(
|
||||
siteNetworks.networkId,
|
||||
updatedSiteResource.networkId!
|
||||
)
|
||||
);
|
||||
|
||||
for (const siteId of siteIds) {
|
||||
await trx.insert(siteNetworks).values({
|
||||
siteId: siteId,
|
||||
networkId: updatedSiteResource.networkId!
|
||||
});
|
||||
}
|
||||
|
||||
const [adminRole] = await trx
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(
|
||||
and(
|
||||
eq(roles.isAdmin, true),
|
||||
eq(roles.orgId, updatedSiteResource.orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!adminRole) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Admin role not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await trx.insert(roleSiteResources).values({
|
||||
roleId: adminRole.roleId,
|
||||
siteResourceId: updatedSiteResource.siteResourceId
|
||||
for (const siteId of siteIds) {
|
||||
await trx.insert(siteNetworks).values({
|
||||
siteId: siteId,
|
||||
networkId: updatedSiteResource.networkId!
|
||||
});
|
||||
|
||||
if (roleIds.length > 0) {
|
||||
await trx.insert(roleSiteResources).values(
|
||||
roleIds.map((roleId) => ({
|
||||
roleId,
|
||||
siteResourceId: updatedSiteResource!.siteResourceId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
if (userIds.length > 0) {
|
||||
await trx.insert(userSiteResources).values(
|
||||
userIds.map((userId) => ({
|
||||
userId,
|
||||
siteResourceId: updatedSiteResource!.siteResourceId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
if (clientIds.length > 0) {
|
||||
await trx.insert(clientSiteResources).values(
|
||||
clientIds.map((clientId) => ({
|
||||
clientId,
|
||||
siteResourceId: updatedSiteResource!.siteResourceId
|
||||
}))
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Update the site resource
|
||||
const sshPamSet =
|
||||
isLicensedSshPam &&
|
||||
(authDaemonPort !== undefined ||
|
||||
authDaemonMode !== undefined ||
|
||||
pamMode !== undefined)
|
||||
? {
|
||||
...(authDaemonPort !== undefined && {
|
||||
authDaemonPort
|
||||
}),
|
||||
...(authDaemonMode !== undefined && {
|
||||
authDaemonMode
|
||||
}),
|
||||
...(pamMode !== undefined && {
|
||||
pamMode
|
||||
})
|
||||
}
|
||||
: {};
|
||||
let tcpPortRangeStringAdjusted = tcpPortRangeString;
|
||||
if (mode === "http") {
|
||||
tcpPortRangeStringAdjusted = "443,80";
|
||||
} else if (mode === "ssh") {
|
||||
tcpPortRangeStringAdjusted = destinationPort
|
||||
? destinationPort.toString()
|
||||
: "22";
|
||||
}
|
||||
|
||||
[updatedSiteResource] = await trx
|
||||
.update(siteResources)
|
||||
.set({
|
||||
name: name,
|
||||
niceId: niceId,
|
||||
mode: mode,
|
||||
scheme,
|
||||
ssl,
|
||||
destination: destination,
|
||||
destinationPort: destinationPort,
|
||||
enabled: enabled,
|
||||
alias: alias ? alias.trim() : null,
|
||||
tcpPortRangeString: tcpPortRangeStringAdjusted,
|
||||
udpPortRangeString:
|
||||
mode == "http" || mode == "ssh"
|
||||
? ""
|
||||
: udpPortRangeString,
|
||||
disableIcmp:
|
||||
disableIcmp ||
|
||||
(mode == "http" || mode == "ssh" ? true : false),
|
||||
domainId,
|
||||
subdomain: finalSubdomain,
|
||||
fullDomain,
|
||||
...sshPamSet
|
||||
})
|
||||
.where(
|
||||
and(eq(siteResources.siteResourceId, siteResourceId))
|
||||
)
|
||||
.returning();
|
||||
|
||||
//////////////////// update the associations ////////////////////
|
||||
|
||||
// delete the site - site resources associations
|
||||
await trx
|
||||
.delete(siteNetworks)
|
||||
.where(
|
||||
eq(
|
||||
siteNetworks.networkId,
|
||||
updatedSiteResource.networkId!
|
||||
)
|
||||
);
|
||||
|
||||
for (const siteId of siteIds) {
|
||||
await trx.insert(siteNetworks).values({
|
||||
siteId: siteId,
|
||||
networkId: updatedSiteResource.networkId!
|
||||
});
|
||||
}
|
||||
|
||||
await trx
|
||||
.delete(clientSiteResources)
|
||||
.where(
|
||||
eq(clientSiteResources.siteResourceId, siteResourceId)
|
||||
);
|
||||
|
||||
if (clientIds.length > 0) {
|
||||
await trx.insert(clientSiteResources).values(
|
||||
clientIds.map((clientId) => ({
|
||||
clientId,
|
||||
siteResourceId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
await trx
|
||||
.delete(userSiteResources)
|
||||
.where(
|
||||
eq(userSiteResources.siteResourceId, siteResourceId)
|
||||
);
|
||||
|
||||
if (userIds.length > 0) {
|
||||
await trx.insert(userSiteResources).values(
|
||||
userIds.map((userId) => ({
|
||||
userId,
|
||||
siteResourceId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// Get all admin role IDs for this org to exclude from deletion
|
||||
const adminRoles = await trx
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(
|
||||
and(
|
||||
eq(roles.isAdmin, true),
|
||||
eq(roles.orgId, updatedSiteResource.orgId)
|
||||
)
|
||||
);
|
||||
const adminRoleIds = adminRoles.map((role) => role.roleId);
|
||||
|
||||
if (adminRoleIds.length > 0) {
|
||||
await trx.delete(roleSiteResources).where(
|
||||
and(
|
||||
eq(
|
||||
roleSiteResources.siteResourceId,
|
||||
siteResourceId
|
||||
),
|
||||
ne(roleSiteResources.roleId, adminRoleIds[0]) // delete all but the admin role
|
||||
)
|
||||
);
|
||||
} else {
|
||||
await trx
|
||||
.delete(roleSiteResources)
|
||||
.where(
|
||||
eq(roleSiteResources.siteResourceId, siteResourceId)
|
||||
);
|
||||
}
|
||||
|
||||
if (roleIds.length > 0) {
|
||||
await trx.insert(roleSiteResources).values(
|
||||
roleIds.map((roleId) => ({
|
||||
roleId,
|
||||
siteResourceId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(`Updated site resource ${siteResourceId}`);
|
||||
}
|
||||
|
||||
await trx
|
||||
.delete(clientSiteResources)
|
||||
.where(eq(clientSiteResources.siteResourceId, siteResourceId));
|
||||
|
||||
if (clientIds.length > 0) {
|
||||
await trx.insert(clientSiteResources).values(
|
||||
clientIds.map((clientId) => ({
|
||||
clientId,
|
||||
siteResourceId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
await trx
|
||||
.delete(userSiteResources)
|
||||
.where(eq(userSiteResources.siteResourceId, siteResourceId));
|
||||
|
||||
if (userIds.length > 0) {
|
||||
await trx.insert(userSiteResources).values(
|
||||
userIds.map((userId) => ({
|
||||
userId,
|
||||
siteResourceId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// Get all admin role IDs for this org to exclude from deletion
|
||||
const adminRoles = await trx
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(
|
||||
and(
|
||||
eq(roles.isAdmin, true),
|
||||
eq(roles.orgId, updatedSiteResource.orgId)
|
||||
)
|
||||
);
|
||||
const adminRoleIds = adminRoles.map((role) => role.roleId);
|
||||
|
||||
if (adminRoleIds.length > 0) {
|
||||
await trx.delete(roleSiteResources).where(
|
||||
and(
|
||||
eq(roleSiteResources.siteResourceId, siteResourceId),
|
||||
ne(roleSiteResources.roleId, adminRoleIds[0]) // delete all but the admin role
|
||||
)
|
||||
);
|
||||
} else {
|
||||
await trx
|
||||
.delete(roleSiteResources)
|
||||
.where(
|
||||
eq(roleSiteResources.siteResourceId, siteResourceId)
|
||||
);
|
||||
}
|
||||
|
||||
if (roleIds.length > 0) {
|
||||
await trx.insert(roleSiteResources).values(
|
||||
roleIds.map((roleId) => ({
|
||||
roleId,
|
||||
siteResourceId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(`Updated site resource ${siteResourceId}`);
|
||||
});
|
||||
|
||||
// Background: wait for removal messages to propagate, then rebuild
|
||||
// associations for the re-created resource. Own transaction ensures
|
||||
// execution on the primary against fully committed state.
|
||||
(async () => {
|
||||
await db.transaction(async (trx) => {
|
||||
if (!updatedSiteResource) {
|
||||
throw new Error("No updated resource found after update");
|
||||
}
|
||||
if (sitesChanged) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 750));
|
||||
await rebuildClientAssociationsFromSiteResource(
|
||||
updatedSiteResource,
|
||||
trx
|
||||
);
|
||||
}
|
||||
await handleMessagingForUpdatedSiteResource(
|
||||
existingSiteResource,
|
||||
updatedSiteResource,
|
||||
siteIds.map((siteId) => ({
|
||||
siteId,
|
||||
orgId: existingSiteResource.orgId
|
||||
})),
|
||||
trx
|
||||
if (!updatedSiteResource) {
|
||||
throw new Error("No updated resource found after update");
|
||||
}
|
||||
|
||||
rebuildClientAssociationsFromSiteResource(updatedSiteResource).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for site resource ${siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
})().catch((err) => {
|
||||
}
|
||||
);
|
||||
|
||||
handleMessagingForUpdatedSiteResource(
|
||||
existingSiteResource,
|
||||
updatedSiteResource,
|
||||
Array.from(existingSiteIdSet).map((siteId: number) => ({
|
||||
// we already added to the new sites above in the rebuild function so we only need to update the ones that did not change
|
||||
siteId,
|
||||
orgId: existingSiteResource.orgId
|
||||
}))
|
||||
).catch((e) => {
|
||||
logger.error(
|
||||
`Error rebuilding client associations for site resource ${updatedSiteResource?.siteResourceId}:`,
|
||||
err
|
||||
`Failed to handle messaging for updated site resource ${siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -837,9 +648,9 @@ export async function updateSiteResource(
|
||||
export async function handleMessagingForUpdatedSiteResource(
|
||||
existingSiteResource: SiteResource | undefined,
|
||||
updatedSiteResource: SiteResource,
|
||||
sites: { siteId: number; orgId: string }[],
|
||||
trx: Transaction
|
||||
sites: { siteId: number; orgId: string }[]
|
||||
) {
|
||||
const trx = primaryDb;
|
||||
logger.debug(
|
||||
"handleMessagingForUpdatedSiteResource: existingSiteResource is: ",
|
||||
existingSiteResource
|
||||
@@ -849,17 +660,14 @@ export async function handleMessagingForUpdatedSiteResource(
|
||||
updatedSiteResource
|
||||
);
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(
|
||||
existingSiteResource || updatedSiteResource, // we want to rebuild based on the existing resource then we will apply the change to the destination below
|
||||
trx
|
||||
);
|
||||
|
||||
const { sitesList, mergedAllClients, mergedAllClientIds } =
|
||||
await getClientSiteResourceAccess(
|
||||
existingSiteResource || updatedSiteResource,
|
||||
trx
|
||||
);
|
||||
|
||||
const siteIds = sites.map((site) => site.siteId);
|
||||
|
||||
// after everything is rebuilt above we still need to update the targets and remote subnets if the destination changed
|
||||
const destinationChanged =
|
||||
existingSiteResource &&
|
||||
@@ -896,12 +704,86 @@ export async function handleMessagingForUpdatedSiteResource(
|
||||
portRangesChanged ||
|
||||
destinationPortChanged
|
||||
) {
|
||||
const newtsForSites =
|
||||
siteIds.length > 0
|
||||
? await trx
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(inArray(newts.siteId, siteIds))
|
||||
: [];
|
||||
const newtBySiteId = new Map(
|
||||
newtsForSites.map((newt) => [newt.siteId, newt])
|
||||
);
|
||||
|
||||
const oldDestinationStillInUseClientSitePairs = new Set<string>();
|
||||
if (
|
||||
existingSiteResource?.destination &&
|
||||
siteIds.length > 0 &&
|
||||
mergedAllClientIds.length > 0
|
||||
) {
|
||||
const oldDestinationStillInUseRows = await trx
|
||||
.select({
|
||||
clientId: clientSiteResourcesAssociationsCache.clientId,
|
||||
siteId: siteNetworks.siteId
|
||||
})
|
||||
.from(siteResources)
|
||||
.innerJoin(
|
||||
clientSiteResourcesAssociationsCache,
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||
siteResources.siteResourceId
|
||||
)
|
||||
)
|
||||
.innerJoin(
|
||||
siteNetworks,
|
||||
eq(siteNetworks.networkId, siteResources.networkId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
inArray(
|
||||
clientSiteResourcesAssociationsCache.clientId,
|
||||
mergedAllClientIds
|
||||
),
|
||||
inArray(siteNetworks.siteId, siteIds),
|
||||
eq(
|
||||
siteResources.destination,
|
||||
existingSiteResource.destination
|
||||
),
|
||||
ne(
|
||||
siteResources.siteResourceId,
|
||||
existingSiteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
for (const row of oldDestinationStillInUseRows) {
|
||||
oldDestinationStillInUseClientSitePairs.add(
|
||||
`${row.clientId}:${row.siteId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const shouldUpdateTargets =
|
||||
destinationChanged ||
|
||||
sslChanged ||
|
||||
portRangesChanged ||
|
||||
fullDomainChanged ||
|
||||
destinationPortChanged;
|
||||
const oldTargets = shouldUpdateTargets
|
||||
? await generateSubnetProxyTargetV2(
|
||||
existingSiteResource,
|
||||
mergedAllClients
|
||||
)
|
||||
: [];
|
||||
const newTargets = shouldUpdateTargets
|
||||
? await generateSubnetProxyTargetV2(
|
||||
updatedSiteResource,
|
||||
mergedAllClients
|
||||
)
|
||||
: [];
|
||||
|
||||
for (const site of sites) {
|
||||
const [newt] = await trx
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(eq(newts.siteId, site.siteId))
|
||||
.limit(1);
|
||||
const newt = newtBySiteId.get(site.siteId);
|
||||
|
||||
if (!newt) {
|
||||
throw new Error(
|
||||
@@ -910,22 +792,7 @@ export async function handleMessagingForUpdatedSiteResource(
|
||||
}
|
||||
|
||||
// Only update targets on newt if these items change
|
||||
if (
|
||||
destinationChanged ||
|
||||
sslChanged || // we need to push a new cert if the ssl changed
|
||||
portRangesChanged ||
|
||||
fullDomainChanged || // if the domain changes we need to update the certs and stuff
|
||||
destinationPortChanged
|
||||
) {
|
||||
const oldTargets = await generateSubnetProxyTargetV2(
|
||||
existingSiteResource,
|
||||
mergedAllClients
|
||||
);
|
||||
const newTargets = await generateSubnetProxyTargetV2(
|
||||
updatedSiteResource,
|
||||
mergedAllClients
|
||||
);
|
||||
|
||||
if (shouldUpdateTargets) {
|
||||
await updateTargets(
|
||||
newt.newtId,
|
||||
{
|
||||
@@ -939,49 +806,19 @@ export async function handleMessagingForUpdatedSiteResource(
|
||||
const olmJobs: Promise<void>[] = [];
|
||||
for (const client of mergedAllClients) {
|
||||
// does this client have access to another resource on this site that has the same destination still? if so we dont want to remove it from their olm yet
|
||||
// todo: optimize this query if needed
|
||||
if (!existingSiteResource.destination) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const oldDestinationStillInUseSites = await trx
|
||||
.select()
|
||||
.from(siteResources)
|
||||
.innerJoin(
|
||||
clientSiteResourcesAssociationsCache,
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||
siteResources.siteResourceId
|
||||
)
|
||||
)
|
||||
.innerJoin(
|
||||
siteNetworks,
|
||||
eq(siteNetworks.networkId, siteResources.networkId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.clientId,
|
||||
client.clientId
|
||||
),
|
||||
eq(siteNetworks.siteId, site.siteId),
|
||||
eq(
|
||||
siteResources.destination,
|
||||
existingSiteResource.destination
|
||||
),
|
||||
ne(
|
||||
siteResources.siteResourceId,
|
||||
existingSiteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const oldDestinationStillInUseByASite =
|
||||
oldDestinationStillInUseSites.length > 0;
|
||||
oldDestinationStillInUseClientSitePairs.has(
|
||||
`${client.clientId}:${site.siteId}`
|
||||
);
|
||||
|
||||
// we also need to update the remote subnets on the olms for each client that has access to this site
|
||||
olmJobs.push(
|
||||
updatePeerData(
|
||||
// TODO: THIS SHOULD BE UPDATED TO WORK I A BATCH
|
||||
client.clientId,
|
||||
site.siteId,
|
||||
destinationChanged
|
||||
|
||||
@@ -33,41 +33,59 @@ const createTargetParamsSchema = z.strictObject({
|
||||
resourceId: z.coerce.number().int().positive()
|
||||
});
|
||||
|
||||
const createTargetSchema = z.strictObject({
|
||||
siteId: z.int().positive(),
|
||||
ip: z.string().refine(isTargetValid),
|
||||
mode: z.enum(["http", "tcp", "udp", "ssh", "rdp", "vnc"]).optional(),
|
||||
method: z.string().optional().nullable(),
|
||||
port: z.int().min(1).max(65535),
|
||||
enabled: z.boolean().default(true),
|
||||
hcEnabled: z.boolean().optional(),
|
||||
hcPath: z.string().min(1).optional().nullable(),
|
||||
hcScheme: z.string().optional().nullable(),
|
||||
hcMode: z.string().optional().nullable(),
|
||||
hcHostname: z.string().optional().nullable(),
|
||||
hcPort: z.int().positive().optional().nullable(),
|
||||
hcInterval: z.int().positive().min(1).optional().nullable(),
|
||||
hcUnhealthyInterval: z.int().positive().min(1).optional().nullable(),
|
||||
hcTimeout: z.int().positive().min(1).optional().nullable(),
|
||||
hcHeaders: z
|
||||
.array(z.strictObject({ name: z.string(), value: z.string() }))
|
||||
.nullable()
|
||||
.optional(),
|
||||
hcFollowRedirects: z.boolean().optional().nullable(),
|
||||
hcMethod: z.string().min(1).optional().nullable(),
|
||||
hcStatus: z.int().optional().nullable(),
|
||||
hcTlsServerName: z.string().optional().nullable(),
|
||||
hcHealthyThreshold: z.int().positive().min(1).optional().nullable(),
|
||||
hcUnhealthyThreshold: z.int().positive().min(1).optional().nullable(),
|
||||
path: z.string().optional().nullable(),
|
||||
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(),
|
||||
rewritePath: z.string().optional().nullable(),
|
||||
rewritePathType: z
|
||||
.enum(["exact", "prefix", "regex", "stripPrefix"])
|
||||
.optional()
|
||||
.nullable(),
|
||||
priority: z.int().min(1).max(1000).optional().nullable()
|
||||
});
|
||||
const createTargetSchema = z
|
||||
.strictObject({
|
||||
siteId: z.int().positive(),
|
||||
ip: z.string().refine(isTargetValid),
|
||||
mode: z.enum(["http", "tcp", "udp", "ssh", "rdp", "vnc"]).optional(),
|
||||
method: z.string().optional().nullable(),
|
||||
port: z.int().min(1).max(65535),
|
||||
enabled: z.boolean().default(true),
|
||||
hcEnabled: z.boolean().optional(),
|
||||
hcPath: z.string().min(1).optional().nullable(),
|
||||
hcScheme: z.string().optional().nullable(),
|
||||
hcMode: z.string().optional().nullable(),
|
||||
hcHostname: z.string().optional().nullable(),
|
||||
hcPort: z.int().positive().optional().nullable(),
|
||||
hcInterval: z.int().positive().min(1).optional().nullable(),
|
||||
hcUnhealthyInterval: z.int().positive().min(1).optional().nullable(),
|
||||
hcTimeout: z.int().positive().min(1).optional().nullable(),
|
||||
hcHeaders: z
|
||||
.array(z.strictObject({ name: z.string(), value: z.string() }))
|
||||
.nullable()
|
||||
.optional(),
|
||||
hcFollowRedirects: z.boolean().optional().nullable(),
|
||||
hcMethod: z.string().min(1).optional().nullable(),
|
||||
hcStatus: z.int().optional().nullable(),
|
||||
hcTlsServerName: z.string().optional().nullable(),
|
||||
hcHealthyThreshold: z.int().positive().min(1).optional().nullable(),
|
||||
hcUnhealthyThreshold: z.int().positive().min(1).optional().nullable(),
|
||||
path: z.string().optional().nullable(),
|
||||
pathMatchType: z
|
||||
.enum(["exact", "prefix", "regex"])
|
||||
.optional()
|
||||
.nullable(),
|
||||
rewritePath: z.string().optional().nullable(),
|
||||
rewritePathType: z
|
||||
.enum(["exact", "prefix", "regex", "stripPrefix"])
|
||||
.optional()
|
||||
.nullable(),
|
||||
priority: z.int().min(1).max(1000).optional().nullable()
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
const hcHostnameMissing =
|
||||
data.hcHostname === undefined ||
|
||||
data.hcHostname === null ||
|
||||
data.hcHostname.trim().length === 0;
|
||||
|
||||
if (data.hcEnabled === true && hcHostnameMissing) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["hcHostname"],
|
||||
message: "hcHostname is required when hcEnabled is true"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type CreateTargetResponse = Target & TargetHealthCheck;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ const listTargetsParamsSchema = z.strictObject({
|
||||
resourceId: z.coerce.number().int().positive()
|
||||
});
|
||||
|
||||
const listTargetsSchema = z.object({
|
||||
const listTargetsSchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -188,6 +188,38 @@ export async function updateTarget(
|
||||
);
|
||||
}
|
||||
|
||||
const [existingHc] = await db
|
||||
.select()
|
||||
.from(targetHealthCheck)
|
||||
.where(eq(targetHealthCheck.targetId, targetId))
|
||||
.limit(1);
|
||||
|
||||
if (!existingHc) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Health check for target with ID ${targetId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const nextHcEnabled = parsedBody.data.hcEnabled ?? existingHc.hcEnabled;
|
||||
const nextHcHostname =
|
||||
parsedBody.data.hcHostname !== undefined
|
||||
? parsedBody.data.hcHostname
|
||||
: existingHc.hcHostname;
|
||||
const hcHostnameMissing =
|
||||
!nextHcHostname || nextHcHostname.trim().length === 0;
|
||||
|
||||
if (nextHcEnabled && hcHostnameMissing) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"hcHostname is required when hcEnabled is true"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const pathMatchTypeRemoved = parsedBody.data.pathMatchType === null;
|
||||
const nextMode =
|
||||
parsedBody.data.mode === null ? undefined : parsedBody.data.mode;
|
||||
@@ -218,21 +250,6 @@ export async function updateTarget(
|
||||
.where(eq(targets.targetId, targetId))
|
||||
.returning();
|
||||
|
||||
const [existingHc] = await trx
|
||||
.select()
|
||||
.from(targetHealthCheck)
|
||||
.where(eq(targetHealthCheck.targetId, targetId))
|
||||
.limit(1);
|
||||
|
||||
if (!existingHc) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Health check for target with ID ${targetId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let hcHeaders = null;
|
||||
if (parsedBody.data.hcHeaders) {
|
||||
hcHeaders = JSON.stringify(parsedBody.data.hcHeaders);
|
||||
|
||||
@@ -202,13 +202,11 @@ export async function acceptInvite(
|
||||
);
|
||||
});
|
||||
|
||||
calculateUserClientsForOrgs(existingUser[0].userId, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to calculate user clients after accepting invite for user ${existingUser[0].userId}: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
calculateUserClientsForOrgs(existingUser[0].userId).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to calculate user clients after accepting invite for user ${existingUser[0].userId}: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response<AcceptInviteResponse>(res, {
|
||||
data: { accepted: true, orgId: existingInvite.orgId },
|
||||
|
||||
@@ -159,13 +159,11 @@ export async function addUserRoleLegacy(
|
||||
});
|
||||
|
||||
for (const orgClient of orgClientsToRebuild) {
|
||||
rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for client ${orgClient.clientId} after adding role: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
rebuildClientAssociationsFromClient(orgClient).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for client ${orgClient.clientId} after adding role: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -55,7 +55,7 @@ export async function adminRemoveUser(
|
||||
await trx.delete(users).where(eq(users.userId, userId));
|
||||
});
|
||||
|
||||
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||
calculateUserClientsForOrgs(userId).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to calculate user clients after removing user ${userId}: ${e}`
|
||||
);
|
||||
|
||||
@@ -56,7 +56,6 @@ const bodySchema = z
|
||||
export type CreateOrgUserResponse = {};
|
||||
const CreateOrgUserResponseDataSchema = z.object({});
|
||||
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/user",
|
||||
@@ -77,7 +76,9 @@ registry.registerPath({
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createApiResponseSchema(CreateOrgUserResponseDataSchema)
|
||||
schema: createApiResponseSchema(
|
||||
CreateOrgUserResponseDataSchema
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,13 +327,11 @@ export async function createOrgUser(
|
||||
});
|
||||
|
||||
if (userIdForClients) {
|
||||
calculateUserClientsForOrgs(userIdForClients, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to calculate user clients after creating org user: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
calculateUserClientsForOrgs(userIdForClients).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to calculate user clients after creating org user: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return next(
|
||||
|
||||
@@ -109,7 +109,7 @@ export async function removeUserOrg(
|
||||
await removeUserFromOrg(org, userId, trx);
|
||||
});
|
||||
|
||||
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||
calculateUserClientsForOrgs(userId).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to calculate user clients after removing user ${userId} from org ${orgId}: ${e}`
|
||||
);
|
||||
|
||||
@@ -41,6 +41,13 @@ import { useParams } from "next/navigation";
|
||||
import { FaApple, FaWindows, FaLinux } from "react-icons/fa";
|
||||
import { SiAndroid } from "react-icons/si";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import {
|
||||
productUpdatesQueries,
|
||||
type LatestVersionResponse
|
||||
} from "@app/lib/queries";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import semver from "semver";
|
||||
import { InfoPopup } from "@app/components/ui/info-popup";
|
||||
|
||||
function formatTimestamp(timestamp: number | null | undefined): string {
|
||||
if (!timestamp) return "-";
|
||||
@@ -166,6 +173,34 @@ export default function GeneralPage() {
|
||||
}>(null);
|
||||
const [isCheckingCache, setIsCheckingCache] = useState(false);
|
||||
const [isRebuildingCache, setIsRebuildingCache] = useState(false);
|
||||
const data = useQuery(productUpdatesQueries.latestVersion(true));
|
||||
const latestPlatformVersions = data.data?.data;
|
||||
|
||||
const agentVersionMap: Record<string, string> = {
|
||||
"Pangolin Windows": "windows",
|
||||
"Pangolin Android": "android",
|
||||
"Pangolin iOS": "ios",
|
||||
"Pangolin iPadOS": "ios",
|
||||
"Pangolin macOS": "mac",
|
||||
"Pangolin CLI": "cli",
|
||||
"Olm CLI": "olm"
|
||||
};
|
||||
|
||||
let updateAvailable = false;
|
||||
if (client.agent && client.olmVersion && latestPlatformVersions) {
|
||||
const agent = agentVersionMap[
|
||||
client.agent
|
||||
] as keyof LatestVersionResponse;
|
||||
|
||||
if (agent in latestPlatformVersions) {
|
||||
const agentVersion = latestPlatformVersions[agent];
|
||||
|
||||
updateAvailable = semver.lt(
|
||||
client.olmVersion,
|
||||
agentVersion.latestVersion
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// get "imp" from local storage to determine if we should show the verify button (imp = "1" means show)
|
||||
const showVerifyButton =
|
||||
@@ -451,11 +486,21 @@ export default function GeneralPage() {
|
||||
{t("agent")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<Badge variant="secondary">
|
||||
{client.agent +
|
||||
" v" +
|
||||
client.olmVersion}
|
||||
</Badge>
|
||||
<div className="flex items-center">
|
||||
<Badge variant="secondary">
|
||||
{client.agent +
|
||||
" v" +
|
||||
client.olmVersion}
|
||||
</Badge>
|
||||
|
||||
{updateAvailable && (
|
||||
<InfoPopup
|
||||
info={t(
|
||||
"updateAvailableInfo"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,6 @@ import Link from "next/link";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useMemo, useState, useTransition } from "react";
|
||||
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
||||
import { build } from "@server/build";
|
||||
import type { QueryRequestAuditLogResponse } from "@server/routers/auditLogs/types";
|
||||
import { ColumnFilterButton } from "@app/components/ColumnFilterButton";
|
||||
|
||||
@@ -122,8 +121,7 @@ export default function GeneralPage() {
|
||||
...logQueries.requests({
|
||||
orgId: orgId as string,
|
||||
filters: queryFilters
|
||||
}),
|
||||
enabled: build !== "oss"
|
||||
})
|
||||
});
|
||||
|
||||
const rows = isLoading ? generateSampleRequestLogs() : (data?.log ?? []);
|
||||
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
} from "@app/components/ui/dropdown-menu";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
||||
import { useOptimisticLabels } from "@app/hooks/useOptimisticLabels";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import type { PaginationState } from "@tanstack/react-table";
|
||||
@@ -31,15 +31,18 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { startTransition, useMemo, useState, useTransition } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import z from "zod";
|
||||
import { ColumnFilterButton } from "./ColumnFilterButton";
|
||||
import { type SelectedLabel } from "./labels-selector";
|
||||
import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
|
||||
import { LabelsTableCell } from "./LabelsTableCell";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { ControlledDataTable } from "./ui/controlled-data-table";
|
||||
import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
|
||||
import { useLocalLabels } from "@app/hooks/useLocalLabels";
|
||||
import { useOptimisticLabels } from "@app/hooks/useOptimisticLabels";
|
||||
import {
|
||||
productUpdatesQueries,
|
||||
type LatestVersionResponse
|
||||
} from "@app/lib/queries";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import semver from "semver";
|
||||
import { InfoPopup } from "./ui/info-popup";
|
||||
|
||||
export type ClientRow = {
|
||||
id: number;
|
||||
@@ -101,6 +104,9 @@ export default function MachineClientsTable({
|
||||
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels);
|
||||
const data = useQuery(productUpdatesQueries.latestVersion(true));
|
||||
|
||||
const latestPlatformVersions = data.data?.data;
|
||||
|
||||
const defaultMachineColumnVisibility = {
|
||||
subnet: false,
|
||||
@@ -375,6 +381,37 @@ export default function MachineClientsTable({
|
||||
cell: ({ row }) => {
|
||||
const originalRow = row.original;
|
||||
|
||||
const agentVersionMap: Record<string, string> = {
|
||||
"Pangolin Windows": "windows",
|
||||
"Pangolin Android": "android",
|
||||
"Pangolin iOS": "ios",
|
||||
"Pangolin iPadOS": "ios",
|
||||
"Pangolin macOS": "mac",
|
||||
"Pangolin CLI": "cli",
|
||||
"Olm CLI": "olm"
|
||||
};
|
||||
|
||||
let updateAvailable = false;
|
||||
|
||||
if (
|
||||
originalRow.olmVersion &&
|
||||
originalRow.agent &&
|
||||
latestPlatformVersions
|
||||
) {
|
||||
const agent = agentVersionMap[
|
||||
originalRow.agent
|
||||
] as keyof LatestVersionResponse;
|
||||
|
||||
if (agent in latestPlatformVersions) {
|
||||
const agentVersion = latestPlatformVersions[agent];
|
||||
|
||||
updateAvailable = semver.lt(
|
||||
originalRow.olmVersion,
|
||||
agentVersion.latestVersion
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-1">
|
||||
{originalRow.agent && originalRow.olmVersion ? (
|
||||
@@ -386,9 +423,9 @@ export default function MachineClientsTable({
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
{/*originalRow.olmUpdateAvailable && (
|
||||
<InfoPopup info={t("olmUpdateAvailableInfo")} />
|
||||
)*/}
|
||||
{updateAvailable && (
|
||||
<InfoPopup info={t("updateAvailableInfo")} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -411,9 +411,9 @@ export function PrivateResourceForm({
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
const rolesQuery = useQuery(orgQueries.roles({ orgId }));
|
||||
const usersQuery = useQuery(orgQueries.users({ orgId }));
|
||||
const clientsQuery = useQuery(orgQueries.machineClients({ orgId }));
|
||||
const clientsQuery = useQuery(
|
||||
orgQueries.machineClients({ orgId, perPage: 1 })
|
||||
);
|
||||
const resourceRolesQuery = useQuery({
|
||||
...resourceQueries.siteResourceRoles({
|
||||
siteResourceId: siteResourceId ?? 0
|
||||
@@ -433,13 +433,6 @@ export function PrivateResourceForm({
|
||||
enabled: siteResourceId != null
|
||||
});
|
||||
|
||||
const allRoles = (rolesQuery.data ?? [])
|
||||
.map((r) => ({ id: r.roleId.toString(), text: r.name }))
|
||||
.filter((r) => r.text !== "Admin");
|
||||
const allUsers = (usersQuery.data ?? []).map((u) => ({
|
||||
id: u.id.toString(),
|
||||
text: `${getUserDisplayName({ email: u.email, username: u.username })}${u.type !== UserType.Internal ? ` (${u.idpName})` : ""}`
|
||||
}));
|
||||
const allClients = (clientsQuery.data ?? [])
|
||||
.filter((c) => !c.userId)
|
||||
.map((c) => ({ id: c.clientId.toString(), text: c.name }));
|
||||
@@ -478,8 +471,6 @@ export function PrivateResourceForm({
|
||||
}
|
||||
|
||||
const loadingRolesUsers =
|
||||
rolesQuery.isLoading ||
|
||||
usersQuery.isLoading ||
|
||||
clientsQuery.isLoading ||
|
||||
(siteResourceId != null &&
|
||||
(resourceRolesQuery.isLoading ||
|
||||
@@ -488,16 +479,6 @@ export function PrivateResourceForm({
|
||||
|
||||
const hasMachineClients = allClients.length > 0;
|
||||
|
||||
const [activeRolesTagIndex, setActiveRolesTagIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const [activeUsersTagIndex, setActiveUsersTagIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const [activeClientsTagIndex, setActiveClientsTagIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const [sshServerMode, setSshServerMode] = useState<"standard" | "native">(
|
||||
() => {
|
||||
if (variant === "edit" && resource) {
|
||||
@@ -599,6 +580,7 @@ export function PrivateResourceForm({
|
||||
});
|
||||
|
||||
const mode = form.watch("mode");
|
||||
const aliasValue = form.watch("alias");
|
||||
const httpConfigSubdomain = form.watch("httpConfigSubdomain");
|
||||
const httpConfigDomainId = form.watch("httpConfigDomainId");
|
||||
const httpConfigFullDomain = form.watch("httpConfigFullDomain");
|
||||
@@ -614,6 +596,9 @@ export function PrivateResourceForm({
|
||||
!isNative &&
|
||||
pamMode === "push" &&
|
||||
authDaemonMode === "remote";
|
||||
const aliasEndsWithLocal =
|
||||
typeof aliasValue === "string" &&
|
||||
aliasValue.trim().toLowerCase().endsWith(".local");
|
||||
const hasInitialized = useRef(false);
|
||||
const previousResourceId = useRef<number | null>(null);
|
||||
const initialSitesRef = useRef(initialSites);
|
||||
@@ -1228,6 +1213,13 @@ export function PrivateResourceForm({
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
{aliasEndsWithLocal && (
|
||||
<p className="text-xs text-amber-700/80 mt-1">
|
||||
{t(
|
||||
"internalResourceAliasLocalWarning"
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -1756,6 +1748,7 @@ export function PrivateResourceForm({
|
||||
field.value ?? []
|
||||
}
|
||||
orgId={orgId}
|
||||
restrictAdminRole
|
||||
onSelectRoles={(
|
||||
newUsers
|
||||
) => {
|
||||
|
||||
@@ -55,6 +55,9 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
|
||||
import { LabelsTableCell } from "./LabelsTableCell";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { productUpdatesQueries } from "@app/lib/queries";
|
||||
import semver from "semver";
|
||||
|
||||
export type SiteRow = {
|
||||
id: number;
|
||||
@@ -113,12 +116,11 @@ export default function SitesTable({
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
|
||||
// useEffect(() => {
|
||||
// const interval = setInterval(() => {
|
||||
// router.refresh();
|
||||
// }, 30_000);
|
||||
// return () => clearInterval(interval);
|
||||
// }, []);
|
||||
const { data: latestVersions } = useQuery(
|
||||
productUpdatesQueries.latestVersion(true)
|
||||
);
|
||||
|
||||
const latestNewtVersion = latestVersions?.data?.newt?.latestVersion;
|
||||
|
||||
const booleanSearchFilterSchema = z
|
||||
.enum(["true", "false"])
|
||||
@@ -333,6 +335,11 @@ export default function SitesTable({
|
||||
cell: ({ row }) => {
|
||||
const originalRow = row.original;
|
||||
|
||||
let updateAvailable =
|
||||
latestNewtVersion &&
|
||||
originalRow.newtVersion &&
|
||||
semver.lt(originalRow.newtVersion, latestNewtVersion);
|
||||
|
||||
if (originalRow.type === "newt") {
|
||||
return (
|
||||
<div className="flex items-center space-x-1">
|
||||
@@ -346,7 +353,7 @@ export default function SitesTable({
|
||||
)}
|
||||
</div>
|
||||
</Badge>
|
||||
{originalRow.newtUpdateAvailable && (
|
||||
{updateAvailable && (
|
||||
<InfoPopup
|
||||
info={t("newtUpdateAvailableInfo")}
|
||||
/>
|
||||
@@ -561,7 +568,7 @@ export default function SitesTable({
|
||||
}
|
||||
|
||||
return cols;
|
||||
}, [isLabelFeatureEnabled, orgId, t, searchParams]);
|
||||
}, [isLabelFeatureEnabled, orgId, t, searchParams, latestNewtVersion]);
|
||||
|
||||
function toggleSort(column: string) {
|
||||
const newSearch = getNextSortOrder(column, searchParams);
|
||||
|
||||
@@ -38,6 +38,12 @@ import { ColumnFilterButton } from "./ColumnFilterButton";
|
||||
import IdpTypeBadge from "./IdpTypeBadge";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { ControlledDataTable } from "./ui/controlled-data-table";
|
||||
import {
|
||||
productUpdatesQueries,
|
||||
type LatestVersionResponse
|
||||
} from "@app/lib/queries";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import semver from "semver";
|
||||
|
||||
export type ClientRow = {
|
||||
id: number;
|
||||
@@ -100,6 +106,9 @@ export default function UserDevicesTable({
|
||||
searchParams
|
||||
} = useNavigationContext();
|
||||
const [isRefreshing, startTransition] = useTransition();
|
||||
const data = useQuery(productUpdatesQueries.latestVersion(true));
|
||||
|
||||
const latestPlatformVersions = data.data?.data;
|
||||
|
||||
const defaultUserColumnVisibility = {
|
||||
subnet: false,
|
||||
@@ -555,6 +564,37 @@ export default function UserDevicesTable({
|
||||
cell: ({ row }) => {
|
||||
const originalRow = row.original;
|
||||
|
||||
const agentVersionMap: Record<string, string> = {
|
||||
"Pangolin Windows": "windows",
|
||||
"Pangolin Android": "android",
|
||||
"Pangolin iOS": "ios",
|
||||
"Pangolin iPadOS": "ios",
|
||||
"Pangolin macOS": "mac",
|
||||
"Pangolin CLI": "cli",
|
||||
"Olm CLI": "olm"
|
||||
};
|
||||
|
||||
let updateAvailable = false;
|
||||
|
||||
if (
|
||||
originalRow.olmVersion &&
|
||||
originalRow.agent &&
|
||||
latestPlatformVersions
|
||||
) {
|
||||
const agent = agentVersionMap[
|
||||
originalRow.agent
|
||||
] as keyof LatestVersionResponse;
|
||||
|
||||
if (agent in latestPlatformVersions) {
|
||||
const agentVersion = latestPlatformVersions[agent];
|
||||
|
||||
updateAvailable = semver.lt(
|
||||
originalRow.olmVersion,
|
||||
agentVersion.latestVersion
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-1">
|
||||
{originalRow.agent && originalRow.olmVersion ? (
|
||||
@@ -567,9 +607,9 @@ export default function UserDevicesTable({
|
||||
"-"
|
||||
)}
|
||||
|
||||
{/*originalRow.olmUpdateAvailable && (
|
||||
<InfoPopup info={t("olmUpdateAvailableInfo")} />
|
||||
)*/}
|
||||
{updateAvailable && (
|
||||
<InfoPopup info={t("updateAvailableInfo")} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -714,7 +754,7 @@ export default function UserDevicesTable({
|
||||
}
|
||||
|
||||
return allOptions;
|
||||
}, [t]);
|
||||
}, [t, latestPlatformVersions]);
|
||||
|
||||
function handleFilterChange(
|
||||
column: string,
|
||||
|
||||
@@ -63,6 +63,34 @@ export type LatestVersionResponse = {
|
||||
latestVersion: string;
|
||||
releaseNotes: string;
|
||||
};
|
||||
newt: {
|
||||
latestVersion: string;
|
||||
releaseNotes: string;
|
||||
};
|
||||
cli: {
|
||||
latestVersion: string;
|
||||
releaseNotes: string;
|
||||
};
|
||||
"panglin-node": {
|
||||
latestVersion: string;
|
||||
releaseNotes: string;
|
||||
};
|
||||
windows: {
|
||||
latestVersion: string;
|
||||
releaseNotes: string;
|
||||
};
|
||||
android: {
|
||||
latestVersion: string;
|
||||
releaseNotes: string;
|
||||
};
|
||||
mac: {
|
||||
latestVersion: string;
|
||||
releaseNotes: string;
|
||||
};
|
||||
ios: {
|
||||
latestVersion: string;
|
||||
releaseNotes: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const productUpdatesQueries = {
|
||||
|
||||
Reference in New Issue
Block a user