Compare commits

...

50 Commits

Author SHA1 Message Date
Owen bc63747efe Refactor out transactions and always call rebuild on update 2026-06-23 18:12:51 -04:00
Owen 2a8ceeec1b Restrict admin role 2026-06-23 17:45:42 -04:00
Owen 91ef0d0153 Show warning about the .local aliases 2026-06-23 17:44:18 -04:00
Owen b8101402cd Merge branch 'main' into dev 2026-06-23 17:14:53 -04:00
Owen 7731849a2f Standardize db rebuildClientAssociationsFromClient 2026-06-23 17:14:40 -04:00
Owen c11d24e10a Standardize db rebuildClientAssociationsFromClient 2026-06-23 17:14:40 -04:00
Owen a9b7cce49b Improve efficiency of calculateUserClientsForOrgs 2026-06-23 17:14:40 -04:00
Owen d78223b94f Fix import to be private 2026-06-23 17:14:40 -04:00
Owen Schwartz 963e9da7dd Merge pull request #3213 from ivenos/rename-to-compose-yaml
Rename docker-compose.yml to compose.yaml
2026-06-23 12:03:56 -07:00
Owen Schwartz 2cbc88fa05 Merge pull request #3326 from fosrl/dependabot/npm_and_yarn/js-yaml-4.2.0
Bump js-yaml from 4.1.1 to 4.2.0
2026-06-23 12:02:49 -07:00
Owen Schwartz 65bad456cb Merge pull request #3327 from fosrl/crowdin_dev
New Crowdin updates
2026-06-23 12:02:11 -07:00
Owen f48a4f7bc0 Enforce strick query params
Fixes #3313
2026-06-23 12:22:47 -04:00
Owen ce3c2f7583 Fix #3314 2026-06-23 12:05:47 -04:00
Owen 51c357e6c7 Merge branch 'main' into dev 2026-06-23 11:30:46 -04:00
Owen Schwartz 7ae29612d4 Merge pull request #3302 from RitwijParmar/codex/resource-update-inline-policy-response
Fix inline policy fields in resource update response
2026-06-23 08:30:31 -07:00
Owen da794adb7d Merge branch 'main' into dev 2026-06-23 11:26:03 -04:00
Owen 8004ae6870 Use the policy when updating rule
Fixes #3273
2026-06-23 11:25:53 -04:00
Owen Schwartz 1bff7bbc2f Merge pull request #3315 from fosrl/dependabot/npm_and_yarn/nodemailer-9.0.1
Bump nodemailer from 8.0.9 to 9.0.1
2026-06-23 07:58:58 -07:00
Owen Schwartz 50db5695fc Merge pull request #3264 from fosrl/dependabot/npm_and_yarn/esbuild-0.28.1
Bump esbuild from 0.28.0 to 0.28.1
2026-06-23 07:57:47 -07:00
dependabot[bot] cfb63f9742 Bump js-yaml from 4.1.1 to 4.2.0
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.2.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-22 21:04:39 +00:00
Owen Schwartz c76b4555e1 Merge pull request #3316 from fosrl/dependabot/npm_and_yarn/form-data-4.0.6
Bump form-data from 4.0.5 to 4.0.6
2026-06-22 14:03:06 -07:00
Owen Schwartz c25bfbad27 Merge pull request #3317 from fosrl/dependabot/github_actions/actions/checkout-7.0.0
Bump actions/checkout from 6.0.2 to 7.0.0
2026-06-22 14:02:51 -07:00
Owen Schwartz 44782f8963 Merge pull request #3323 from fosrl/dependabot/go_modules/install/go-install-dependencies-4dfeb96e78
Bump golang.org/x/term from 0.43.0 to 0.44.0 in /install in the go-install-dependencies group
2026-06-22 14:02:31 -07:00
Owen Schwartz e6f7cd6da9 Merge pull request #3206 from gmpinder/fix-idp-delete
fix: Add DELETE /idp/{idpId} to integration API
2026-06-22 14:01:47 -07:00
Owen Schwartz 19faa3a29c Merge pull request #3223 from Adityakk9031/#2867
fix: request logs not loading on initial page open in Community Editi…
2026-06-22 14:00:29 -07:00
Owen c284dc2e83 Merge branch 'Fredkiss3-refactor/show-if-client-needs-update' into dev 2026-06-22 16:58:55 -04:00
Owen 1b634955d8 Merge branch 'refactor/show-if-client-needs-update' of github.com:Fredkiss3/pangolin into dev 2026-06-22 16:58:50 -04:00
Fred KISSIE be888c3fc1 💄 Show the latest new update in machine client table 2026-06-22 16:57:47 -04:00
Fred KISSIE 3f2bb42221 ♻️ lt instead of lte 2026-06-22 16:57:47 -04:00
Fred KISSIE 5dc3ae4c7f ♻️ sites & clients should not get latest versions on the server 2026-06-22 16:57:45 -04:00
Fred KISSIE ffb6c64de0 💄 Show updates available in the frontend, on sites & user devices 2026-06-22 16:57:08 -04:00
Fred KISSIE 2cbc6fb128 🏷️ types 2026-06-22 16:57:08 -04:00
Fred KISSIE 75084028d7 ♻️ Remove queries that prefetch 1000 users/roles in private resources form 2026-06-22 16:57:08 -04:00
Owen f44a7c55dd Merge branch 'refactor/show-if-client-needs-update' of github.com:Fredkiss3/pangolin into Fredkiss3-refactor/show-if-client-needs-update 2026-06-22 16:56:52 -04:00
dependabot[bot] 753358a17d Bump golang.org/x/term in /install in the go-install-dependencies group
Bumps the go-install-dependencies group in /install with 1 update: [golang.org/x/term](https://github.com/golang/term).


Updates `golang.org/x/term` from 0.43.0 to 0.44.0
- [Commits](https://github.com/golang/term/compare/v0.43.0...v0.44.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-install-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-22 14:32:24 +00:00
dependabot[bot] d747b45f0b Bump actions/checkout from 6.0.2 to 7.0.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 7.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-22 01:33:16 +00:00
dependabot[bot] a24091257a Bump form-data from 4.0.5 to 4.0.6
Bumps [form-data](https://github.com/form-data/form-data) from 4.0.5 to 4.0.6.
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.5...v4.0.6)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-21 19:40:11 +00:00
dependabot[bot] 1c60041390 Bump nodemailer from 8.0.9 to 9.0.1
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 8.0.9 to 9.0.1.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v8.0.9...v9.0.1)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 9.0.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-21 09:46:17 +00:00
Ritwij Aryan Parmar 95c3f74a33 Fix inline policy fields in resource update response 2026-06-17 14:36:34 -04:00
dependabot[bot] cedccd8cdb Bump esbuild from 0.28.0 to 0.28.1
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.28.0 to 0.28.1.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.28.0...v0.28.1)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-13 01:23:46 +00:00
Fred KISSIE 7a275c86c2 Merge branch 'dev' into refactor/show-if-client-needs-update 2026-06-11 21:05:31 +02:00
Fred KISSIE 4b703b5c11 💄 Show the latest new update in machine client table 2026-06-11 20:58:23 +02:00
Fred KISSIE 1b6e9e8cfe ♻️ lt instead of lte 2026-06-11 19:55:48 +02:00
Fred KISSIE fe55956079 ♻️ sites & clients should not get latest versions on the server 2026-06-10 22:58:42 +02:00
Fred KISSIE 4cd0b9a0bb 💄 Show updates available in the frontend, on sites & user devices 2026-06-10 22:57:55 +02:00
Fred KISSIE ab4d567af9 🏷️ types 2026-06-10 20:56:24 +02:00
Fred KISSIE 38203e522b ♻️ Remove queries that prefetch 1000 users/roles in private resources form 2026-06-09 19:29:00 +02:00
Aditya kumar singh 13b691fd7d fix: request logs not loading on initial page open in Community Edition (#2867) 2026-06-06 00:34:48 +05:30
ivenos 89f3f3c8cd Rename docker-compose.yml to compose.yaml 2026-06-04 10:01:35 +02:00
Gerald Pinder 44c16d69af fix: Add DELETE /idp/{idpId} to integration API 2026-06-03 12:48:34 -04:00
83 changed files with 1326 additions and 1345 deletions
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
View File
+2 -2
View File
@@ -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
View File
@@ -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=
+2
View File
@@ -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.",
+200 -238
View File
@@ -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
View File
@@ -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"
}
+30 -163
View File
@@ -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;
+55 -20
View File
@@ -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(
+6 -9
View File
@@ -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);
}
});
}
+1 -1
View File
@@ -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}`
);
+5 -7
View File
@@ -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()
+1 -1
View File
@@ -15,7 +15,7 @@ const paramsSchema = z.object({
apiKeyId: z.string().nonempty()
});
const querySchema = z.object({
const querySchema = z.strictObject({
limit: z
.string()
.optional()
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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}`
);
+5 -7
View File
@@ -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, {
+5 -7
View File
@@ -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, {
+5 -7
View File
@@ -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,
+1 -1
View File
@@ -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()
+1 -26
View File
@@ -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) {
+1 -1
View File
@@ -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 }
+7 -1
View File
@@ -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,
+1 -1
View File
@@ -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
+5 -7
View File
@@ -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,
+1 -1
View File
@@ -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()
+10 -4
View File
@@ -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(
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -14,7 +14,7 @@ const listOrgsParamsSchema = z.object({
userId: z.string()
});
const listOrgsSchema = z.object({
const listOrgsSchema = z.strictObject({
limit: z
.string()
.optional()
+2 -7
View File
@@ -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
};
}
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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()
+8 -1
View File
@@ -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",
+124 -33
View File
@@ -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,
+1 -1
View File
@@ -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()
+5 -130
View File
@@ -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, {
+240 -403
View File
@@ -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
+53 -35
View File
@@ -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;
+1 -1
View File
@@ -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()
+32 -15
View File
@@ -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);
+5 -7
View File
@@ -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 },
+5 -7
View File
@@ -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, {
+1 -1
View File
@@ -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}`
);
+8 -9
View File
@@ -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(
+1 -1
View File
@@ -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 ?? []);
+46 -9
View File
@@ -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>
);
}
+15 -22
View File
@@ -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
) => {
+15 -8
View File
@@ -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);
+44 -4
View File
@@ -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,
+28
View File
@@ -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 = {