From 47251539625bab6815ef6fd525f0b5dd82b99007 Mon Sep 17 00:00:00 2001 From: xJayMorex <3513159+xJayMorex@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:49:17 +0200 Subject: [PATCH 01/62] fix: only chown /data/keys.json if necessary --- docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh index 4236c90a2e..1306e70356 100755 --- a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh @@ -49,7 +49,9 @@ done # Ensure the JWT key file is owned by the runtime user, even when the /data # directory ownership already matches PUID:PGID (chownit skips recursion then) if [ -f /data/keys.json ]; then - chown "$PUID:$PGID" /data/keys.json + if [ "$(stat -c '%u:%g' '/data/keys.json')" != "$PUID:$PGID" ]; then + chown "$PUID:$PGID" /data/keys.json + fi fi if [ "$(is_true "${SKIP_CERTBOT_OWNERSHIP:-}")" = '1' ]; then From a54b73f1535efc357cea46961e84c73f76661e7c Mon Sep 17 00:00:00 2001 From: Amit Yadav Date: Fri, 25 Sep 2026 20:35:32 +0530 Subject: [PATCH 02/62] fix: omit certificate_key from certificate API responses The private key of custom certificates was returned in meta by GET /api/nginx/certificates and GET /api/nginx/certificates/{id}. Add meta.certificate_key to omissions(), the same way meta.dns_provider_credentials is hidden. The key stays in the database and is still written to /data/custom_ssl on upload. --- backend/internal/certificate.js | 2 +- test/cypress/e2e/api/Certificates.cy.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 963d2bb6a4..9a14f58938 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -25,7 +25,7 @@ const certbotLogsDir = "/data/logs"; const certbotWorkDir = "/tmp/letsencrypt-lib"; const omissions = () => { - return ["is_deleted", "owner.is_deleted", "meta.dns_provider_credentials"]; + return ["is_deleted", "owner.is_deleted", "meta.dns_provider_credentials", "meta.certificate_key"]; }; const internalCertificate = { diff --git a/test/cypress/e2e/api/Certificates.cy.js b/test/cypress/e2e/api/Certificates.cy.js index 25d1b8c41f..7d75d40cf8 100644 --- a/test/cypress/e2e/api/Certificates.cy.js +++ b/test/cypress/e2e/api/Certificates.cy.js @@ -69,6 +69,9 @@ describe('Certificates endpoints', () => { }).then((data) => { cy.validateSwaggerSchema('get', 200, '/nginx/certificates', data); expect(data.length).to.be.greaterThan(0); + const cert = data.find((c) => c.id === certID); + expect(cert.meta).to.have.property('certificate'); + expect(cert.meta).to.not.have.property('certificate_key'); // Delete cert cy.task('backendApiDelete', { From c699dc96d41693b3f40c907c3b78d747e5eee2a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 07:19:54 +0000 Subject: [PATCH 03/62] build(deps): bump ip-address from 10.5.0 to 10.7.2 in /backend Bumps [ip-address](https://github.com/beaugunderson/ip-address) from 10.5.0 to 10.7.2. - [Release notes](https://github.com/beaugunderson/ip-address/releases) - [Commits](https://github.com/beaugunderson/ip-address/compare/v10.5.0...v10.7.2) --- updated-dependencies: - dependency-name: ip-address dependency-version: 10.7.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- backend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/yarn.lock b/backend/yarn.lock index 39efe544ad..1d7daab0a9 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1150,9 +1150,9 @@ interpret@^2.2.0: integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== ip-address@^10.1.1: - version "10.5.0" - resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.5.0.tgz#fcd6d7dfe9e68416b7cb71afe9bc960b3e38c13b" - integrity sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g== + version "10.7.2" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.7.2.tgz#5b3b2b7d46c6293861b82dfcd49cc203aa8f2c5c" + integrity sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w== ipaddr.js@1.9.1: version "1.9.1" From 77dfc3d3c1fb01b968f4b373c8d85a050f187b92 Mon Sep 17 00:00:00 2001 From: Prassanth <80441547+PrassanthVG@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:08:28 +0530 Subject: [PATCH 04/62] Don't start IP ranges renewal timer when fetching is disabled IP_RANGES_FETCH_ENABLED=false only skipped the initial fetch; the renewal timer still fetched the ranges every 6 hours. Fixes #5748 --- backend/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/index.js b/backend/index.js index 0028566727..a6afda7bbf 100644 --- a/backend/index.js +++ b/backend/index.js @@ -26,7 +26,9 @@ async function appStart() { }) .then(() => { internalCertificate.initTimer(); - internalIpRanges.initTimer(); + if (IP_RANGES_FETCH_ENABLED) { + internalIpRanges.initTimer(); + } const server = app.listen(3000, () => { logger.info(`Backend PID ${process.pid} listening on port 3000 ...`); From 092d212ad70d00c57d6f15f43a8026c9e8c42c13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 14:38:20 +0000 Subject: [PATCH 05/62] build(deps): bump serialize-javascript from 7.1.1 to 7.1.2 in /test Bumps [serialize-javascript](https://github.com/yahoo/serialize-javascript) from 7.1.1 to 7.1.2. - [Release notes](https://github.com/yahoo/serialize-javascript/releases) - [Commits](https://github.com/yahoo/serialize-javascript/compare/v7.1.1...v7.1.2) --- updated-dependencies: - dependency-name: serialize-javascript dependency-version: 7.1.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- test/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/yarn.lock b/test/yarn.lock index 4eb1a544f7..5f24217e98 100644 --- a/test/yarn.lock +++ b/test/yarn.lock @@ -1928,9 +1928,9 @@ semver@^7.6.3: integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== serialize-javascript@^7.0.2: - version "7.1.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-7.1.1.tgz#c3b0a7ac13df6cf2fcda71cbb142ba9392a710b9" - integrity sha512-k3CMsaIvvdSwm8oLB4MXSl0wH2/cwlH7xGcnRd2DaeRmBkbzYmyT8j0tsX60DwD1eRwHTpNpH8ljKu9oUT1MeQ== + version "7.1.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-7.1.2.tgz#0161010a0815dd7b7e011cbc5a3644850b8c2289" + integrity sha512-GL2BWwVa6JydKO6l/ljVgjAZF4QJ3S7dWDWi53s5GZT8PJzD2fNxi67HANQGKAqPrwEpL5ba7gUBGi0Ls/sEoQ== shebang-command@^2.0.0: version "2.0.0" From f5e6dfecfbdcf6742b58d5805891c4fccf3c29c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 09:34:18 +0000 Subject: [PATCH 06/62] build(deps): bump fast-uri from 3.1.7 to 3.1.8 in /backend Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.7 to 3.1.8. - [Release notes](https://github.com/fastify/fast-uri/releases) - [Commits](https://github.com/fastify/fast-uri/compare/v3.1.7...v3.1.8) --- updated-dependencies: - dependency-name: fast-uri dependency-version: 3.1.8 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- backend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/yarn.lock b/backend/yarn.lock index 1d7daab0a9..8f4238ad58 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -899,9 +899,9 @@ fast-fifo@^1.2.0, fast-fifo@^1.3.2: integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== fast-uri@^3.0.1: - version "3.1.7" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.7.tgz#743157d957f3cbb4c65310e033dc2ad4ad7dc60a" - integrity sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg== + version "3.1.8" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.8.tgz#f7db8d942e20eead3cfe93b0beeb4eb0169e938b" + integrity sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg== fdir@^6.5.0: version "6.5.0" From 2afd723cd8ab4b0b3f7a45cb6230c6b42b09cb02 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Mon, 5 Oct 2026 21:59:46 +1000 Subject: [PATCH 07/62] Fix access list rules and SSL missing from re-rendered host configs Some paths rendered host configs without fully loading the relations the templates depend on, silently dropping access rules (and SSL): - Enabling a proxy host loaded the access list without its clients or auth items, so the host came back with no access restrictions. - Requesting a Let's Encrypt certificate re-rendered every host sharing its domains from bare rows, dropping access rules and SSL. generateConfig now loads any referenced certificate or access list that isn't fully loaded, so no caller can render a partial config. The two callers above also fetch the relations themselves, and a warning is logged when a host's access list is missing or has no rules. Adds integration tests covering access lists across disable/enable. Fixes #5919, #4286, #5710 Co-Authored-By: Claude Opus 5.5 --- backend/internal/host.js | 9 +- backend/internal/nginx.js | 52 ++++- backend/internal/proxy-host.js | 2 +- docker/docker-compose.ci.yml | 1 + docker/docker-compose.dev.yml | 1 + .../e2e/api/AccessListHostToggle.cy.js | 206 ++++++++++++++++++ test/docker/nginx/conf.d/website123.conf | 2 +- 7 files changed, 267 insertions(+), 6 deletions(-) create mode 100644 test/cypress/e2e/api/AccessListHostToggle.cy.js diff --git a/backend/internal/host.js b/backend/internal/host.js index 748716262a..81efe160e9 100644 --- a/backend/internal/host.js +++ b/backend/internal/host.js @@ -79,15 +79,18 @@ const internalHost = { redirection_hosts: [], }; - const proxyRes = await proxyHostModel.query().where("is_deleted", 0); + const proxyRes = await proxyHostModel + .query() + .where("is_deleted", 0) + .withGraphFetched("[certificate, access_list.[clients,items]]"); responseObject.proxy_hosts = internalHost._getHostsWithDomains(proxyRes, domainNames); responseObject.total_count += responseObject.proxy_hosts.length; - const redirRes = await redirectionHostModel.query().where("is_deleted", 0); + const redirRes = await redirectionHostModel.query().where("is_deleted", 0).withGraphFetched("[certificate]"); responseObject.redirection_hosts = internalHost._getHostsWithDomains(redirRes, domainNames); responseObject.total_count += responseObject.redirection_hosts.length; - const deadRes = await deadHostModel.query().where("is_deleted", 0); + const deadRes = await deadHostModel.query().where("is_deleted", 0).withGraphFetched("[certificate]"); responseObject.dead_hosts = internalHost._getHostsWithDomains(deadRes, domainNames); responseObject.total_count += responseObject.dead_hosts.length; diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js index a50e0780dc..378b53591d 100644 --- a/backend/internal/nginx.js +++ b/backend/internal/nginx.js @@ -7,6 +7,7 @@ import errs from "../lib/error.js"; import utils from "../lib/utils.js"; import { debug, nginx as logger } from "../logger.js"; import accessListModel from "../models/access_list.js"; +import certificateModel from "../models/certificate.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -209,11 +210,13 @@ const internalNginx = { * @param {Object} host * @returns {Promise} */ - generateConfig: (host_type, host_row) => { + generateConfig: async (host_type, host_row) => { // Prevent modifying the original object: const host = JSON.parse(JSON.stringify(host_row)); const nice_host_type = internalNginx.getFileFriendlyHostType(host_type); + await internalNginx.loadRenderRelations(nice_host_type, host); + debug(logger, `Generating ${nice_host_type} Config:`, JSON.stringify(host, null, 2)); const renderEngine = utils.getRenderEngine(); @@ -299,6 +302,53 @@ const internalNginx = { }); }, + /** + * The templates rely on the certificate and access list relations being loaded. + * Not every caller fetches them (or fetches them fully), and when they're missing + * the config silently renders without SSL or without any access rules (#5919). + * Load anything that is referenced but not loaded onto the given host object. + * + * @param {String} host_type + * @param {Object} host + * @returns {Promise} + */ + loadRenderRelations: async (host_type, host) => { + if (host.certificate_id > 0 && host.certificate?.id !== host.certificate_id) { + const certificate = await certificateModel + .query() + .where("is_deleted", 0) + .andWhere("id", host.certificate_id) + .first(); + host.certificate = certificate ? JSON.parse(JSON.stringify(certificate)) : null; + } + + if (host_type === "proxy_host" && host.access_list_id > 0) { + if ( + host.access_list?.id !== host.access_list_id || + !Array.isArray(host.access_list.clients) || + !Array.isArray(host.access_list.items) + ) { + const accessList = await accessListModel + .query() + .where("is_deleted", 0) + .andWhere("id", host.access_list_id) + .withGraphFetched("[clients,items]") + .first(); + host.access_list = accessList ? JSON.parse(JSON.stringify(accessList)) : null; + } + + if (!host.access_list) { + logger.warn( + `Proxy Host #${host.id} references Access List #${host.access_list_id} which does not exist`, + ); + } else if (!host.access_list.clients.length && !host.access_list.items.length) { + logger.warn( + `Proxy Host #${host.id} uses Access List #${host.access_list_id} which has no clients or authorizations, no access restrictions will apply`, + ); + } + } + }, + /** * This generates a temporary nginx config listening on port 80 for the domain names listed * in the certificate setup. It allows the letsencrypt acme challenge to be requested by letsencrypt diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js index eac0c148aa..f079dab023 100644 --- a/backend/internal/proxy-host.js +++ b/backend/internal/proxy-host.js @@ -323,7 +323,7 @@ const internalProxyHost = { .then(() => { return internalProxyHost.get(access, { id: data.id, - expand: ["certificate", "owner", "access_list"], + expand: ["certificate", "owner", "access_list.[clients,items]"], }); }) .then(async (row) => { diff --git a/docker/docker-compose.ci.yml b/docker/docker-compose.ci.yml index be6f517d3d..2aef4e8ff7 100644 --- a/docker/docker-compose.ci.yml +++ b/docker/docker-compose.ci.yml @@ -37,6 +37,7 @@ services: - website1.example.com - website2.example.com - website3.example.com + - website4.example.com examplesite: image: "${IMAGE}-examplesite:ci-${BUILD_NUMBER}" diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index d6b07ec012..20e01e6be5 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -16,6 +16,7 @@ services: - website1.example.com - website2.example.com - website3.example.com + - website4.example.com environment: TZ: "${TZ:-Australia/Brisbane}" PUID: 1000 diff --git a/test/cypress/e2e/api/AccessListHostToggle.cy.js b/test/cypress/e2e/api/AccessListHostToggle.cy.js new file mode 100644 index 0000000000..b566a3a98f --- /dev/null +++ b/test/cypress/e2e/api/AccessListHostToggle.cy.js @@ -0,0 +1,206 @@ +/// + +// Regression tests for #5919, #4286 and #5710: access list rules must survive +// every path that re-renders a proxy host config, not just the initial save. +describe('Access Lists across Proxy Host disable/enable', () => { + const domain = 'website4.example.com'; + + const creds = { + username: 'toggle-user', + password: 'toggle-pass', + }; + + let token; + let authListId; + let clientListId; + let hostId; + + /** + * Requests a path on the proxy host directly (bypassing squid) + * and yields the HTTP status code + * + * @param {object} [auth] + * @param {string} auth.username + * @param {string} auth.password + */ + const request = (auth) => { + const authArg = auth ? `-u '${auth.username}:${auth.password}'` : ''; + return cy.exec(`curl --noproxy '*' -s -o /dev/null -w '%{http_code}' ${authArg} http://${domain}/`) + .then((result) => { + expect(result.exitCode).to.eq(0); + return result.stdout.trim(); + }); + }; + + // nginx reloads are signalled and return immediately, so the old + // config can still be served for a moment after an API change. + // Retry until the expected status is seen. + const expectStatus = (auth, status) => { + let last = null; + cy.waitUntil(() => request(auth).then((res) => { + last = res; + return res === status; + }), { + timeout: 15000, + interval: 500, + errorMsg: () => `${domain} did not return ${status}, last status: ${last}`, + }); + }; + + const createAccessList = (data) => { + return cy.task('backendApiPost', { + token: token, + path: '/api/nginx/access-lists', + data: { + satisfy_any: false, + pass_auth: false, + items: [], + clients: [], + ...data, + } + }).then((res) => { + cy.validateSwaggerSchema('post', 201, '/nginx/access-lists', res); + expect(res).to.have.property('id'); + expect(res.id).to.be.greaterThan(0); + return cy.wrap(res.id); + }); + }; + + const setHostAccessList = (accessListId) => { + cy.task('backendApiPut', { + token: token, + path: `/api/nginx/proxy-hosts/${hostId}`, + data: { + access_list_id: accessListId, + } + }).then((data) => { + // No swagger validation here: with a host access list set, the expanded + // access_list in the response has no proxy_host_count, which the schema requires + expect(data).to.have.property('access_list_id', accessListId); + }); + }; + + const toggleHost = () => { + cy.task('backendApiPost', { + token: token, + path: `/api/nginx/proxy-hosts/${hostId}/disable`, + }).then((data) => { + cy.validateSwaggerSchema('post', 200, '/nginx/proxy-hosts/{hostID}/disable', data); + expect(data).to.be.equal(true); + }); + + cy.task('backendApiPost', { + token: token, + path: `/api/nginx/proxy-hosts/${hostId}/enable`, + }).then((data) => { + cy.validateSwaggerSchema('post', 200, '/nginx/proxy-hosts/{hostID}/enable', data); + expect(data).to.be.equal(true); + }); + }; + + before(() => { + cy.resetUsers(); + cy.getToken().then((tok) => { + token = tok; + }); + }); + + it('Should be able to create an auth access list and a client access list', () => { + createAccessList({ + name: 'Toggle Auth', + items: [creds], + }).then((id) => { + authListId = id; + }); + + // The test runner is never in this range, so it must always be denied + createAccessList({ + name: 'Toggle Clients', + clients: [ + { + directive: 'allow', + address: '203.0.113.0/24', + } + ], + }).then((id) => { + clientListId = id; + }); + }); + + it('Should be able to create a proxy host without an access list', () => { + cy.task('backendApiPost', { + token: token, + path: '/api/nginx/proxy-hosts', + data: { + domain_names: [domain], + forward_scheme: 'http', + forward_host: 'examplesite', + forward_port: 80, + access_list_id: 0, + certificate_id: 0, + meta: { + dns_challenge: false + }, + advanced_config: '', + locations: [], + block_exploits: false, + caching_enabled: false, + allow_websocket_upgrade: false, + http2_support: false, + hsts_enabled: false, + hsts_subdomains: false, + ssl_forced: false + } + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/nginx/proxy-hosts', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.greaterThan(0); + hostId = data.id; + }); + + expectStatus(null, '200'); + }); + + it('Should enforce an auth access list applied to an existing host', () => { + setHostAccessList(authListId); + expectStatus(null, '401'); + expectStatus(creds, '200'); + }); + + it('Should still enforce the auth access list after the host is disabled and enabled', () => { + toggleHost(); + expectStatus(null, '401'); + expectStatus(creds, '200'); + }); + + it('Should enforce a client access list applied to an existing host', () => { + setHostAccessList(clientListId); + expectStatus(null, '403'); + }); + + it('Should still enforce the client access list after the host is disabled and enabled', () => { + toggleHost(); + expectStatus(null, '403'); + }); + + it('Should be able to delete the proxy host and access lists', () => { + cy.task('backendApiDelete', { + token: token, + path: `/api/nginx/proxy-hosts/${hostId}`, + }).then((data) => { + cy.validateSwaggerSchema('delete', 200, '/nginx/proxy-hosts/{hostID}', data); + expect(data).to.be.equal(true); + }); + + [authListId, clientListId].forEach((id) => { + cy.task('backendApiDelete', { + token: token, + path: `/api/nginx/access-lists/${id}`, + }).then((data) => { + cy.validateSwaggerSchema('delete', 200, '/nginx/access-lists/{listID}', data); + expect(data).to.be.equal(true); + }); + }); + }); + +}); diff --git a/test/docker/nginx/conf.d/website123.conf b/test/docker/nginx/conf.d/website123.conf index 656f3ade42..eefb233bcd 100644 --- a/test/docker/nginx/conf.d/website123.conf +++ b/test/docker/nginx/conf.d/website123.conf @@ -1,6 +1,6 @@ server { listen 80; - server_name website1.example.com website2.example.com website3.example.com; + server_name website1.example.com website2.example.com website3.example.com website4.example.com; root /www; index index.html; From 81e7044a174d7bcccbdc9134255c720d61a1a3ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:34:59 +0000 Subject: [PATCH 08/62] build(deps): bump proxy-addr from 2.0.7 to 2.0.8 in /backend Bumps [proxy-addr](https://github.com/jshttp/proxy-addr) from 2.0.7 to 2.0.8. - [Release notes](https://github.com/jshttp/proxy-addr/releases) - [Changelog](https://github.com/jshttp/proxy-addr/blob/master/HISTORY.md) - [Commits](https://github.com/jshttp/proxy-addr/compare/v2.0.7...v2.0.8) --- updated-dependencies: - dependency-name: proxy-addr dependency-version: 2.0.8 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- backend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/yarn.lock b/backend/yarn.lock index 8f4238ad58..a9d6c2b513 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1873,9 +1873,9 @@ process@^0.11.1, process@^0.11.10: integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== proxy-addr@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + version "2.0.8" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.8.tgz#624e760fd8ab06b1b8320c2f8506d4422cf8e932" + integrity sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ== dependencies: forwarded "0.2.0" ipaddr.js "1.9.1" From 3270873cc83b3b91d29f68e313339e7f3ead0db6 Mon Sep 17 00:00:00 2001 From: Juice-de-Orange <200407033+Juice-de-Orange@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:59:42 +0200 Subject: [PATCH 09/62] Add missing German (de) and Italian (it) translations --- frontend/src/locale/src/de.json | 66 ++++++++++++ frontend/src/locale/src/it.json | 183 ++++++++++++++++++++++++++++++++ 2 files changed, 249 insertions(+) diff --git a/frontend/src/locale/src/de.json b/frontend/src/locale/src/de.json index 6cdb64cf39..e5ce5355a3 100644 --- a/frontend/src/locale/src/de.json +++ b/frontend/src/locale/src/de.json @@ -71,6 +71,12 @@ "access-list.help.rules-order": { "defaultMessage": "Beachten Sie, dass die Anweisungen „Erlauben“ und „Verbieten“ in der Reihenfolge ihrer Definition angewendet werden." }, + "access-list.inherit": { + "defaultMessage": "Vom Proxy Host übernehmen" + }, + "access-list.inherit.subtitle": { + "defaultMessage": "Verwendet die Zugriffsliste des Proxy Hosts" + }, "access-list.pass-auth": { "defaultMessage": "Authentifizierung an Upstream weiterleiten" }, @@ -101,6 +107,9 @@ "action.allow": { "defaultMessage": "Erlauben" }, + "action.clear": { + "defaultMessage": "Leeren" + }, "action.close": { "defaultMessage": "Schließen" }, @@ -122,6 +131,9 @@ "action.enable": { "defaultMessage": "Aktivieren" }, + "action.logs": { + "defaultMessage": "Logs" + }, "action.permissions": { "defaultMessage": "Berechtigungen" }, @@ -266,6 +278,9 @@ "column.email": { "defaultMessage": "E-Mail" }, + "column.error": { + "defaultMessage": "Fehler" + }, "column.event": { "defaultMessage": "Ereignis" }, @@ -461,6 +476,12 @@ "loading": { "defaultMessage": "Laden…" }, + "location.advanced-config": { + "defaultMessage": "Hat eine eigene Nginx-Konfiguration" + }, + "location.filter": { + "defaultMessage": "Nach Pfad oder Ziel filtern" + }, "login.2fa-code": { "defaultMessage": "Verifizierungscode" }, @@ -479,6 +500,51 @@ "login.title": { "defaultMessage": "Anmelden" }, + "logs": { + "defaultMessage": "Logs" + }, + "logs.channel.access": { + "defaultMessage": "Zugriff" + }, + "logs.channel.error": { + "defaultMessage": "Fehler" + }, + "logs.download": { + "defaultMessage": "Herunterladen" + }, + "logs.empty": { + "defaultMessage": "Noch keine Log-Einträge." + }, + "logs.level": { + "defaultMessage": "Level" + }, + "logs.level.all": { + "defaultMessage": "Alle Level" + }, + "logs.lines": { + "defaultMessage": "Zeilen" + }, + "logs.live": { + "defaultMessage": "Live" + }, + "logs.paused": { + "defaultMessage": "Pausiert" + }, + "logs.refresh": { + "defaultMessage": "Aktualisieren" + }, + "logs.search-placeholder": { + "defaultMessage": "Log durchsuchen…" + }, + "logs.source": { + "defaultMessage": "Quelle" + }, + "logs.source.system": { + "defaultMessage": "System" + }, + "logs.truncated-warning": { + "defaultMessage": "Es wird nur der neueste Teil dieser Datei angezeigt." + }, "nginx-config.label": { "defaultMessage": "Benutzerdefinierte Nginx Konfiguration" }, diff --git a/frontend/src/locale/src/it.json b/frontend/src/locale/src/it.json index 7e5ca77113..d8506bd953 100644 --- a/frontend/src/locale/src/it.json +++ b/frontend/src/locale/src/it.json @@ -1,4 +1,61 @@ { + "2fa.backup-codes-remaining": { + "defaultMessage": "Codici di backup rimanenti: {count}" + }, + "2fa.backup-warning": { + "defaultMessage": "Conserva questi codici di backup in un luogo sicuro. Ogni codice può essere usato una sola volta." + }, + "2fa.disable": { + "defaultMessage": "Disattiva Autenticazione a Due Fattori" + }, + "2fa.disable-confirm": { + "defaultMessage": "Disattiva 2FA" + }, + "2fa.disable-warning": { + "defaultMessage": "Disattivare l'autenticazione a due fattori renderà il tuo account meno sicuro." + }, + "2fa.disabled": { + "defaultMessage": "Disattivata" + }, + "2fa.done": { + "defaultMessage": "Ho salvato i miei codici di backup" + }, + "2fa.enable": { + "defaultMessage": "Attiva Autenticazione a Due Fattori" + }, + "2fa.enabled": { + "defaultMessage": "Attivata" + }, + "2fa.enter-code": { + "defaultMessage": "Inserisci il codice di verifica" + }, + "2fa.enter-code-disable": { + "defaultMessage": "Inserisci il codice di verifica per disattivarla" + }, + "2fa.regenerate": { + "defaultMessage": "Rigenera" + }, + "2fa.regenerate-backup": { + "defaultMessage": "Rigenera Codici di Backup" + }, + "2fa.regenerate-instructions": { + "defaultMessage": "Inserisci un codice di verifica per generare nuovi codici di backup. I vecchi codici non saranno più validi." + }, + "2fa.secret-key": { + "defaultMessage": "Chiave Segreta" + }, + "2fa.setup-instructions": { + "defaultMessage": "Scansiona questo codice QR con la tua app di autenticazione, oppure inserisci la chiave segreta manualmente." + }, + "2fa.status": { + "defaultMessage": "Stato" + }, + "2fa.title": { + "defaultMessage": "Autenticazione a Due Fattori" + }, + "2fa.verify-enable": { + "defaultMessage": "Verifica e Attiva" + }, "access-list": { "defaultMessage": "Lista di Accesso" }, @@ -14,6 +71,12 @@ "access-list.help.rules-order": { "defaultMessage": "Nota che le direttive di allow e deny saranno applicate nell'ordine in cui sono definite." }, + "access-list.inherit": { + "defaultMessage": "Eredita dal Proxy Host" + }, + "access-list.inherit.subtitle": { + "defaultMessage": "Usa la lista di accesso del proxy host" + }, "access-list.pass-auth": { "defaultMessage": "Passa Autenticazione all'Upstream" }, @@ -23,6 +86,9 @@ "access-list.public.subtitle": { "defaultMessage": "Nessuna autenticazione base richiesta" }, + "access-list.rule-source.placeholder": { + "defaultMessage": "192.168.1.100 o 192.168.1.0/24 o 2001:0db8::/32" + }, "access-list.satisfy-any": { "defaultMessage": "Soddisfa Qualsiasi" }, @@ -38,12 +104,21 @@ "action.add-location": { "defaultMessage": "Aggiungi Percorso" }, + "action.allow": { + "defaultMessage": "Consenti" + }, + "action.clear": { + "defaultMessage": "Svuota" + }, "action.close": { "defaultMessage": "Chiudi" }, "action.delete": { "defaultMessage": "Elimina" }, + "action.deny": { + "defaultMessage": "Nega" + }, "action.disable": { "defaultMessage": "Disabilita" }, @@ -56,6 +131,9 @@ "action.enable": { "defaultMessage": "Abilita" }, + "action.logs": { + "defaultMessage": "Log" + }, "action.permissions": { "defaultMessage": "Permessi" }, @@ -68,6 +146,9 @@ "auditlogs": { "defaultMessage": "Log di Audit" }, + "auto": { + "defaultMessage": "Auto" + }, "cancel": { "defaultMessage": "Annulla" }, @@ -128,6 +209,9 @@ "certificates.dns.provider": { "defaultMessage": "Provider DNS" }, + "certificates.dns.provider.placeholder": { + "defaultMessage": "Seleziona un Provider..." + }, "certificates.dns.warning": { "defaultMessage": "Questa sezione richiede conoscenze su Certbot e i relativi plugin DNS. Consulta la documentazione del plugin." }, @@ -194,6 +278,9 @@ "column.email": { "defaultMessage": "Email" }, + "column.error": { + "defaultMessage": "Errore" + }, "column.event": { "defaultMessage": "Evento" }, @@ -275,6 +362,9 @@ "domain-names.wildcards-not-supported": { "defaultMessage": "Wildcard non supportate per questa CA" }, + "domains.advanced": { + "defaultMessage": "Avanzate" + }, "domains.force-ssl": { "defaultMessage": "Forza SSL" }, @@ -287,6 +377,9 @@ "domains.http2-support": { "defaultMessage": "Supporto HTTP/2" }, + "domains.trust-forwarded-proto": { + "defaultMessage": "Considera Attendibili gli Header Forwarded Proto dell'Upstream" + }, "domains.use-dns": { "defaultMessage": "Usa Challenge DNS" }, @@ -383,9 +476,75 @@ "loading": { "defaultMessage": "Caricamento…" }, + "location.advanced-config": { + "defaultMessage": "Ha una configurazione Nginx personalizzata" + }, + "location.filter": { + "defaultMessage": "Filtra per percorso o destinazione" + }, + "login.2fa-code": { + "defaultMessage": "Codice di Verifica" + }, + "login.2fa-code-placeholder": { + "defaultMessage": "Inserisci il codice" + }, + "login.2fa-description": { + "defaultMessage": "Inserisci il codice della tua app di autenticazione" + }, + "login.2fa-title": { + "defaultMessage": "Autenticazione a Due Fattori" + }, + "login.2fa-verify": { + "defaultMessage": "Verifica" + }, "login.title": { "defaultMessage": "Accedi al tuo account" }, + "logs": { + "defaultMessage": "Log" + }, + "logs.channel.access": { + "defaultMessage": "Accessi" + }, + "logs.channel.error": { + "defaultMessage": "Errori" + }, + "logs.download": { + "defaultMessage": "Scarica" + }, + "logs.empty": { + "defaultMessage": "Nessuna voce di log per ora." + }, + "logs.level": { + "defaultMessage": "Livello" + }, + "logs.level.all": { + "defaultMessage": "Tutti i Livelli" + }, + "logs.lines": { + "defaultMessage": "Righe" + }, + "logs.live": { + "defaultMessage": "Live" + }, + "logs.paused": { + "defaultMessage": "In Pausa" + }, + "logs.refresh": { + "defaultMessage": "Aggiorna" + }, + "logs.search-placeholder": { + "defaultMessage": "Cerca nel log…" + }, + "logs.source": { + "defaultMessage": "Origine" + }, + "logs.source.system": { + "defaultMessage": "Sistema" + }, + "logs.truncated-warning": { + "defaultMessage": "Viene mostrata solo la parte più recente di questo file." + }, "nginx-config.label": { "defaultMessage": "Configurazione Nginx Personalizzata" }, @@ -530,6 +689,24 @@ "redirection-hosts.count": { "defaultMessage": "{count} {count, plural, one {Host di Reindirizzamento} other {Host di Reindirizzamento}}" }, + "redirection-hosts.http-code.300": { + "defaultMessage": "300 Scelte multiple" + }, + "redirection-hosts.http-code.301": { + "defaultMessage": "301 Spostato permanentemente" + }, + "redirection-hosts.http-code.302": { + "defaultMessage": "302 Spostato temporaneamente" + }, + "redirection-hosts.http-code.303": { + "defaultMessage": "303 Vedi altro" + }, + "redirection-hosts.http-code.307": { + "defaultMessage": "307 Reindirizzamento temporaneo" + }, + "redirection-hosts.http-code.308": { + "defaultMessage": "308 Reindirizzamento permanente" + }, "role.admin": { "defaultMessage": "Amministratore" }, @@ -587,6 +764,9 @@ "stream.forward-host": { "defaultMessage": "Host di Destinazione" }, + "stream.forward-host.placeholder": { + "defaultMessage": "example.com o 10.0.0.1 o 2001:db8:3333:4444:5555:6666:7777:8888" + }, "stream.incoming-port": { "defaultMessage": "Porta in Ingresso" }, @@ -650,6 +830,9 @@ "user.switch-light": { "defaultMessage": "Passa alla modalità Chiara" }, + "user.two-factor": { + "defaultMessage": "Autenticazione 2FA" + }, "username": { "defaultMessage": "Nome Utente" }, From eac1e973dc6b9aecd9c18115616ce1bd402605f5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:29:47 +0000 Subject: [PATCH 10/62] Update docker/dockerfile Docker tag to v1.28.0 --- Dockerfile | 2 +- caddy/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4fc6161e98..7225f45375 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.27.0@sha256:bde3983e9c939224420ddaf6b784cc30e09b035a4dea01f581230c50809f372e +# syntax=docker/dockerfile:1.28.0@sha256:bb22d9815c728170f72750f4e5b0d672e06176142e1d602c7e66c050100b7e5b FROM alpine:3.24.2@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 AS nginx SHELL ["/bin/ash", "-eo", "pipefail", "-c"] diff --git a/caddy/Dockerfile b/caddy/Dockerfile index ee791d828a..8a338b4eb1 100644 --- a/caddy/Dockerfile +++ b/caddy/Dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.27.0@sha256:bde3983e9c939224420ddaf6b784cc30e09b035a4dea01f581230c50809f372e +# syntax=docker/dockerfile:1.28.0@sha256:bb22d9815c728170f72750f4e5b0d672e06176142e1d602c7e66c050100b7e5b FROM alpine:3.24.2@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 RUN apk add --no-cache tzdata COPY --from=caddy:2.11.4@sha256:0c994536bddb66445885237f1a5dcc1916bccea922661c76b4e9fc24061f9b52 /usr/bin/caddy /usr/bin/caddy From d526c21fe7e8cb650e8c1954e243ba465c53814c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 01:16:26 +0000 Subject: [PATCH 11/62] Update caddy Docker tag to v2.11.7 --- caddy/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/caddy/Dockerfile b/caddy/Dockerfile index 8a338b4eb1..dea72a6ed7 100644 --- a/caddy/Dockerfile +++ b/caddy/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1.28.0@sha256:bb22d9815c728170f72750f4e5b0d672e06176142e1d602c7e66c050100b7e5b FROM alpine:3.24.2@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 RUN apk add --no-cache tzdata -COPY --from=caddy:2.11.4@sha256:0c994536bddb66445885237f1a5dcc1916bccea922661c76b4e9fc24061f9b52 /usr/bin/caddy /usr/bin/caddy +COPY --from=caddy:2.11.7@sha256:f2a1290d0463aad60660d4ec134943f183ee2a5f6c3eb7bf32dd984f2f020772 /usr/bin/caddy /usr/bin/caddy COPY Caddyfile /etc/caddy/Caddyfile CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] From e5b5c613274ef43e2fcaf6ea7b917013184499bc Mon Sep 17 00:00:00 2001 From: Zoey2936 <75573284+Zoey2936@users.noreply.github.com> Date: Sun, 27 Sep 2026 15:35:36 +0000 Subject: [PATCH 12/62] dep updates --- .github/workflows/lint-and-format.yml | 2 +- Dockerfile | 2 +- backend/package.json | 10 +- backend/pnpm-lock.yaml | 200 +++++++------- frontend/package.json | 16 +- frontend/pnpm-lock.yaml | 374 +++++++++++++------------- 6 files changed, 302 insertions(+), 302 deletions(-) diff --git a/.github/workflows/lint-and-format.yml b/.github/workflows/lint-and-format.yml index 406d4688af..aa9efc44bc 100644 --- a/.github/workflows/lint-and-format.yml +++ b/.github/workflows/lint-and-format.yml @@ -21,7 +21,7 @@ jobs: BIOME: ${{ github.event_name == 'pull_request' && 'ci' || 'check --write' }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: lts/* - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 diff --git a/Dockerfile b/Dockerfile index 7225f45375..c5000df131 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,7 +64,7 @@ RUN git-clone-commit.sh https://github.com/nginx/nginx "$NGINX_VER" /src/nginx & echo "73fdee62748f1624f87015a951a2480fd0d4fe566a81d92b852b51536d954b91 /src/nginx/1.patch" | sha256sum -c - && \ git apply /src/nginx/1.patch && \ wget -q https://patch-diff.githubusercontent.com/raw/nginx/nginx/pull/1756.patch -O /src/nginx/2.patch && \ - echo "5e9428aa81586c093440155c54669df444a0c61f8b0ab7056f20a7ebe21594fa /src/nginx/2.patch" | sha256sum -c - && \ + echo "1f720c05c166e15f41d78fbd598561745591847ba1b892bae42a2f47758b1fc6 /src/nginx/2.patch" | sha256sum -c - && \ git apply /src/nginx/2.patch && \ wget -q https://patch-diff.githubusercontent.com/raw/nginx/nginx/pull/1333.patch -O /src/nginx/3.patch && \ echo "01bf75b130b8f91075ec913a400a8debfab6da0ac609711c7d412ddbe59dd898 /src/nginx/3.patch" | sha256sum -c - && \ diff --git a/backend/package.json b/backend/package.json index dd28cc9371..04dda229e7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,21 +15,21 @@ "cookie-parser": "1.4.7", "dayjs": "1.11.23", "express": "5.2.1", - "express-rate-limit": "8.7.0", + "express-rate-limit": "8.7.1", "jsonwebtoken": "9.0.3", "knex": "3.3.0", - "liquidjs": "10.29.0", + "liquidjs": "10.30.0", "multer": "2.4.0", - "mysql2": "3.24.4", + "mysql2": "3.24.5", "objection": "3.1.5", "openid-client": "6.8.8", "otplib": "13.5.0", - "pg": "8.23.0", + "pg": "8.23.1", "swagger-ui-express": "5.0.1", "undici": "8.11.2" }, "devDependencies": { "@apidevtools/swagger-parser": "13.1.0", - "@biomejs/biome": "2.5.14" + "@biomejs/biome": "2.5.15" } } diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 01a03dd3e4..ad10b56342 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -33,26 +33,26 @@ importers: specifier: 5.2.1 version: 5.2.1 express-rate-limit: - specifier: 8.7.0 - version: 8.7.0(express@5.2.1) + specifier: 8.7.1 + version: 8.7.1(express@5.2.1) jsonwebtoken: specifier: 9.0.3 version: 9.0.3 knex: specifier: 3.3.0 - version: 3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.4(@types/node@26.6.2))(pg@8.23.0) + version: 3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.5(@types/node@26.6.4))(pg@8.23.1) liquidjs: - specifier: 10.29.0 - version: 10.29.0 + specifier: 10.30.0 + version: 10.30.0 multer: specifier: 2.4.0 version: 2.4.0 mysql2: - specifier: 3.24.4 - version: 3.24.4(@types/node@26.6.2) + specifier: 3.24.5 + version: 3.24.5(@types/node@26.6.4) objection: specifier: 3.1.5 - version: 3.1.5(knex@3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.4(@types/node@26.6.2))(pg@8.23.0)) + version: 3.1.5(knex@3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.5(@types/node@26.6.4))(pg@8.23.1)) openid-client: specifier: 6.8.8 version: 6.8.8 @@ -60,8 +60,8 @@ importers: specifier: 13.5.0 version: 13.5.0 pg: - specifier: 8.23.0 - version: 8.23.0 + specifier: 8.23.1 + version: 8.23.1 swagger-ui-express: specifier: 5.0.1 version: 5.0.1(express@5.2.1) @@ -73,8 +73,8 @@ importers: specifier: 13.1.0 version: 13.1.0(openapi-types@12.1.3) '@biomejs/biome': - specifier: 2.5.14 - version: 2.5.14 + specifier: 2.5.15 + version: 2.5.15 packages: @@ -97,59 +97,59 @@ packages: peerDependencies: openapi-types: '>=7' - '@biomejs/biome@2.5.14': - resolution: {integrity: sha512-0FabLIjd4M/dm8VFI86RMaLLdgepzbdfiAL2R8cr7V81OYYrP1w7Z73KfAwPK5h9SrEBXTzNG2y+mBwPo9xRnw==} + '@biomejs/biome@2.5.15': + resolution: {integrity: sha512-WZTW4slm/pdkh92K6t/3aEN++44JD1PQ7squ7RCMLI1flHGl43DVOIGXvCjHiZgBMO1V4uYxoFNtqrIA4qGuIQ==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.5.14': - resolution: {integrity: sha512-UnzaXO65L4tsZimFITFP2M121GyhDcWFrT3pL5ZJ5U4XcS/0L5VHYytVohdCf/gnDUFHgl2I9xnt5bV/J1kxHQ==} + '@biomejs/cli-darwin-arm64@2.5.15': + resolution: {integrity: sha512-BZVzFhJ/mUvTLMYbc9x6el0o2Uv5zP7bACYyhFb6fSc1giroXm7PL0a5KqMJ9F+/BnkYRjagD/ROSDVOZ6eapg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.5.14': - resolution: {integrity: sha512-kiy8qA16K93J7uvFfWi4LgjqDpnRKyePAna6A0Y4jxyUga35SYpIZ2cNWlhy0lsfgqRenCpfymnJWEZ6mgpRgA==} + '@biomejs/cli-darwin-x64@2.5.15': + resolution: {integrity: sha512-V5Kw63V+fVGNFhFrizDxMbGXAzZCh8kYzJpy3GI6gTapWDg7bZK9oYC9CbVgQSEdTnupz0U5Efz8Ev+5vdBBXw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.5.14': - resolution: {integrity: sha512-SJ9PrZkBnnH9dHJDnxk34vKs0GB2dbsioaft6/hPhWJ7AHpwYH83Isnhj0FSRpFkGgpVfJ1lds23ApB2czUDLQ==} + '@biomejs/cli-linux-arm64-musl@2.5.15': + resolution: {integrity: sha512-tGzZUTcJCV7tj5Adh/Gn6nR4MycqA3iohq2LEcrarAgRBkU0h0OKd2UzCRkOsOYOuUGaUJb4YmqRqyy2feGGSw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@2.5.14': - resolution: {integrity: sha512-vO/9BaU1n30CiFNLx49gMTMtbCAAqlo/EqFoG0BU3deQInTbJrmXSmTQ9e3FoDZIKQnvSLDVNL4lx1ci7y1T1Q==} + '@biomejs/cli-linux-arm64@2.5.15': + resolution: {integrity: sha512-XaG7P7eeSLYETD3K9grfB0mQmpLQZjygdbsxekWBJAg4am79fGAtRpfRyNnTvXB2fZFNnJZG3nXm7HBat0+VUw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.5.14': - resolution: {integrity: sha512-2kI5PrMgW5dcEZYrstLPmUmCwkUwZY39rP4BN93Vxbwcs2O57LDQOktUZZYPvvspN5i0mhGr3TJtF5sU26NUWg==} + '@biomejs/cli-linux-x64-musl@2.5.15': + resolution: {integrity: sha512-IlxUcyxilVGPsE008x13pWdkTbU3nQpP2i9b5UrbOYj6goBkKsRX1XB7yJLcV+O1H9LBchIIm4adOaVLZBX0ZQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@2.5.14': - resolution: {integrity: sha512-VHZRa7CCQBUWKxNwUvHJeuW03WFRgN5NWO//SDGOitc9QIeNyfA42V9zlKm+x82xFSG9Bzi5fi+hbhm9anO8yA==} + '@biomejs/cli-linux-x64@2.5.15': + resolution: {integrity: sha512-xE4iEW/3LqlYj9GFgGrFsFSH51dEEpUbYWBfeOv0q87WkUDxK2o/HhreSb7qMJqck70RVM6Lg96hgedcAYcOkA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@2.5.14': - resolution: {integrity: sha512-pHgAFffmZtaYoxavEsWEYNvcA7NwOIjLyqw2HXyLbt16xYryX0L4fMV2u0G9ag6LNYPIoqWaWqzUcnc6rLGGXg==} + '@biomejs/cli-win32-arm64@2.5.15': + resolution: {integrity: sha512-2kPKzhNlm8C+Ru3GcO0Me2ODkCBLrVOUNuyi84RJyWDVKAA4+Kjj3jMjL938lF6BRfvltC4vB2nPY9SvN51Ovg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.5.14': - resolution: {integrity: sha512-oJWmBhoHsnhUKIke+0gXDX0mltJrWHA1UyHsrTlXwX0TL64ilVZAo+TYZm97baecV0esBdpzy3k96q+09JaZUQ==} + '@biomejs/cli-win32-x64@2.5.15': + resolution: {integrity: sha512-yAzh4UqEImV6Hcy0zjUqNfJAsUhoD64FBQCdTG8Md/Z2wQeVP/ZmJ3XRYAs1g8J9fjTFyl2fCtc5FB5til47+g==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -185,8 +185,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@26.6.2': - resolution: {integrity: sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==} + '@types/node@26.6.4': + resolution: {integrity: sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==} abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} @@ -252,8 +252,8 @@ packages: bare-abort-controller: optional: true - bare-fs@4.8.1: - resolution: {integrity: sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==} + bare-fs@4.8.2: + resolution: {integrity: sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==} engines: {bare: '>=1.28.0'} peerDependencies: bare-buffer: '*' @@ -459,8 +459,8 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - express-rate-limit@8.7.0: - resolution: {integrity: sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==} + express-rate-limit@8.7.1: + resolution: {integrity: sha512-2FXbqkygil3mZiXzEtM0IjE/UBmBNot0J1GEVrJT4JbrvwOSoEtqtO+wHbiMQELWe1eNvHoYRg2dkjOid+6JkA==} engines: {node: '>= 16'} peerDependencies: express: '>= 4.11' @@ -541,8 +541,8 @@ packages: resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} engines: {node: '>= 0.10'} - ip-address@10.7.2: - resolution: {integrity: sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==} + ip-address@10.7.3: + resolution: {integrity: sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -624,8 +624,8 @@ packages: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} - liquidjs@10.29.0: - resolution: {integrity: sha512-pCVOhs6FLAR8su3ItJ07diN26t6W5dHQRnmTMy8HPyTFuv1+oSCVJIGp5pGjfQyOZfh50KswvKtMTp6p4JEIdw==} + liquidjs@10.30.0: + resolution: {integrity: sha512-Fw4wA+8CZsJkaeLEIlPE3AoponfjEZhj7FQvYNcAEcg7jNRik7mEMoaco382FBdrYlykVf6w4vClPNctz2OYjg==} engines: {node: '>=16'} hasBin: true @@ -706,8 +706,8 @@ packages: resolution: {integrity: sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==} engines: {node: '>= 10.16.0'} - mysql2@3.24.4: - resolution: {integrity: sha512-A2olluVlj0mvgyIRRISMEzXc51m+21mRtcMVjJyIpt2GG98+XrC9m9HzsqcMsX2LcnfccJvY5NB22g8fENBnOA==} + mysql2@3.24.5: + resolution: {integrity: sha512-X6Ujsr2QSkkLpkQGjxzpKRAPn9nu4axpR63ntBzquFVEvPOArgbUQ1sJjFKI7hnaYtiVZxa17Z7q18KSubW0IQ==} engines: {node: '>= 8.0'} peerDependencies: '@types/node': '>= 8' @@ -767,11 +767,11 @@ packages: path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} - pg-cloudflare@1.4.0: - resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + pg-cloudflare@1.4.1: + resolution: {integrity: sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==} - pg-connection-string@2.14.0: - resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + pg-connection-string@2.14.1: + resolution: {integrity: sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==} pg-connection-string@2.6.2: resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==} @@ -785,15 +785,15 @@ packages: peerDependencies: pg: '>=8.0' - pg-protocol@1.16.0: - resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==} + pg-protocol@1.16.1: + resolution: {integrity: sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==} pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} - pg@8.23.0: - resolution: {integrity: sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==} + pg@8.23.1: + resolution: {integrity: sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==} engines: {node: '>= 16.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -945,8 +945,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - swagger-ui-dist@5.33.0: - resolution: {integrity: sha512-wpdK+m6BU5yj6pmUdMskZVTSWYG4DLglAx3sIhylloY37i8O37IrH+YEpqdXNfpaTGxILRBFzUqLF2jKqbfI7A==} + swagger-ui-dist@5.33.1: + resolution: {integrity: sha512-H872wWkA53bFIsGgi7OWgmq+CRWw3nFQGdJWRqOB9wNwTm6e5ol34+qPDkV4AJlK+gglM2EsJiOOzsGsEGbluA==} swagger-ui-express@5.0.1: resolution: {integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==} @@ -954,8 +954,8 @@ packages: peerDependencies: express: '>=4.0.0 || >=5.0.0-beta' - tar-stream@3.2.1: - resolution: {integrity: sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==} + tar-stream@3.2.2: + resolution: {integrity: sha512-+8NeqHRjQWH9nYlwo2gamAMImZCVzI4UoEgDpWorBt9OEfppiZn+uSkskzQKPWIZyji/C8fpWO7u69G0DX0tbg==} tarn@3.1.2: resolution: {integrity: sha512-3RTvqKZcK/17jnJ8rMKFXbyNogywTs1z0gVPPwFsJGX46rkmUHOdIaSQ/aVO1rS7nH+soiXiWk7rvUXxndm8Dg==} @@ -1035,39 +1035,39 @@ snapshots: call-me-maybe: 1.0.2 openapi-types: 12.1.3 - '@biomejs/biome@2.5.14': + '@biomejs/biome@2.5.15': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.5.14 - '@biomejs/cli-darwin-x64': 2.5.14 - '@biomejs/cli-linux-arm64': 2.5.14 - '@biomejs/cli-linux-arm64-musl': 2.5.14 - '@biomejs/cli-linux-x64': 2.5.14 - '@biomejs/cli-linux-x64-musl': 2.5.14 - '@biomejs/cli-win32-arm64': 2.5.14 - '@biomejs/cli-win32-x64': 2.5.14 - - '@biomejs/cli-darwin-arm64@2.5.14': + '@biomejs/cli-darwin-arm64': 2.5.15 + '@biomejs/cli-darwin-x64': 2.5.15 + '@biomejs/cli-linux-arm64': 2.5.15 + '@biomejs/cli-linux-arm64-musl': 2.5.15 + '@biomejs/cli-linux-x64': 2.5.15 + '@biomejs/cli-linux-x64-musl': 2.5.15 + '@biomejs/cli-win32-arm64': 2.5.15 + '@biomejs/cli-win32-x64': 2.5.15 + + '@biomejs/cli-darwin-arm64@2.5.15': optional: true - '@biomejs/cli-darwin-x64@2.5.14': + '@biomejs/cli-darwin-x64@2.5.15': optional: true - '@biomejs/cli-linux-arm64-musl@2.5.14': + '@biomejs/cli-linux-arm64-musl@2.5.15': optional: true - '@biomejs/cli-linux-arm64@2.5.14': + '@biomejs/cli-linux-arm64@2.5.15': optional: true - '@biomejs/cli-linux-x64-musl@2.5.14': + '@biomejs/cli-linux-x64-musl@2.5.15': optional: true - '@biomejs/cli-linux-x64@2.5.14': + '@biomejs/cli-linux-x64@2.5.15': optional: true - '@biomejs/cli-win32-arm64@2.5.14': + '@biomejs/cli-win32-arm64@2.5.15': optional: true - '@biomejs/cli-win32-x64@2.5.14': + '@biomejs/cli-win32-x64@2.5.15': optional: true '@noble/hashes@2.4.0': {} @@ -1105,7 +1105,7 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@26.6.2': + '@types/node@26.6.4': dependencies: undici-types: 8.9.0 @@ -1144,7 +1144,7 @@ snapshots: normalize-path: 3.0.0 readable-stream: 4.7.0 readdir-glob: 3.0.0 - tar-stream: 3.2.1 + tar-stream: 3.2.2 zip-stream: 7.0.5 transitivePeerDependencies: - bare-abort-controller @@ -1163,7 +1163,7 @@ snapshots: bare-events@2.9.2: {} - bare-fs@4.8.1: + bare-fs@4.8.2: dependencies: bare-events: 2.9.2 bare-path: 3.1.2 @@ -1335,11 +1335,11 @@ snapshots: events@3.3.0: {} - express-rate-limit@8.7.0(express@5.2.1): + express-rate-limit@8.7.1(express@5.2.1): dependencies: debug: 4.4.3 express: 5.2.1 - ip-address: 10.7.2 + ip-address: 10.7.3 transitivePeerDependencies: - supports-color @@ -1451,7 +1451,7 @@ snapshots: interpret@2.2.0: {} - ip-address@10.7.2: {} + ip-address@10.7.3: {} ipaddr.js@1.9.1: {} @@ -1499,7 +1499,7 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 - knex@3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.4(@types/node@26.6.2))(pg@8.23.0): + knex@3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.5(@types/node@26.6.4))(pg@8.23.1): dependencies: colorette: 2.0.19 commander: 10.0.1 @@ -1517,8 +1517,8 @@ snapshots: tildify: 2.0.0 optionalDependencies: better-sqlite3: 13.0.3 - mysql2: 3.24.4(@types/node@26.6.2) - pg: 8.23.0 + mysql2: 3.24.5(@types/node@26.6.4) + pg: 8.23.1 transitivePeerDependencies: - supports-color @@ -1526,7 +1526,7 @@ snapshots: dependencies: readable-stream: 2.3.8 - liquidjs@10.29.0: + liquidjs@10.30.0: dependencies: commander: 10.0.1 @@ -1584,9 +1584,9 @@ snapshots: busboy: 1.6.0 type-is: 1.6.18 - mysql2@3.24.4(@types/node@26.6.2): + mysql2@3.24.5(@types/node@26.6.4): dependencies: - '@types/node': 26.6.2 + '@types/node': 26.6.4 aws-ssl-profiles: 1.1.2 generate-function: 2.3.1 iconv-lite: 0.7.3 @@ -1611,12 +1611,12 @@ snapshots: object-inspect@1.13.4: {} - objection@3.1.5(knex@3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.4(@types/node@26.6.2))(pg@8.23.0)): + objection@3.1.5(knex@3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.5(@types/node@26.6.4))(pg@8.23.1)): dependencies: ajv: 8.20.0 ajv-formats: 2.1.1(ajv@8.20.0) db-errors: 0.2.3 - knex: 3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.4(@types/node@26.6.2))(pg@8.23.0) + knex: 3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.5(@types/node@26.6.4))(pg@8.23.1) on-finished@2.4.1: dependencies: @@ -1648,20 +1648,20 @@ snapshots: path-to-regexp@8.4.2: {} - pg-cloudflare@1.4.0: + pg-cloudflare@1.4.1: optional: true - pg-connection-string@2.14.0: {} + pg-connection-string@2.14.1: {} pg-connection-string@2.6.2: {} pg-int8@1.0.1: {} - pg-pool@3.14.0(pg@8.23.0): + pg-pool@3.14.0(pg@8.23.1): dependencies: - pg: 8.23.0 + pg: 8.23.1 - pg-protocol@1.16.0: {} + pg-protocol@1.16.1: {} pg-types@2.2.0: dependencies: @@ -1671,15 +1671,15 @@ snapshots: postgres-date: 1.0.7 postgres-interval: 1.2.0 - pg@8.23.0: + pg@8.23.1: dependencies: - pg-connection-string: 2.14.0 - pg-pool: 3.14.0(pg@8.23.0) - pg-protocol: 1.16.0 + pg-connection-string: 2.14.1 + pg-pool: 3.14.0(pg@8.23.1) + pg-protocol: 1.16.1 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: - pg-cloudflare: 1.4.0 + pg-cloudflare: 1.4.1 pgpass@1.0.5: dependencies: @@ -1855,19 +1855,19 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swagger-ui-dist@5.33.0: + swagger-ui-dist@5.33.1: dependencies: '@scarf/scarf': 1.4.0 swagger-ui-express@5.0.1(express@5.2.1): dependencies: express: 5.2.1 - swagger-ui-dist: 5.33.0 + swagger-ui-dist: 5.33.1 - tar-stream@3.2.1: + tar-stream@3.2.2: dependencies: b4a: 1.9.0 - bare-fs: 4.8.1 + bare-fs: 4.8.2 fast-fifo: 1.3.2 streamx: 2.28.1 transitivePeerDependencies: diff --git a/frontend/package.json b/frontend/package.json index 54a280c935..c5b7a65ec1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,10 +7,10 @@ "type": "module", "dependencies": { "@ebay/nice-modal-react": "1.2.13", - "@tabler/core": "1.6.0", - "@tabler/icons-react": "3.48.0", - "@tanstack/react-query": "5.104.0", - "@tanstack/react-table": "9.2.4", + "@tabler/core": "1.6.1", + "@tabler/icons-react": "3.49.0", + "@tanstack/react-query": "5.104.1", + "@tanstack/react-table": "9.2.6", "clsx": "2.1.1", "country-flag-icons": "1.6.20", "date-fns": "4.4.0", @@ -21,15 +21,15 @@ "react": "19.3.0", "react-bootstrap": "2.10.10", "react-dom": "19.3.0", - "react-intl": "12.1.3", + "react-intl": "12.1.4", "react-router": "8.4.0", "react-select": "5.10.2", "react-toastify": "11.1.0", "rooks": "9.9.0" }, "devDependencies": { - "@biomejs/biome": "2.5.14", - "@vitejs/plugin-react": "6.1.1", - "vite": "8.3.1" + "@biomejs/biome": "2.5.15", + "@vitejs/plugin-react": "6.1.2", + "vite": "8.3.4" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index fb860da37f..ac9d3f5f49 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -12,17 +12,17 @@ importers: specifier: 1.2.13 version: 1.2.13(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@tabler/core': - specifier: 1.6.0 - version: 1.6.0 + specifier: 1.6.1 + version: 1.6.1 '@tabler/icons-react': - specifier: 3.48.0 - version: 3.48.0(react@19.3.0) + specifier: 3.49.0 + version: 3.49.0(react@19.3.0) '@tanstack/react-query': - specifier: 5.104.0 - version: 5.104.0(react@19.3.0) + specifier: 5.104.1 + version: 5.104.1(react@19.3.0) '@tanstack/react-table': - specifier: 9.2.4 - version: 9.2.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + specifier: 9.2.6 + version: 9.2.6(react-dom@19.3.0(react@19.3.0))(react@19.3.0) clsx: specifier: 2.1.1 version: 2.1.1 @@ -54,8 +54,8 @@ importers: specifier: 19.3.0 version: 19.3.0(react@19.3.0) react-intl: - specifier: 12.1.3 - version: 12.1.3(@types/react@19.3.0)(react@19.3.0) + specifier: 12.1.4 + version: 12.1.4(@types/react@19.3.0)(react@19.3.0) react-router: specifier: 8.4.0 version: 8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) @@ -70,14 +70,14 @@ importers: version: 9.9.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) devDependencies: '@biomejs/biome': - specifier: 2.5.14 - version: 2.5.14 + specifier: 2.5.15 + version: 2.5.15 '@vitejs/plugin-react': - specifier: 6.1.1 - version: 6.1.1(vite@8.3.1) + specifier: 6.1.2 + version: 6.1.2(vite@8.3.4) vite: - specifier: 8.3.1 - version: 8.3.1 + specifier: 8.3.4 + version: 8.3.4 packages: @@ -126,59 +126,59 @@ packages: resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.5.14': - resolution: {integrity: sha512-0FabLIjd4M/dm8VFI86RMaLLdgepzbdfiAL2R8cr7V81OYYrP1w7Z73KfAwPK5h9SrEBXTzNG2y+mBwPo9xRnw==} + '@biomejs/biome@2.5.15': + resolution: {integrity: sha512-WZTW4slm/pdkh92K6t/3aEN++44JD1PQ7squ7RCMLI1flHGl43DVOIGXvCjHiZgBMO1V4uYxoFNtqrIA4qGuIQ==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.5.14': - resolution: {integrity: sha512-UnzaXO65L4tsZimFITFP2M121GyhDcWFrT3pL5ZJ5U4XcS/0L5VHYytVohdCf/gnDUFHgl2I9xnt5bV/J1kxHQ==} + '@biomejs/cli-darwin-arm64@2.5.15': + resolution: {integrity: sha512-BZVzFhJ/mUvTLMYbc9x6el0o2Uv5zP7bACYyhFb6fSc1giroXm7PL0a5KqMJ9F+/BnkYRjagD/ROSDVOZ6eapg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.5.14': - resolution: {integrity: sha512-kiy8qA16K93J7uvFfWi4LgjqDpnRKyePAna6A0Y4jxyUga35SYpIZ2cNWlhy0lsfgqRenCpfymnJWEZ6mgpRgA==} + '@biomejs/cli-darwin-x64@2.5.15': + resolution: {integrity: sha512-V5Kw63V+fVGNFhFrizDxMbGXAzZCh8kYzJpy3GI6gTapWDg7bZK9oYC9CbVgQSEdTnupz0U5Efz8Ev+5vdBBXw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.5.14': - resolution: {integrity: sha512-SJ9PrZkBnnH9dHJDnxk34vKs0GB2dbsioaft6/hPhWJ7AHpwYH83Isnhj0FSRpFkGgpVfJ1lds23ApB2czUDLQ==} + '@biomejs/cli-linux-arm64-musl@2.5.15': + resolution: {integrity: sha512-tGzZUTcJCV7tj5Adh/Gn6nR4MycqA3iohq2LEcrarAgRBkU0h0OKd2UzCRkOsOYOuUGaUJb4YmqRqyy2feGGSw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@2.5.14': - resolution: {integrity: sha512-vO/9BaU1n30CiFNLx49gMTMtbCAAqlo/EqFoG0BU3deQInTbJrmXSmTQ9e3FoDZIKQnvSLDVNL4lx1ci7y1T1Q==} + '@biomejs/cli-linux-arm64@2.5.15': + resolution: {integrity: sha512-XaG7P7eeSLYETD3K9grfB0mQmpLQZjygdbsxekWBJAg4am79fGAtRpfRyNnTvXB2fZFNnJZG3nXm7HBat0+VUw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.5.14': - resolution: {integrity: sha512-2kI5PrMgW5dcEZYrstLPmUmCwkUwZY39rP4BN93Vxbwcs2O57LDQOktUZZYPvvspN5i0mhGr3TJtF5sU26NUWg==} + '@biomejs/cli-linux-x64-musl@2.5.15': + resolution: {integrity: sha512-IlxUcyxilVGPsE008x13pWdkTbU3nQpP2i9b5UrbOYj6goBkKsRX1XB7yJLcV+O1H9LBchIIm4adOaVLZBX0ZQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@2.5.14': - resolution: {integrity: sha512-VHZRa7CCQBUWKxNwUvHJeuW03WFRgN5NWO//SDGOitc9QIeNyfA42V9zlKm+x82xFSG9Bzi5fi+hbhm9anO8yA==} + '@biomejs/cli-linux-x64@2.5.15': + resolution: {integrity: sha512-xE4iEW/3LqlYj9GFgGrFsFSH51dEEpUbYWBfeOv0q87WkUDxK2o/HhreSb7qMJqck70RVM6Lg96hgedcAYcOkA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@2.5.14': - resolution: {integrity: sha512-pHgAFffmZtaYoxavEsWEYNvcA7NwOIjLyqw2HXyLbt16xYryX0L4fMV2u0G9ag6LNYPIoqWaWqzUcnc6rLGGXg==} + '@biomejs/cli-win32-arm64@2.5.15': + resolution: {integrity: sha512-2kPKzhNlm8C+Ru3GcO0Me2ODkCBLrVOUNuyi84RJyWDVKAA4+Kjj3jMjL938lF6BRfvltC4vB2nPY9SvN51Ovg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.5.14': - resolution: {integrity: sha512-oJWmBhoHsnhUKIke+0gXDX0mltJrWHA1UyHsrTlXwX0TL64ilVZAo+TYZm97baecV0esBdpzy3k96q+09JaZUQ==} + '@biomejs/cli-win32-x64@2.5.15': + resolution: {integrity: sha512-yAzh4UqEImV6Hcy0zjUqNfJAsUhoD64FBQCdTG8Md/Z2wQeVP/ZmJ3XRYAs1g8J9fjTFyl2fCtc5FB5til47+g==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -242,14 +242,14 @@ packages: '@formatjs/fast-memoize@3.1.7': resolution: {integrity: sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==} - '@formatjs/icu-messageformat-parser@3.5.20': - resolution: {integrity: sha512-DUzCb2iskGDKvLU7P+nz/rjpmN59luoTRbWHFFNYNnZTxWCKvwPI94rNll99TGZaI6XoTbDF0QGegk21GFrQZQ==} + '@formatjs/icu-messageformat-parser@3.5.21': + resolution: {integrity: sha512-Vnl7IOSyMRaP4Pjv48etkT2nlcIwzh4QWIa3hewzGlZZPBbft+TbMir4MijpDViOLN/1hvkGC10oYMuilsG+fw==} '@formatjs/icu-skeleton-parser@2.1.12': resolution: {integrity: sha512-ca7tZMLpfHX3sUD7hP6gE/qr6Iq89EDd3ZLeRhbsB7r0UD55DGJq+tmFLzrGrnx5StahUevQmQQVtbDRHsfDHA==} - '@formatjs/intl@6.1.2': - resolution: {integrity: sha512-YH3Ldfa2vmavWgBlCyrT9WiobphTXlNHnHxhhB6nLSUOxmitJ6Bzqzg61uBcnc6yhKTU+YRo8hLhEatOZrVJUg==} + '@formatjs/intl@6.1.3': + resolution: {integrity: sha512-fBxQVx6wi0f3iibauVFKr/uY+BJR0Z64Zuu6SB+p8hmxjqZoX2u4/RPYaKnFMut2+kNPIeGOaaKDHQ5odnXsZQ==} '@internationalized/date@3.12.4': resolution: {integrity: sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ==} @@ -277,8 +277,8 @@ packages: resolution: {integrity: sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==} engines: {node: '>=12'} - '@oxc-project/types@0.151.0': - resolution: {integrity: sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==} + '@oxc-project/types@0.152.0': + resolution: {integrity: sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==} '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -314,98 +314,98 @@ packages: react: '>=16.14.0' react-dom: '>=16.14.0' - '@rolldown/binding-android-arm-eabi@1.2.10': - resolution: {integrity: sha512-bp9svZb+QurZeh+8H4BhrZkifEB0YBNvTVzNSJnJQkj4NrRwmQoDUCGP0vSN7PbvLeM7l1tK6GXL8mrTiH2myg==} + '@rolldown/binding-android-arm-eabi@1.2.12': + resolution: {integrity: sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@rolldown/binding-android-arm64@1.2.10': - resolution: {integrity: sha512-wm6Dld3RXUAZ/gRWKyUy+4W1B5CB5UeFaOzsSWJWEdxZXHH8rCYiZ5dGe6oJmhsunAPWzL7FZV+VtvmN5Ye2eA==} + '@rolldown/binding-android-arm64@1.2.12': + resolution: {integrity: sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.2.10': - resolution: {integrity: sha512-UbEfXq/AqGNgRTV3ik+X/iR6mUxu2QdYAadwRxJWquUGnW6gDqdP1FtLtFXRow7RJx0ssRwi80XAPr4r+4DtsA==} + '@rolldown/binding-darwin-arm64@1.2.12': + resolution: {integrity: sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.10': - resolution: {integrity: sha512-7f5h17q5KZVx/ji1vb8OTq31ch1O2I7K8NPIr44GkyWTApXMIsmhWqZfgpOH10xeauqghDAvGlZktasCkcF6Eg==} + '@rolldown/binding-darwin-x64@1.2.12': + resolution: {integrity: sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.2.10': - resolution: {integrity: sha512-ynOk/eEYhC6ZB2xCGvKrEOwE58oBy9LnrAqtkrDF9Fz1VTaNdGZTsV0VarJdhPwb+sOJTGjCLwcuyRJZ1dnMcQ==} + '@rolldown/binding-freebsd-x64@1.2.12': + resolution: {integrity: sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.2.10': - resolution: {integrity: sha512-ERrAs185meZZhGan7a4l3RiiJK1ArSDlHdST++uvSxe+FDbR4TwUPahT/cbZJvaG6fIpDpF78surN+tX708Y4Q==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.12': + resolution: {integrity: sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.2.10': - resolution: {integrity: sha512-KN7OHKD0J3jy1UzBwZWPxpwhODf9IARUIJcrH+yLYKOcmegZ8luEUM38lDP1bDVj40yP6PsSzCqOJF76vljFnQ==} + '@rolldown/binding-linux-arm64-gnu@1.2.12': + resolution: {integrity: sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.2.10': - resolution: {integrity: sha512-8l9wP8O+wa8zD6iw6egSfzVtu7oZVfH3hlUsMM4MwbLMhxleqeoXbZzjddyK3YyNlwLhqznq3tF7PkNJ8T/V2w==} + '@rolldown/binding-linux-arm64-musl@1.2.12': + resolution: {integrity: sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.2.10': - resolution: {integrity: sha512-SeXNKeQzA5kLhz/J0CH6ZP0/HJ3v1xm/0YbiYpE0kK7emfRC2OIGGIaE14xzkISEGv2aYuUSpiLiU5Gbq+OI0A==} + '@rolldown/binding-linux-ppc64-gnu@1.2.12': + resolution: {integrity: sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.10': - resolution: {integrity: sha512-mtht0nR+y8/hart4175Ll15w7lY8dg7CtQ+j2FDNTsDRspOWTK/2V3l0aj9sIj7XmvqxT8Yli/wq22e7feTTWg==} + '@rolldown/binding-linux-s390x-gnu@1.2.12': + resolution: {integrity: sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.10': - resolution: {integrity: sha512-FSM94nGd55NYo48usCyM/nHfUKRnqc9+b0vJNuKV0oCCpIp/OGims7rO1Nv/DkFkt0S/s2rxsJ2kkS8J3HcpeA==} + '@rolldown/binding-linux-x64-gnu@1.2.12': + resolution: {integrity: sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.2.10': - resolution: {integrity: sha512-C3YxNB16myRLs7o+B+6PnQ6jBsdIS4+AE4Ah8glVGhDpEv9AOvxhZ/1duAb4B0UGczEK/lBbccksd8VI+p6zfw==} + '@rolldown/binding-linux-x64-musl@1.2.12': + resolution: {integrity: sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.2.10': - resolution: {integrity: sha512-571TlE/F1eeTjjdjYAMMMPs1Mfv3MtX6s3+ZKVU6HiUjZ5Njc6c/qzNy/8K3zALTZnaw3JQVYrHxvNfjm43KAg==} + '@rolldown/binding-openharmony-arm64@1.2.12': + resolution: {integrity: sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-win32-arm64-msvc@1.2.10': - resolution: {integrity: sha512-QXW+ZWaiqs2c7Fi++D/SsW07LTPcUrncxcskJGfGNBoaLik1IU6fJymz4HsqwEO0u5Iq11yTO0B/mc4cPk7jrQ==} + '@rolldown/binding-win32-arm64-msvc@1.2.12': + resolution: {integrity: sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.10': - resolution: {integrity: sha512-5FQFGgah17YeMtG1Yd5a+rMxQpTksyNXxRtKz06FVTaQw3RKYUJQbUoKk0/5jrXBpDo+7makNP7UHA2LQyH64A==} + '@rolldown/binding-win32-x64-msvc@1.2.12': + resolution: {integrity: sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -416,8 +416,8 @@ packages: '@swc/helpers@0.5.23': resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} - '@tabler/core@1.6.0': - resolution: {integrity: sha512-AdYNV9SVWQy1h3SvcvRG0WH/N7objsf4tzRFVcRdKq7EUogF5hH9EkJup5SycBMrs7g7fGZHdY9h/s18rgy/Ww==} + '@tabler/core@1.6.1': + resolution: {integrity: sha512-lgmR9gcxRp5JlLxcmRi9Rz85JrEvH6Bz71KnlcPVSa97wtwQNjgSs3g3fxwbAlulHyRMghhIV04QUuW3pqDsIg==} engines: {node: '>=20'} peerDependencies: vanilla-calendar-pro: ^3.3.2 @@ -425,39 +425,39 @@ packages: vanilla-calendar-pro: optional: true - '@tabler/icons-react@3.48.0': - resolution: {integrity: sha512-OCeAlpaWHEMV+EsgdVeGQ3YuKfmfnThZhc++CaZ/ujYzi5+/DONDQlX4OGvH1ikYhIfmc0grI4otJgHy7QAvng==} + '@tabler/icons-react@3.49.0': + resolution: {integrity: sha512-Um59pm5RVNUaQjFUtG0r4oYJbZuBS3tGugAA2tgBg0WufMk34Eio3nsO30RUkLuL2giYPQUNdY9HvqltLLYyNA==} peerDependencies: react: '>= 16' - '@tabler/icons@3.48.0': - resolution: {integrity: sha512-lQk06gVHNVBnJp0UOgtth54mcZBJM7rdYKsAxKOProj1bbSAx+/xiuOWfHlyvgdL3M+Y+I0HINrjHa3ygp8eXQ==} + '@tabler/icons@3.49.0': + resolution: {integrity: sha512-l+GnSkFETx0U26icxv1VUFnxxiTqXfCcVRiRAnyMwibupM2S2L/hSaZ9gmSgz01rkrsH/93NvLsxykqdanc6lw==} - '@tanstack/query-core@5.104.0': - resolution: {integrity: sha512-JrC2r/JQlXt7khBSdUpsgxNvybzOg+aITa+ARRMlP2AFo93Y8vIqz077rp+e61mJetSZ0T6AJbwW1JHer1vrPQ==} + '@tanstack/query-core@5.104.1': + resolution: {integrity: sha512-TiRghcrGkUTE+M4JBxvpPSpBhxvIzQairw+cksrHYGr+2uwC77yW1SY9nYxnYRYOppvucf8rEg6b5byoz4rTpw==} - '@tanstack/react-query@5.104.0': - resolution: {integrity: sha512-e1TZmDCQnWIfiDVryIHeA6Idj+Lfx1hORLOhXS/l5wcgvtVD4yYqbTDl378sGQKIi2cSgb0TEMC9cKK8GGoJEw==} + '@tanstack/react-query@5.104.1': + resolution: {integrity: sha512-Zz70EgjahNI7aCz8BZxM3vUGU44ycxEAzMuCUjLeA7SVhhOkg3ZgPjPJPvHjs6Rmky/dtmnu1WzjGHNHmhCoZQ==} peerDependencies: react: ^18 || ^19 - '@tanstack/react-store@0.11.1': - resolution: {integrity: sha512-HaIGKI3YLmjBYIvy5DFDY23oNaYZIsTZfngey07Uh5iLVJgM3bIGCnZeOFOqzjFld9JHWcaHJnasD/bKoGKwJQ==} + '@tanstack/react-store@0.11.2': + resolution: {integrity: sha512-oj5O5DmiMWE+ih+SioX2ayC1kbK7d3fpLx0B75fPJr9nY3zU+hoW010yr64qXsY/9fyiLVE2bn5mGNV3owm8RQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/react-table@9.2.4': - resolution: {integrity: sha512-Rzp1Q4e0/nIgEjmISYR5HeEgLTNtrG+C7NFZf/AbCxPO5hg+zC3yNGwcoECigUk8SFzzvhXbd2rFdtP2ZiGrfA==} + '@tanstack/react-table@9.2.6': + resolution: {integrity: sha512-Wmt3sbzwz+5CtB0pqIxGw2FrNW9mF4AhJ9KnS1/VGZ8cDAEg51S4ixru2dnF0xQnY407kdYNLer3ms+n8zCN6w==} engines: {node: '>=20'} peerDependencies: react: '>=18' - '@tanstack/store@0.11.1': - resolution: {integrity: sha512-mzTOBhypOuDJAy/D8n2MfUZ1HFkXnmSETviRyhqEC8LUE7/IZQExOTxMANj3KjTofYTkFNpBY67qaVrT41YccA==} + '@tanstack/store@0.11.2': + resolution: {integrity: sha512-sJ4mjol8uQsHV0gOJzzjwXfh2Fwm+Sz0+8deqiTm4jGbMdjzNSW+xZCFm0kUa870uhd8yi+DpKnZb5Kc4apa0Q==} - '@tanstack/table-core@9.2.4': - resolution: {integrity: sha512-GwdDyGGr6UXAtubF14yAwcXvdaogqfsQgIk89Suebjxob/Rjq2xvfmXsjDF1rQmKmhHsJm9TOY7myxv3i6geJw==} + '@tanstack/table-core@9.2.6': + resolution: {integrity: sha512-t5LaLx8gDCgzjvwtZo5xtLklJP11D9zaKOW8q6jOCyuQYhkqpTK1heqMfgTQwDlhl0dU8f1X66Xurbj3jpCUSw==} engines: {node: '>=20'} '@types/hoist-non-react-statics@3.3.7': @@ -482,13 +482,13 @@ packages: '@types/warning@3.0.4': resolution: {integrity: sha512-CqN8MnISMwQbLJXO3doBAV4Yw9hx9/Pyr2rZ78+NfaCnhyRA/nKrpyk6E7mKw17ZOaQdLpK9GiUjrqLzBlN3sg==} - '@vitejs/plugin-react@6.1.1': - resolution: {integrity: sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==} + '@vitejs/plugin-react@6.1.2': + resolution: {integrity: sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 babel-plugin-react-compiler: ^1.0.0 - oxc-transform-react: ^0.145.0 + oxc-transform-react: ^0.152.0 vite: ^8.0.0 peerDependenciesMeta: '@rolldown/plugin-babel': @@ -618,8 +618,8 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} - intl-messageformat@12.1.2: - resolution: {integrity: sha512-Y7ZnMtT/04nVJnjf1/x/AkfP5we/iN0AhckDdlFPtnetk6/VTuj2OPW5SE7steMd3UNaOtky4KJJY6jwyvWfJg==} + intl-messageformat@12.1.3: + resolution: {integrity: sha512-JXi5wk8R/oSECKyxSIZ00VS8KG6MZkHM0qU4NS7RrvIWWXkiv3wVqGlCvEvyXiDvKlt2yqwZMiZGIv+I8rrnZQ==} invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} @@ -793,8 +793,8 @@ packages: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} - postcss@8.5.28: - resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + postcss@8.5.29: + resolution: {integrity: sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==} engines: {node: ^10 || ^12 || >=14} prop-types-extra@1.1.1: @@ -841,8 +841,8 @@ packages: react-fast-compare@2.0.4: resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==} - react-intl@12.1.3: - resolution: {integrity: sha512-o14ogaAGSaYZMpyXcEnweedO2QoD1I5EdrZXq1iy+rFfhLWf8Nmr0YDCWtGwDAIPD/32ymnylpk7LGiS/AB5yw==} + react-intl@12.1.4: + resolution: {integrity: sha512-huK6iXamrEJ4D011VCmO+/CyzFtSKeF7eqcHW1dVe3t2wpgB/Iscl6CoMso/YLItX3Qh/gcVQMXNbbhpGbCrUw==} peerDependencies: '@types/react': '>=18.0.0' react: '>=18.0.0' @@ -899,8 +899,8 @@ packages: engines: {node: '>= 0.4'} hasBin: true - rolldown@1.2.10: - resolution: {integrity: sha512-OxkA08pSryMK7B3XiFA09B4OJ1xJMPgIYCBMY2xchzpqgBGsV1o0DetPAE+Sl3N3L4oCPiEzmHVSOj7iR04Zog==} + rolldown@1.2.12: + resolution: {integrity: sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -914,8 +914,8 @@ packages: scheduler@0.28.0: resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} source-map@0.5.7: @@ -967,8 +967,8 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - vite@8.3.1: - resolution: {integrity: sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==} + vite@8.3.4: + resolution: {integrity: sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -1075,39 +1075,39 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@biomejs/biome@2.5.14': + '@biomejs/biome@2.5.15': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.5.14 - '@biomejs/cli-darwin-x64': 2.5.14 - '@biomejs/cli-linux-arm64': 2.5.14 - '@biomejs/cli-linux-arm64-musl': 2.5.14 - '@biomejs/cli-linux-x64': 2.5.14 - '@biomejs/cli-linux-x64-musl': 2.5.14 - '@biomejs/cli-win32-arm64': 2.5.14 - '@biomejs/cli-win32-x64': 2.5.14 - - '@biomejs/cli-darwin-arm64@2.5.14': + '@biomejs/cli-darwin-arm64': 2.5.15 + '@biomejs/cli-darwin-x64': 2.5.15 + '@biomejs/cli-linux-arm64': 2.5.15 + '@biomejs/cli-linux-arm64-musl': 2.5.15 + '@biomejs/cli-linux-x64': 2.5.15 + '@biomejs/cli-linux-x64-musl': 2.5.15 + '@biomejs/cli-win32-arm64': 2.5.15 + '@biomejs/cli-win32-x64': 2.5.15 + + '@biomejs/cli-darwin-arm64@2.5.15': optional: true - '@biomejs/cli-darwin-x64@2.5.14': + '@biomejs/cli-darwin-x64@2.5.15': optional: true - '@biomejs/cli-linux-arm64-musl@2.5.14': + '@biomejs/cli-linux-arm64-musl@2.5.15': optional: true - '@biomejs/cli-linux-arm64@2.5.14': + '@biomejs/cli-linux-arm64@2.5.15': optional: true - '@biomejs/cli-linux-x64-musl@2.5.14': + '@biomejs/cli-linux-x64-musl@2.5.15': optional: true - '@biomejs/cli-linux-x64@2.5.14': + '@biomejs/cli-linux-x64@2.5.15': optional: true - '@biomejs/cli-win32-arm64@2.5.14': + '@biomejs/cli-win32-arm64@2.5.15': optional: true - '@biomejs/cli-win32-x64@2.5.14': + '@biomejs/cli-win32-x64@2.5.15': optional: true '@ebay/nice-modal-react@1.2.13(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': @@ -1192,17 +1192,17 @@ snapshots: '@formatjs/fast-memoize@3.1.7': {} - '@formatjs/icu-messageformat-parser@3.5.20': + '@formatjs/icu-messageformat-parser@3.5.21': dependencies: '@formatjs/icu-skeleton-parser': 2.1.12 '@formatjs/icu-skeleton-parser@2.1.12': {} - '@formatjs/intl@6.1.2': + '@formatjs/intl@6.1.3': dependencies: '@formatjs/fast-memoize': 3.1.7 - '@formatjs/icu-messageformat-parser': 3.5.20 - intl-messageformat: 12.1.2 + '@formatjs/icu-messageformat-parser': 3.5.21 + intl-messageformat: 12.1.3 '@internationalized/date@3.12.4': dependencies: @@ -1235,7 +1235,7 @@ snapshots: jsbi: 4.3.2 optional: true - '@oxc-project/types@0.151.0': {} + '@oxc-project/types@0.152.0': {} '@popperjs/core@2.11.8': {} @@ -1276,49 +1276,49 @@ snapshots: uncontrollable: 8.0.4(react@19.3.0) warning: 4.0.3 - '@rolldown/binding-android-arm-eabi@1.2.10': + '@rolldown/binding-android-arm-eabi@1.2.12': optional: true - '@rolldown/binding-android-arm64@1.2.10': + '@rolldown/binding-android-arm64@1.2.12': optional: true - '@rolldown/binding-darwin-arm64@1.2.10': + '@rolldown/binding-darwin-arm64@1.2.12': optional: true - '@rolldown/binding-darwin-x64@1.2.10': + '@rolldown/binding-darwin-x64@1.2.12': optional: true - '@rolldown/binding-freebsd-x64@1.2.10': + '@rolldown/binding-freebsd-x64@1.2.12': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.10': + '@rolldown/binding-linux-arm-gnueabihf@1.2.12': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.10': + '@rolldown/binding-linux-arm64-gnu@1.2.12': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.10': + '@rolldown/binding-linux-arm64-musl@1.2.12': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.10': + '@rolldown/binding-linux-ppc64-gnu@1.2.12': optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.10': + '@rolldown/binding-linux-s390x-gnu@1.2.12': optional: true - '@rolldown/binding-linux-x64-gnu@1.2.10': + '@rolldown/binding-linux-x64-gnu@1.2.12': optional: true - '@rolldown/binding-linux-x64-musl@1.2.10': + '@rolldown/binding-linux-x64-musl@1.2.12': optional: true - '@rolldown/binding-openharmony-arm64@1.2.10': + '@rolldown/binding-openharmony-arm64@1.2.12': optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.10': + '@rolldown/binding-win32-arm64-msvc@1.2.12': optional: true - '@rolldown/binding-win32-x64-msvc@1.2.10': + '@rolldown/binding-win32-x64-msvc@1.2.12': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -1327,44 +1327,44 @@ snapshots: dependencies: tslib: 2.8.1 - '@tabler/core@1.6.0': + '@tabler/core@1.6.1': dependencies: '@popperjs/core': 2.11.8 - '@tabler/icons-react@3.48.0(react@19.3.0)': + '@tabler/icons-react@3.49.0(react@19.3.0)': dependencies: - '@tabler/icons': 3.48.0 + '@tabler/icons': 3.49.0 react: 19.3.0 - '@tabler/icons@3.48.0': {} + '@tabler/icons@3.49.0': {} - '@tanstack/query-core@5.104.0': {} + '@tanstack/query-core@5.104.1': {} - '@tanstack/react-query@5.104.0(react@19.3.0)': + '@tanstack/react-query@5.104.1(react@19.3.0)': dependencies: - '@tanstack/query-core': 5.104.0 + '@tanstack/query-core': 5.104.1 react: 19.3.0 - '@tanstack/react-store@0.11.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + '@tanstack/react-store@0.11.2(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': dependencies: - '@tanstack/store': 0.11.1 + '@tanstack/store': 0.11.2 react: 19.3.0 react-dom: 19.3.0(react@19.3.0) use-sync-external-store: 1.7.0(react@19.3.0) - '@tanstack/react-table@9.2.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + '@tanstack/react-table@9.2.6(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': dependencies: - '@tanstack/react-store': 0.11.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - '@tanstack/table-core': 9.2.4 + '@tanstack/react-store': 0.11.2(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@tanstack/table-core': 9.2.6 react: 19.3.0 transitivePeerDependencies: - react-dom - '@tanstack/store@0.11.1': {} + '@tanstack/store@0.11.2': {} - '@tanstack/table-core@9.2.4': + '@tanstack/table-core@9.2.6': dependencies: - '@tanstack/store': 0.11.1 + '@tanstack/store': 0.11.2 '@types/hoist-non-react-statics@3.3.7(@types/react@19.3.0)': dependencies: @@ -1385,10 +1385,10 @@ snapshots: '@types/warning@3.0.4': {} - '@vitejs/plugin-react@6.1.1(vite@8.3.1)': + '@vitejs/plugin-react@6.1.2(vite@8.3.4)': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.3.1 + vite: 8.3.4 aria-hidden@1.2.6: dependencies: @@ -1491,10 +1491,10 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 - intl-messageformat@12.1.2: + intl-messageformat@12.1.3: dependencies: '@formatjs/fast-memoize': 3.1.7 - '@formatjs/icu-messageformat-parser': 3.5.20 + '@formatjs/icu-messageformat-parser': 3.5.21 invariant@2.2.4: dependencies: @@ -1609,11 +1609,11 @@ snapshots: picomatch@4.0.7: {} - postcss@8.5.28: + postcss@8.5.29: dependencies: nanoid: 3.3.19 picocolors: 1.1.1 - source-map-js: 1.2.1 + source-map-js: 1.2.2 prop-types-extra@1.1.1(react@19.3.0): dependencies: @@ -1682,12 +1682,12 @@ snapshots: react-fast-compare@2.0.4: {} - react-intl@12.1.3(@types/react@19.3.0)(react@19.3.0): + react-intl@12.1.4(@types/react@19.3.0)(react@19.3.0): dependencies: - '@formatjs/icu-messageformat-parser': 3.5.20 - '@formatjs/intl': 6.1.2 + '@formatjs/icu-messageformat-parser': 3.5.21 + '@formatjs/intl': 6.1.3 '@types/react': 19.3.0 - intl-messageformat: 12.1.2 + intl-messageformat: 12.1.3 react: 19.3.0 react-is@16.13.1: {} @@ -1755,26 +1755,26 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rolldown@1.2.10: + rolldown@1.2.12: dependencies: - '@oxc-project/types': 0.151.0 + '@oxc-project/types': 0.152.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm-eabi': 1.2.10 - '@rolldown/binding-android-arm64': 1.2.10 - '@rolldown/binding-darwin-arm64': 1.2.10 - '@rolldown/binding-darwin-x64': 1.2.10 - '@rolldown/binding-freebsd-x64': 1.2.10 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.10 - '@rolldown/binding-linux-arm64-gnu': 1.2.10 - '@rolldown/binding-linux-arm64-musl': 1.2.10 - '@rolldown/binding-linux-ppc64-gnu': 1.2.10 - '@rolldown/binding-linux-s390x-gnu': 1.2.10 - '@rolldown/binding-linux-x64-gnu': 1.2.10 - '@rolldown/binding-linux-x64-musl': 1.2.10 - '@rolldown/binding-openharmony-arm64': 1.2.10 - '@rolldown/binding-win32-arm64-msvc': 1.2.10 - '@rolldown/binding-win32-x64-msvc': 1.2.10 + '@rolldown/binding-android-arm-eabi': 1.2.12 + '@rolldown/binding-android-arm64': 1.2.12 + '@rolldown/binding-darwin-arm64': 1.2.12 + '@rolldown/binding-darwin-x64': 1.2.12 + '@rolldown/binding-freebsd-x64': 1.2.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.12 + '@rolldown/binding-linux-arm64-gnu': 1.2.12 + '@rolldown/binding-linux-arm64-musl': 1.2.12 + '@rolldown/binding-linux-ppc64-gnu': 1.2.12 + '@rolldown/binding-linux-s390x-gnu': 1.2.12 + '@rolldown/binding-linux-x64-gnu': 1.2.12 + '@rolldown/binding-linux-x64-musl': 1.2.12 + '@rolldown/binding-openharmony-arm64': 1.2.12 + '@rolldown/binding-win32-arm64-msvc': 1.2.12 + '@rolldown/binding-win32-x64-msvc': 1.2.12 rooks@9.9.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: @@ -1789,7 +1789,7 @@ snapshots: scheduler@0.28.0: {} - source-map-js@1.2.1: {} + source-map-js@1.2.2: {} source-map@0.5.7: {} @@ -1830,12 +1830,12 @@ snapshots: dependencies: react: 19.3.0 - vite@8.3.1: + vite@8.3.4: dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 - postcss: 8.5.28 - rolldown: 1.2.10 + postcss: 8.5.29 + rolldown: 1.2.12 tinyglobby: 0.2.17 optionalDependencies: fsevents: 2.3.3 From 22007fc02f9402c96b739c239da6324ba309523f Mon Sep 17 00:00:00 2001 From: Zoey2936 <75573284+Zoey2936@users.noreply.github.com> Date: Fri, 2 Oct 2026 05:00:26 +0000 Subject: [PATCH 13/62] update aws-lc version to v5.11.0 Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c5000df131..0d25c09057 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ SHELL ["/bin/ash", "-eo", "pipefail", "-c"] ARG LUAJIT_INC=/usr/include/luajit-2.1 ARG LUAJIT_LIB=/usr/lib -ARG AWSLC_VER=3fe7e081e62131b6776f0d923312b5e6756907ce # v5.10.0 +ARG AWSLC_VER=fa9bc8d6f7cfb2cf849b247fb06bbbfc41ecc9d6 # v5.11.0 ARG NGINX_VER=45a318d05a0fd23f57ffe9579f7f0969c0fe402a # release-1.31.6 ARG DTR_VER=1.29.2 From fd40672f4aa8d16069e5869a4e72ee63e90254fc Mon Sep 17 00:00:00 2001 From: Zoey Date: Sat, 3 Oct 2026 12:54:32 +0200 Subject: [PATCH 14/62] update nextcloud aio defaults Signed-off-by: Zoey --- backend/setup.js | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/setup.js b/backend/setup.js index fa80d2c6a2..0c3c743294 100644 --- a/backend/setup.js +++ b/backend/setup.js @@ -196,6 +196,7 @@ const setupAio = async () => { npmplus_crowdsec_appsec: false, npmplus_proxy_request_buffering: true, npmplus_proxy_response_buffering: true, + npmplus_upstream_compression: true, certificate_id: "new", ssl_forced: true, hsts_enabled: true, From ebae8098d28545aeac82e3aa4f08c29a700d722d Mon Sep 17 00:00:00 2001 From: Zoey Date: Sat, 3 Oct 2026 21:05:48 +0200 Subject: [PATCH 15/62] small fixes --- backend/internal/access-list.js | 11 ++++++++++- backend/internal/certificate.js | 1 + backend/internal/ip_ranges.js | 1 + .../schema/components/redirection-host-object.json | 1 - .../schema/paths/nginx/redirection-hosts/post.json | 4 ++-- frontend/src/hooks/useAccessList.js | 2 +- frontend/src/hooks/useDeadHost.js | 2 +- frontend/src/hooks/useProxyHost.js | 2 +- frontend/src/hooks/useRedirectionHost.js | 4 ++-- frontend/src/hooks/useStream.js | 2 +- frontend/src/hooks/useUser.js | 2 +- frontend/src/modals/RedirectionHostModal.jsx | 2 +- 12 files changed, 22 insertions(+), 12 deletions(-) diff --git a/backend/internal/access-list.js b/backend/internal/access-list.js index 9cdc638e04..69f296426c 100644 --- a/backend/internal/access-list.js +++ b/backend/internal/access-list.js @@ -18,6 +18,10 @@ const internalAccessList = { */ create: async (access, data) => { access.can("access_lists:manage"); + if (data.items?.some((item) => !item.password)) { + throw new errs.ValidationError("New access list users need a password"); + } + const row = await accessListModel.query().insertAndFetch({ name: data.name, satisfy_any: data.satisfy_any, @@ -80,13 +84,18 @@ const internalAccessList = { */ update: async (access, data) => { access.can("access_lists:manage"); - const row = await internalAccessList.get(access, { id: data.id }); + const row = await internalAccessList.get(access, { id: data.id, expand: ["items"] }); if (row.id !== data.id) { // Sanity check that something crazy hasn't happened throw new errs.InternalValidationError( `Access List could not be updated, IDs do not match: ${row.id} !== ${data.id}`, ); } + if ( + data.items?.some((item) => !item.password && !row.items.some(({ username }) => username === item.username)) + ) { + throw new errs.ValidationError("New access list users need a password"); + } // patch name if specified if ( diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 7c731268a7..4cec0c5320 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -30,6 +30,7 @@ const internalCertificate = { intervalProcessing: false, initTimer: () => { + if (internalCertificate.interval) return; logger.info("Certbot Renewal Timer initialized"); internalCertificate.interval = setInterval( internalCertificate.processExpiringHosts, diff --git a/backend/internal/ip_ranges.js b/backend/internal/ip_ranges.js index 3e98e8d9cc..a437eaa253 100644 --- a/backend/internal/ip_ranges.js +++ b/backend/internal/ip_ranges.js @@ -21,6 +21,7 @@ const internalIpRanges = { interval_processing: false, initTimer: () => { + if (internalIpRanges.interval) return; logger.info("IP Ranges Renewal Timer initialized"); internalIpRanges.interval = setInterval(internalIpRanges.fetch, internalIpRanges.interval_timeout); }, diff --git a/backend/schema/components/redirection-host-object.json b/backend/schema/components/redirection-host-object.json index 8375baa021..0d48f75f90 100644 --- a/backend/schema/components/redirection-host-object.json +++ b/backend/schema/components/redirection-host-object.json @@ -51,7 +51,6 @@ "forward_scheme": { "type": "string", "enum": [ - "auto", "$scheme", "http", "https" diff --git a/backend/schema/paths/nginx/redirection-hosts/post.json b/backend/schema/paths/nginx/redirection-hosts/post.json index 9619e20d93..2a3c007366 100644 --- a/backend/schema/paths/nginx/redirection-hosts/post.json +++ b/backend/schema/paths/nginx/redirection-hosts/post.json @@ -88,7 +88,7 @@ "test.example.com" ], "forward_domain_name": "example.com", - "forward_scheme": "auto", + "forward_scheme": "$scheme", "forward_http_code": 301, "preserve_path": false, "certificate_id": 0, @@ -130,7 +130,7 @@ "enabled": true, "hsts_enabled": false, "hsts_subdomains": false, - "forward_scheme": "auto", + "forward_scheme": "$scheme", "forward_http_code": 301 } } diff --git a/frontend/src/hooks/useAccessList.js b/frontend/src/hooks/useAccessList.js index 86281d3af1..9667ea8ee0 100644 --- a/frontend/src/hooks/useAccessList.js +++ b/frontend/src/hooks/useAccessList.js @@ -40,7 +40,7 @@ const useSetAccessList = () => { })); return () => queryClient.setQueryData(["access-list", values.id], previousObject); }, - onError: (_, __, rollback) => rollback(), + onError: (_, __, rollback) => rollback?.(), onSuccess: async ({ id }) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["access-list", id] }), diff --git a/frontend/src/hooks/useDeadHost.js b/frontend/src/hooks/useDeadHost.js index 18b30f5b81..80174aae00 100644 --- a/frontend/src/hooks/useDeadHost.js +++ b/frontend/src/hooks/useDeadHost.js @@ -48,7 +48,7 @@ const useSetDeadHost = () => { })); return () => queryClient.setQueryData(["dead-host", values.id], previousObject); }, - onError: (_, __, rollback) => rollback(), + onError: (_, __, rollback) => rollback?.(), onSuccess: async ({ id }) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["dead-host", id] }), diff --git a/frontend/src/hooks/useProxyHost.js b/frontend/src/hooks/useProxyHost.js index 0c090535db..c68539461e 100644 --- a/frontend/src/hooks/useProxyHost.js +++ b/frontend/src/hooks/useProxyHost.js @@ -63,7 +63,7 @@ const useSetProxyHost = () => { })); return () => queryClient.setQueryData(["proxy-host", values.id], previousObject); }, - onError: (_, __, rollback) => rollback(), + onError: (_, __, rollback) => rollback?.(), onSuccess: async ({ id }) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["proxy-host", id] }), diff --git a/frontend/src/hooks/useRedirectionHost.js b/frontend/src/hooks/useRedirectionHost.js index 7ee5b3c53d..f237869331 100644 --- a/frontend/src/hooks/useRedirectionHost.js +++ b/frontend/src/hooks/useRedirectionHost.js @@ -18,7 +18,7 @@ const fetchRedirectionHost = (id) => { npmplusMtlsCertificateId: 0, npmplusMtlsVerifyClientOptional: false, npmplusHttp3Support: false, - forwardScheme: "auto", + forwardScheme: "$scheme", forwardHttpCode: 301, enabled: true, hstsEnabled: false, @@ -52,7 +52,7 @@ const useSetRedirectionHost = () => { })); return () => queryClient.setQueryData(["redirection-host", values.id], previousObject); }, - onError: (_, __, rollback) => rollback(), + onError: (_, __, rollback) => rollback?.(), onSuccess: async ({ id }) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["redirection-host", id] }), diff --git a/frontend/src/hooks/useStream.js b/frontend/src/hooks/useStream.js index e5f4f3a946..9c9ef3666d 100644 --- a/frontend/src/hooks/useStream.js +++ b/frontend/src/hooks/useStream.js @@ -48,7 +48,7 @@ const useSetStream = () => { })); return () => queryClient.setQueryData(["stream", values.id], previousObject); }, - onError: (_, __, rollback) => rollback(), + onError: (_, __, rollback) => rollback?.(), onSuccess: async ({ id }) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["stream", id] }), diff --git a/frontend/src/hooks/useUser.js b/frontend/src/hooks/useUser.js index bf5077737d..83d27ec5b9 100644 --- a/frontend/src/hooks/useUser.js +++ b/frontend/src/hooks/useUser.js @@ -41,7 +41,7 @@ const useSetUser = () => { })); return () => queryClient.setQueryData(["user", values.id], previousObject); }, - onError: (_, __, rollback) => rollback(), + onError: (_, __, rollback) => rollback?.(), onSuccess: async ({ id }) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["user", id] }), diff --git a/frontend/src/modals/RedirectionHostModal.jsx b/frontend/src/modals/RedirectionHostModal.jsx index 583b7791e7..067b86a6f2 100644 --- a/frontend/src/modals/RedirectionHostModal.jsx +++ b/frontend/src/modals/RedirectionHostModal.jsx @@ -79,7 +79,7 @@ const RedirectionHostModal = EasyModal.create(({ id, visible, remove }) => { // Details tab domainNames: data?.domainNames || [], forwardDomainName: data?.forwardDomainName || "", - forwardScheme: data?.forwardScheme || "auto", + forwardScheme: data?.forwardScheme || "$scheme", forwardHttpCode: data?.forwardHttpCode || 301, preservePath: data?.preservePath || false, // SSL tab From 1b9b5e159b73fd87e53539607d64b991bdf89a70 Mon Sep 17 00:00:00 2001 From: Zoey Date: Thu, 8 Oct 2026 21:16:18 +0200 Subject: [PATCH 16/62] remove not needed gso fix patch --- .github/workflows/dependency-updates.yml | 3 --- Dockerfile | 19 ++++++++----------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/.github/workflows/dependency-updates.yml b/.github/workflows/dependency-updates.yml index f5c624fa6b..0927baeb6e 100644 --- a/.github/workflows/dependency-updates.yml +++ b/.github/workflows/dependency-updates.yml @@ -173,9 +173,6 @@ jobs: - name: nginx repo: nginx/nginx pull: 1333 - - name: nginx - repo: nginx/nginx - pull: 1430 - name: fancyindex repo: aperezdc/ngx-fancyindex pull: 176 diff --git a/Dockerfile b/Dockerfile index 0d25c09057..d5f6b95b2b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -78,19 +78,16 @@ RUN git-clone-commit.sh https://github.com/nginx/nginx "$NGINX_VER" /src/nginx & wget -q https://raw.githubusercontent.com/zlib-ng/patches/master/nginx/"$ZNP_VER"-zlib-ng.patch -O /src/nginx/6.patch && \ echo "bcd0f2fb9723fc1f251f94cead8d5160e767f7d4a04365331396a72a9ba54c6b /src/nginx/6.patch" | sha256sum -c - && \ git apply /src/nginx/6.patch && \ - wget -q https://patch-diff.githubusercontent.com/raw/nginx/nginx/pull/1430.patch -O /src/nginx/7.patch && \ - echo "c8e827d50314b6ec027677ae8c70b11f805408af3efb5175bf377071bd2a14a5 /src/nginx/7.patch" | sha256sum -c - && \ + wget -q https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx/"$ORP_VER"/nginx-"$ORP_VER"-resolver_hosts.patch -O /src/nginx/7.patch && \ + echo "7a3e9ebe4fafaef0a90773ed093bed83c3e753af5983718b0aee50881c32151b /src/nginx/7.patch" | sha256sum -c - && \ git apply /src/nginx/7.patch && \ - wget -q https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx/"$ORP_VER"/nginx-"$ORP_VER"-resolver_hosts.patch -O /src/nginx/8.patch && \ - echo "7a3e9ebe4fafaef0a90773ed093bed83c3e753af5983718b0aee50881c32151b /src/nginx/8.patch" | sha256sum -c - && \ - git apply /src/nginx/8.patch && \ sed -i "s|ngx_destroy_pool(r->hosts->pool);|r->hosts->pool->log = r->log; &|" /src/nginx/src/core/ngx_resolver.c && \ - wget -q https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx/"$ORP_VER"/nginx-"$ORP_VER"-upstream_pipelining.patch -O /src/nginx/9.patch && \ - echo "f147c9724a0ad33084a3cb51acdafbd4dbd2c40ba5840af1684b7b9595b24ae2 /src/nginx/9.patch" | sha256sum -c - && \ - git apply /src/nginx/9.patch && \ - wget -q https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx/"$ORP_VER"/nginx-"$ORP_VER"-reuseport_close_unused_fds.patch -O /src/nginx/10.patch && \ - echo "7f1c3eefcd841502515f529e3a0c0a17492e7e44ba5c47fc50ae6a3123d8937f /src/nginx/10.patch" | sha256sum -c - && \ - git apply -C1 /src/nginx/10.patch && \ + wget -q https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx/"$ORP_VER"/nginx-"$ORP_VER"-upstream_pipelining.patch -O /src/nginx/8.patch && \ + echo "f147c9724a0ad33084a3cb51acdafbd4dbd2c40ba5840af1684b7b9595b24ae2 /src/nginx/8.patch" | sha256sum -c - && \ + git apply /src/nginx/8.patch && \ + wget -q https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx/"$ORP_VER"/nginx-"$ORP_VER"-reuseport_close_unused_fds.patch -O /src/nginx/9.patch && \ + echo "7f1c3eefcd841502515f529e3a0c0a17492e7e44ba5c47fc50ae6a3123d8937f /src/nginx/9.patch" | sha256sum -c - && \ + git apply -C1 /src/nginx/9.patch && \ git apply /src/nginx-footer.patch && \ git apply /src/nginx-ip-sni.patch && \ git apply /src/nginx-buffer-log.patch && \ From 3ce647e6e91822d44f8e98899241c88a5bf578a4 Mon Sep 17 00:00:00 2001 From: Zoey Date: Thu, 8 Oct 2026 21:38:37 +0200 Subject: [PATCH 17/62] add toggle to skip auth request if mtls successeded #3964 Signed-off-by: Zoey --- backend/internal/host.js | 7 ++++ backend/internal/nginx.js | 2 + ...20261008213833_mtls_bypass_auth_request.js | 33 +++++++++++++++ backend/models/proxy_host.js | 1 + backend/schema/common.json | 5 +++ .../schema/components/proxy-host-object.json | 4 ++ .../schema/paths/nginx/proxy-hosts/get.json | 1 + .../nginx/proxy-hosts/hostID/delete.json | 1 + .../proxy-hosts/hostID/disable/post.json | 1 + .../nginx/proxy-hosts/hostID/enable/post.json | 1 + .../paths/nginx/proxy-hosts/hostID/get.json | 1 + .../paths/nginx/proxy-hosts/hostID/put.json | 4 ++ .../schema/paths/nginx/proxy-hosts/post.json | 4 ++ backend/templates/proxy_host.conf | 6 +++ .../src/components/Form/SSLOptionsFields.jsx | 40 ++++++++++++++++++- frontend/src/hooks/useProxyHost.js | 1 + frontend/src/modals/ProxyHostModal.jsx | 3 +- frontend/translations/ui/en.json | 1 + frontend/translations/ui/nl.json | 1 + 19 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 backend/migrations/20261008213833_mtls_bypass_auth_request.js diff --git a/backend/internal/host.js b/backend/internal/host.js index 7ed2331660..e427659483 100644 --- a/backend/internal/host.js +++ b/backend/internal/host.js @@ -45,6 +45,13 @@ const internalHost = { combinedData.hsts_subdomains = false; } + combinedData.npmplus_mtls_bypass_auth_request &&= + combinedData.npmplus_mtls_certificate_id > 0 && + combinedData.npmplus_mtls_verify_client_optional === true && + [combinedData, ...(combinedData.locations ?? [])].some( + ({ npmplus_auth_request = "none" }) => npmplus_auth_request !== "none", + ); + return combinedData; }, diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js index d0164af40f..7bbdf3291a 100644 --- a/backend/internal/nginx.js +++ b/backend/internal/nginx.js @@ -290,6 +290,7 @@ const internalNginx = { if (htpasswdFileName.length > 0) { location.filename = htpasswdFileName; } + if (location.npmplus_enabled === false) continue; if (location.npmplus_auth_request === "anubis") { host.create_anubis_locations = true; } @@ -328,6 +329,7 @@ const internalNginx = { } else { for (const location of originalLocations) { if ( + location.npmplus_enabled !== false && (location.npmplus_auth_request === provider || (provider === "authentik" && location.npmplus_auth_request === "authentik-send-basic-auth")) && diff --git a/backend/migrations/20261008213833_mtls_bypass_auth_request.js b/backend/migrations/20261008213833_mtls_bypass_auth_request.js new file mode 100644 index 0000000000..e0e6a4a03e --- /dev/null +++ b/backend/migrations/20261008213833_mtls_bypass_auth_request.js @@ -0,0 +1,33 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "mtls_bypass_auth_request"; + +/** + * Migrate + * + * @see https://knexjs.org/guide/migrations.html#migration-api + * + * @param {Object} knex + * @returns {Promise} + */ +const up = async (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + await knex.schema.table("proxy_host", (proxy_host) => { + proxy_host.integer("npmplus_mtls_bypass_auth_request").notNull().unsigned().defaultTo(0); + }); + + logger.info(`[${migrateName}] proxy_host Table altered`); +}; + +/** + * Undo Migrate + * + * @param {Object} _knex + * @returns {Promise} + */ +const down = (_knex) => { + throw new Error(`[${migrateName}] You can't migrate down this one.`); +}; + +export { down, up }; diff --git a/backend/models/proxy_host.js b/backend/models/proxy_host.js index 43a73cfceb..5f137812ab 100644 --- a/backend/models/proxy_host.js +++ b/backend/models/proxy_host.js @@ -25,6 +25,7 @@ const boolFields = [ "npmplus_fancyindex", "npmplus_nginx_online", "npmplus_mtls_verify_client_optional", + "npmplus_mtls_bypass_auth_request", ]; class ProxyHost extends Model { diff --git a/backend/schema/common.json b/backend/schema/common.json index 8554b3d96f..09f8184680 100644 --- a/backend/schema/common.json +++ b/backend/schema/common.json @@ -147,6 +147,11 @@ "type": "boolean", "example": false }, + "npmplus_mtls_bypass_auth_request": { + "description": "Skip the auth request if the mTLS client verification succeeded", + "type": "boolean", + "example": false + }, "npmplus_noindex": { "description": "Send noindex header and block some user agents", "type": "boolean", diff --git a/backend/schema/components/proxy-host-object.json b/backend/schema/components/proxy-host-object.json index d8b1482fc7..40e3a3c661 100644 --- a/backend/schema/components/proxy-host-object.json +++ b/backend/schema/components/proxy-host-object.json @@ -20,6 +20,7 @@ "npmplus_directory", "npmplus_mtls_certificate_id", "npmplus_mtls_verify_client_optional", + "npmplus_mtls_bypass_auth_request", "npmplus_http3_support", "forward_scheme", "enabled", @@ -151,6 +152,9 @@ "npmplus_mtls_verify_client_optional": { "$ref": "../common.json#/properties/npmplus_mtls_verify_client_optional" }, + "npmplus_mtls_bypass_auth_request": { + "$ref": "../common.json#/properties/npmplus_mtls_bypass_auth_request" + }, "npmplus_http3_support": { "$ref": "../common.json#/properties/npmplus_http3_support" }, diff --git a/backend/schema/paths/nginx/proxy-hosts/get.json b/backend/schema/paths/nginx/proxy-hosts/get.json index 44fd37a387..040d4a0445 100644 --- a/backend/schema/paths/nginx/proxy-hosts/get.json +++ b/backend/schema/paths/nginx/proxy-hosts/get.json @@ -62,6 +62,7 @@ "npmplus_directory": "", "npmplus_mtls_certificate_id": 0, "npmplus_mtls_verify_client_optional": false, + "npmplus_mtls_bypass_auth_request": false, "npmplus_http3_support": false, "forward_scheme": "http", "enabled": true, diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/delete.json b/backend/schema/paths/nginx/proxy-hosts/hostID/delete.json index 7675accc5c..72e70ee00d 100644 --- a/backend/schema/paths/nginx/proxy-hosts/hostID/delete.json +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/delete.json @@ -59,6 +59,7 @@ "npmplus_directory": "", "npmplus_mtls_certificate_id": 0, "npmplus_mtls_verify_client_optional": false, + "npmplus_mtls_bypass_auth_request": false, "npmplus_http3_support": false, "forward_scheme": "http", "enabled": true, diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/disable/post.json b/backend/schema/paths/nginx/proxy-hosts/hostID/disable/post.json index cbb078656b..7c98b0849b 100644 --- a/backend/schema/paths/nginx/proxy-hosts/hostID/disable/post.json +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/disable/post.json @@ -59,6 +59,7 @@ "npmplus_directory": "", "npmplus_mtls_certificate_id": 0, "npmplus_mtls_verify_client_optional": false, + "npmplus_mtls_bypass_auth_request": false, "npmplus_http3_support": false, "forward_scheme": "http", "enabled": false, diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/enable/post.json b/backend/schema/paths/nginx/proxy-hosts/hostID/enable/post.json index 5caa795ac4..274afc9b51 100644 --- a/backend/schema/paths/nginx/proxy-hosts/hostID/enable/post.json +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/enable/post.json @@ -59,6 +59,7 @@ "npmplus_directory": "", "npmplus_mtls_certificate_id": 0, "npmplus_mtls_verify_client_optional": false, + "npmplus_mtls_bypass_auth_request": false, "npmplus_http3_support": false, "forward_scheme": "http", "enabled": true, diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/get.json b/backend/schema/paths/nginx/proxy-hosts/hostID/get.json index 75dfd78828..7282aeebce 100644 --- a/backend/schema/paths/nginx/proxy-hosts/hostID/get.json +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/get.json @@ -59,6 +59,7 @@ "npmplus_directory": "", "npmplus_mtls_certificate_id": 0, "npmplus_mtls_verify_client_optional": false, + "npmplus_mtls_bypass_auth_request": false, "npmplus_http3_support": false, "forward_scheme": "http", "enabled": true, diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json index 818b8a4b04..e6627222d5 100644 --- a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json @@ -107,6 +107,9 @@ "npmplus_mtls_verify_client_optional": { "$ref": "../../../../components/proxy-host-object.json#/properties/npmplus_mtls_verify_client_optional" }, + "npmplus_mtls_bypass_auth_request": { + "$ref": "../../../../components/proxy-host-object.json#/properties/npmplus_mtls_bypass_auth_request" + }, "npmplus_reuse_key": { "$ref": "../../../../components/certificate-object.json#/properties/npmplus_reuse_key" }, @@ -167,6 +170,7 @@ "npmplus_directory": "", "npmplus_mtls_certificate_id": 0, "npmplus_mtls_verify_client_optional": false, + "npmplus_mtls_bypass_auth_request": false, "npmplus_http3_support": false, "forward_scheme": "http", "enabled": true, diff --git a/backend/schema/paths/nginx/proxy-hosts/post.json b/backend/schema/paths/nginx/proxy-hosts/post.json index f42d5a6c21..6563fc8d6b 100644 --- a/backend/schema/paths/nginx/proxy-hosts/post.json +++ b/backend/schema/paths/nginx/proxy-hosts/post.json @@ -99,6 +99,9 @@ "npmplus_mtls_verify_client_optional": { "$ref": "../../../components/proxy-host-object.json#/properties/npmplus_mtls_verify_client_optional" }, + "npmplus_mtls_bypass_auth_request": { + "$ref": "../../../components/proxy-host-object.json#/properties/npmplus_mtls_bypass_auth_request" + }, "npmplus_reuse_key": { "$ref": "../../../components/certificate-object.json#/properties/npmplus_reuse_key" }, @@ -167,6 +170,7 @@ "npmplus_directory": "", "npmplus_mtls_certificate_id": 0, "npmplus_mtls_verify_client_optional": false, + "npmplus_mtls_bypass_auth_request": false, "npmplus_http3_support": false, "forward_scheme": "http", "enabled": true, diff --git a/backend/templates/proxy_host.conf b/backend/templates/proxy_host.conf index d643409cfa..1f942a5496 100644 --- a/backend/templates/proxy_host.conf +++ b/backend/templates/proxy_host.conf @@ -325,6 +325,7 @@ {% if npmplus_auth_request == "anubis" or create_anubis_locations %} location /.within.website/ { + {% if npmplus_mtls_bypass_auth_request == true %}if ($ssl_client_verify = "SUCCESS") { return 200; }{% endif %} include proxy-headers.conf; proxy_ssl_name {{ auth_request_anubis_upstream_sni_name }}; proxy_set_header Host {{ auth_request_anubis_upstream_host_header }}; @@ -345,6 +346,7 @@ {% if npmplus_auth_request == "tinyauth" or create_tinyauth_locations %} location = /tinyauth { + {% if npmplus_mtls_bypass_auth_request == true %}if ($ssl_client_verify = "SUCCESS") { return 200; }{% endif %} include proxy-headers.conf; proxy_ssl_name {{ auth_request_tinyauth_upstream_sni_name }}; proxy_set_header Host {{ auth_request_tinyauth_upstream_host_header }}; @@ -368,6 +370,7 @@ proxy_set_header X-Auth-Request-Redirect $scheme://$host$is_request_port$request_port$request_uri; } location = /oauth2/auth { + {% if npmplus_mtls_bypass_auth_request == true %}if ($ssl_client_verify = "SUCCESS") { return 200; }{% endif %} include proxy-headers.conf; proxy_ssl_name {{ auth_request_oauth2proxy_upstream_sni_name }}; proxy_set_header Host {{ auth_request_oauth2proxy_upstream_host_header }}; @@ -387,6 +390,7 @@ {% if npmplus_auth_request == "voidauth" or create_voidauth_locations %} location = /api/authz/auth-request { + {% if npmplus_mtls_bypass_auth_request == true %}if ($ssl_client_verify = "SUCCESS") { return 200; }{% endif %} include proxy-headers.conf; proxy_ssl_name {{ auth_request_voidauth_upstream_sni_name }}; proxy_set_header Host {{ auth_request_voidauth_upstream_host_header }}; @@ -403,6 +407,7 @@ {% if npmplus_auth_request == "authelia" or create_authelia_locations %} location = /internal/authelia/authz { + {% if npmplus_mtls_bypass_auth_request == true %}if ($ssl_client_verify = "SUCCESS") { return 200; }{% endif %} include proxy-headers.conf; proxy_ssl_name {{ auth_request_authelia_upstream_sni_name }}; proxy_set_header Host {{ auth_request_authelia_upstream_host_header }}; @@ -419,6 +424,7 @@ {% if npmplus_auth_request == "authentik" or npmplus_auth_request == "authentik-send-basic-auth" or create_authentik_locations %} location /outpost.goauthentik.io { + {% if npmplus_mtls_bypass_auth_request == true %}if ($ssl_client_verify = "SUCCESS") { return 200; }{% endif %} include proxy-headers.conf; proxy_ssl_name {{ auth_request_authentik_upstream_sni_name }}; proxy_set_header Host {{ auth_request_authentik_upstream_host_header }}; diff --git a/frontend/src/components/Form/SSLOptionsFields.jsx b/frontend/src/components/Form/SSLOptionsFields.jsx index cd597d51a7..1a630f82b4 100644 --- a/frontend/src/components/Form/SSLOptionsFields.jsx +++ b/frontend/src/components/Form/SSLOptionsFields.jsx @@ -4,7 +4,13 @@ import { useEffect } from "react"; import { DNSProviderFields, DomainNamesField } from "src/components"; import { T } from "src/locale"; -export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomainNames, color = "bg-cyan" }) { +export function SSLOptionsFields({ + forHttp = true, + forProxy = false, + forceDNSForNew, + requireDomainNames, + color = "bg-cyan", +}) { const { values, setFieldValue } = useFormikContext(); const v = values || {}; @@ -19,8 +25,13 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain npmplusReuseKey: reuseKey, npmplusMtlsCertificateId, npmplusMtlsVerifyClientOptional, + npmplusMtlsBypassAuthRequest, } = v; + const mtlsBypassAuthRequestPossible = + npmplusMtlsVerifyClientOptional === true && + [v, ...(v.locations ?? [])].some(({ npmplusAuthRequest = "none" }) => npmplusAuthRequest !== "none"); + useEffect(() => { if (forceDNSForNew && newCertificate && !dnsChallenge) { void setFieldValue("npmplusDnsChallenge", true); @@ -134,7 +145,7 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain return (
-
+
{({ field }) => (
+ {forProxy ? ( +
+ + {({ field }) => ( + + )} + +
+ ) : null}
{forHttp ? getHttpOptions() : null} {newCertificate ? ( diff --git a/frontend/src/hooks/useProxyHost.js b/frontend/src/hooks/useProxyHost.js index c68539461e..9dc4d48511 100644 --- a/frontend/src/hooks/useProxyHost.js +++ b/frontend/src/hooks/useProxyHost.js @@ -19,6 +19,7 @@ const fetchProxyHost = (id) => { npmplusDirectory: "", npmplusMtlsCertificateId: 0, npmplusMtlsVerifyClientOptional: false, + npmplusMtlsBypassAuthRequest: false, npmplusHttp3Support: false, forwardScheme: "", enabled: true, diff --git a/frontend/src/modals/ProxyHostModal.jsx b/frontend/src/modals/ProxyHostModal.jsx index 5d8a5583e6..af6245d2df 100644 --- a/frontend/src/modals/ProxyHostModal.jsx +++ b/frontend/src/modals/ProxyHostModal.jsx @@ -117,6 +117,7 @@ const ProxyHostModal = EasyModal.create(({ id, isClone = false, visible, remove npmplusDirectory: data?.npmplusDirectory || "", npmplusMtlsCertificateId: data?.npmplusMtlsCertificateId || 0, npmplusMtlsVerifyClientOptional: data?.npmplusMtlsVerifyClientOptional || false, + npmplusMtlsBypassAuthRequest: data?.npmplusMtlsBypassAuthRequest || false, npmplusNoindex: data?.npmplusNoindex || false, npmplusCrowdsecAppsec: data?.npmplusCrowdsecAppsec || false, npmplusProxyResponseBuffering: data?.npmplusProxyResponseBuffering || false, @@ -701,7 +702,7 @@ const ProxyHostModal = EasyModal.create(({ id, isClone = false, visible, remove allowNew /> - +
diff --git a/frontend/translations/ui/en.json b/frontend/translations/ui/en.json index 2c3926a9e4..5851ba51e1 100644 --- a/frontend/translations/ui/en.json +++ b/frontend/translations/ui/en.json @@ -120,6 +120,7 @@ "domains.hsts-subdomains": "HSTS Sub-domains", "domains.http2-support": "HTTP/2 Support", "domains.http3-support": "HTTP/3 Support", + "domains.mtls-bypass-auth-request": "mTLS bypass Auth Request", "domains.mtls-verify-client-optional": "Make mTLS Optional", "domains.reuse-key": "Reuse Key (not recommended)", "domains.trust-forwarded-proto": "Trust Upstream Forwarded Proto Headers", diff --git a/frontend/translations/ui/nl.json b/frontend/translations/ui/nl.json index 47f6d65945..ef8a83993b 100644 --- a/frontend/translations/ui/nl.json +++ b/frontend/translations/ui/nl.json @@ -120,6 +120,7 @@ "domains.hsts-subdomains": "HSTS-subdomein", "domains.http2-support": "HTTP/2-ondersteuning", "domains.http3-support": "HTTP/3-ondersteuning", + "domains.mtls-bypass-auth-request": "mTLS omzeilt Auth-verzoek", "domains.mtls-verify-client-optional": "mTLS optioneel maken", "domains.reuse-key": "Sleutel hergebruiken (niet aanbevolen)", "domains.trust-forwarded-proto": "Forwarded-Proto-headers van upstream vertrouwen", From 33fb449f776b662dc296b666554aa4a19f46e7ec Mon Sep 17 00:00:00 2001 From: Zoey Date: Thu, 8 Oct 2026 21:50:31 +0200 Subject: [PATCH 18/62] merge upstream Signed-off-by: Zoey --- frontend/translations/ui/de.json | 22 ++++++++++ frontend/translations/ui/it.json | 61 ++++++++++++++++++++++++++ rootfs/usr/local/nginx/conf/nginx.conf | 2 +- 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/frontend/translations/ui/de.json b/frontend/translations/ui/de.json index 7f1a446697..64adb87e23 100644 --- a/frontend/translations/ui/de.json +++ b/frontend/translations/ui/de.json @@ -4,6 +4,8 @@ "access-list.auth-count": "{count} {count, plural, one {User} other {Users}}", "access-list.help-rules-last": "Wenn mindestens eine Regel vorhanden ist, wird diese Regel zum Ablehnen aller Anfragen als letzte hinzugefügt.", "access-list.help.rules-order": "Beachten Sie, dass die Anweisungen „Erlauben“ und „Verbieten“ in der Reihenfolge ihrer Definition angewendet werden.", + "access-list.inherit": "Vom Proxy Host übernehmen", + "access-list.inherit.subtitle": "Verwendet die Zugriffsliste des Proxy Hosts", "access-list.pass-auth": "Authentifizierung an Upstream weiterleiten", "access-list.public": "Öffentlich", "access-list.public.subtitle": "Keine Authentifizierung erforderlich", @@ -14,6 +16,7 @@ "action.add": "Hinzufügen", "action.add-location": "Pfad hinzufügen", "action.allow": "Erlauben", + "action.clear": "Leeren", "action.close": "Schließen", "action.delete": "Löschen", "action.deny": "Verweigern", @@ -21,6 +24,7 @@ "action.download": "Herunterladen", "action.edit": "Bearbeiten", "action.enable": "Aktivieren", + "action.logs": "Logs", "action.permissions": "Berechtigungen", "action.renew": "Erneuern", "action.view-details": "Details anzeigen", @@ -69,6 +73,7 @@ "column.destination": "Ziel", "column.details": "Details", "column.email": "E-Mail", + "column.error": "Fehler", "column.event": "Ereignis", "column.expires": "Verfällt am", "column.http-code": "HTTP Code", @@ -134,12 +139,29 @@ "lets-encrypt-via-dns": "Certbot via DNS", "lets-encrypt-via-http": "Certbot via HTTP", "loading": "Laden…", + "location.advanced-config": "Hat eine eigene Nginx-Konfiguration", + "location.filter": "Nach Pfad oder Ziel filtern", "login.title": "Anmelden", "login.totp-code": "Verifizierungscode", "login.totp-code-placeholder": "Code eingeben", "login.totp-description": "Gib den Code aus deiner Authenticator-App ein", "login.totp-title": "MFA", "login.totp-verify": "Verifizieren", + "logs": "Logs", + "logs.channel.access": "Zugriff", + "logs.channel.error": "Fehler", + "logs.download": "Herunterladen", + "logs.empty": "Noch keine Log-Einträge.", + "logs.level": "Level", + "logs.level.all": "Alle Level", + "logs.lines": "Zeilen", + "logs.live": "Live", + "logs.paused": "Pausiert", + "logs.refresh": "Aktualisieren", + "logs.search-placeholder": "Log durchsuchen…", + "logs.source": "Quelle", + "logs.source.system": "System", + "logs.truncated-warning": "Es wird nur der neueste Teil dieser Datei angezeigt.", "mfa.backup-codes-remaining": "Verbleibende Backup-Codes: {count}", "mfa.backup-warning": "Bewahre diese Backup-Codes an einem sicheren Ort auf. Jeder Code kann nur einmal verwendet werden.", "mfa.regenerate": "Neu generieren", diff --git a/frontend/translations/ui/it.json b/frontend/translations/ui/it.json index 48f094cb60..5fbf0e862f 100644 --- a/frontend/translations/ui/it.json +++ b/frontend/translations/ui/it.json @@ -4,24 +4,32 @@ "access-list.auth-count": "{count} {count, plural, one {Utente} other {Utenti}}", "access-list.help-rules-last": "Quando esiste almeno 1 regola, questa regola di negazione verrà aggiunta per ultima", "access-list.help.rules-order": "Nota che le direttive di allow e deny saranno applicate nell'ordine in cui sono definite.", + "access-list.inherit": "Eredita dal Proxy Host", + "access-list.inherit.subtitle": "Usa la lista di accesso del proxy host", "access-list.pass-auth": "Passa Autenticazione all'Upstream", "access-list.public": "Accessibile Pubblicamente", "access-list.public.subtitle": "Nessuna autenticazione base richiesta", + "access-list.rule-source.placeholder": "192.168.1.100 o 192.168.1.0/24 o 2001:0db8::/32", "access-list.satisfy-any": "Soddisfa Qualsiasi", "access-list.subtitle": "{users} {users, plural, one {Utente} other {Utenti}}, {rules} {rules, plural, one {Regola} other {Regole}} - Creato: {date}", "access-lists": "Liste di Accesso", "action.add": "Aggiungi", "action.add-location": "Aggiungi Percorso", + "action.allow": "Consenti", + "action.clear": "Svuota", "action.close": "Chiudi", "action.delete": "Elimina", + "action.deny": "Nega", "action.disable": "Disabilita", "action.download": "Scarica", "action.edit": "Modifica", "action.enable": "Abilita", + "action.logs": "Log", "action.permissions": "Permessi", "action.renew": "Rinnova", "action.view-details": "Visualizza Dettagli", "auditlogs": "Log di Audit", + "auto": "Auto", "cancel": "Annulla", "certificate": "Certificato", "certificate.custom-certificate": "Certificato", @@ -42,6 +50,7 @@ "certificates.dns.propagation-seconds": "Secondi di Propagazione", "certificates.dns.propagation-seconds-note": "Lascia vuoto per usare il valore predefinito del plugin. Numero di secondi da attendere per la propagazione DNS.", "certificates.dns.provider": "Provider DNS", + "certificates.dns.provider.placeholder": "Seleziona un Provider...", "certificates.dns.warning": "Questa sezione richiede conoscenze su Certbot e i relativi plugin DNS. Consulta la documentazione del plugin.", "certificates.http.reachability-404": "È stato trovato un server su questo dominio, ma non sembra essere NPMplus. Assicurati che il dominio punti all'IP dove è in esecuzione NPMplus.", "certificates.http.reachability-failed-to-check": "Verifica di raggiungibilità fallita per errore di comunicazione con site24x7.com.", @@ -64,6 +73,7 @@ "column.destination": "Destinazione", "column.details": "Dettagli", "column.email": "Email", + "column.error": "Errore", "column.event": "Evento", "column.expires": "Scadenza", "column.http-code": "Codice HTTP", @@ -91,10 +101,12 @@ "domain-names.placeholder": "Inizia a digitare per aggiungere un dominio...", "domain-names.wildcards-not-permitted": "Wildcard non consentite per questo tipo", "domain-names.wildcards-not-supported": "Wildcard non supportate per questa CA", + "domains.advanced": "Avanzate", "domains.force-ssl": "Forza HTTPS", "domains.hsts-enabled": "HSTS Abilitato", "domains.hsts-subdomains": "Sottodomini HSTS", "domains.http2-support": "Supporto HTTP/2", + "domains.trust-forwarded-proto": "Considera Attendibili gli Header Forwarded Proto dell'Upstream", "domains.use-dns": "Usa Challenge DNS", "email-address": "Indirizzo Email", "empty-search": "Nessun risultato trovato", @@ -127,7 +139,35 @@ "lets-encrypt-via-dns": "Certbot via DNS", "lets-encrypt-via-http": "Certbot via HTTP", "loading": "Caricamento…", + "location.advanced-config": "Ha una configurazione Nginx personalizzata", + "location.filter": "Filtra per percorso o destinazione", "login.title": "Accedi al tuo account", + "login.totp-code": "Codice di Verifica", + "login.totp-code-placeholder": "Inserisci il codice", + "login.totp-description": "Inserisci il codice della tua app di autenticazione", + "login.totp-title": "MFA", + "login.totp-verify": "Verifica", + "logs": "Log", + "logs.channel.access": "Accessi", + "logs.channel.error": "Errori", + "logs.download": "Scarica", + "logs.empty": "Nessuna voce di log per ora.", + "logs.level": "Livello", + "logs.level.all": "Tutti i Livelli", + "logs.lines": "Righe", + "logs.live": "Live", + "logs.paused": "In Pausa", + "logs.refresh": "Aggiorna", + "logs.search-placeholder": "Cerca nel log…", + "logs.source": "Origine", + "logs.source.system": "Sistema", + "logs.truncated-warning": "Viene mostrata solo la parte più recente di questo file.", + "mfa.backup-codes-remaining": "Codici di backup rimanenti: {count}", + "mfa.backup-warning": "Conserva questi codici di backup in un luogo sicuro. Ogni codice può essere usato una sola volta.", + "mfa.regenerate": "Rigenera", + "mfa.regenerate-backup": "Rigenera Codici di Backup", + "mfa.regenerate-instructions": "Inserisci un codice di verifica per generare nuovi codici di backup. I vecchi codici non saranno più validi.", + "mfa.title": "MFA", "nginx-config.label": "Configurazione Nginx Personalizzata", "nginx-config.placeholder": "# Inserisci qui la configurazione Nginx personalizzata a tuo rischio!", "no-permission-error": "Non hai accesso per visualizzare questa pagina.", @@ -176,6 +216,12 @@ "redirection-host.forward-http-code": "Codice HTTP", "redirection-hosts": "Host di Reindirizzamento", "redirection-hosts.count": "{count} {count, plural, one {Host di Reindirizzamento} other {Host di Reindirizzamento}}", + "redirection-hosts.http-code.300": "300 Scelte multiple", + "redirection-hosts.http-code.301": "301 Spostato permanentemente", + "redirection-hosts.http-code.302": "302 Spostato temporaneamente", + "redirection-hosts.http-code.303": "303 Vedi altro", + "redirection-hosts.http-code.307": "307 Reindirizzamento temporaneo", + "redirection-hosts.http-code.308": "308 Reindirizzamento permanente", "role.admin": "Amministratore", "role.standard-user": "Utente Standard", "save": "Salva", @@ -195,12 +241,26 @@ "ssl-certificate": "Certificato TLS", "stream": "Stream", "stream.forward-host": "Host di Destinazione", + "stream.forward-host.placeholder": "example.com o 10.0.0.1 o 2001:db8:3333:4444:5555:6666:7777:8888", "stream.incoming-port": "Porta in Ingresso", "streams": "Stream", "streams.count": "{count} {count, plural, one {Stream} other {Stream}}", "streams.tcp": "TCP", "streams.udp": "UDP", "test": "Test", + "totp.disable": "Disattiva TOTP", + "totp.disable-confirm": "Disattiva TOTP", + "totp.disable-warning": "Disattivare TOTP renderà il tuo account meno sicuro.", + "totp.disabled": "Disattivata", + "totp.done": "Ho salvato i miei codici di backup", + "totp.enable": "Attiva TOTP", + "totp.enabled": "Attivata", + "totp.enter-code": "Inserisci il codice di verifica", + "totp.enter-code-disable": "Inserisci il codice di verifica per disattivarla", + "totp.secret-key": "Chiave Segreta", + "totp.setup-instructions": "Scansiona questo codice QR con la tua app di autenticazione, oppure inserisci la chiave segreta manualmente.", + "totp.status": "Stato", + "totp.verify-enable": "Verifica e Attiva", "update-available": "Aggiornamento Disponibile: {latestVersion}", "user": "Utente", "user.change-password": "Cambia Password", @@ -210,6 +270,7 @@ "user.full-name": "Nome Completo", "user.login-as": "Accedi come {name}", "user.logout": "Disconnetti", + "user.mfa": "MFA", "user.new-password": "Nuova Password", "user.nickname": "Soprannome", "user.set-password": "Imposta Password", diff --git a/rootfs/usr/local/nginx/conf/nginx.conf b/rootfs/usr/local/nginx/conf/nginx.conf index 7626274957..f380a3d880 100644 --- a/rootfs/usr/local/nginx/conf/nginx.conf +++ b/rootfs/usr/local/nginx/conf/nginx.conf @@ -252,7 +252,7 @@ stream { # Custom include /data/custom_nginx/stream_top.conf; - # Files generated by NPM + # Files generated by NPMplus include /data/nginx/stream/*.conf; # Custom From ac19efbfefeef2d3d65c9c668c9fbd44628112bf Mon Sep 17 00:00:00 2001 From: Zoey Date: Thu, 8 Oct 2026 22:53:58 +0200 Subject: [PATCH 19/62] sort hosts by enabled state Signed-off-by: Zoey --- frontend/src/pages/Nginx/DeadHosts/TableWrapper.jsx | 2 +- frontend/src/pages/Nginx/ProxyHosts/TableWrapper.jsx | 2 +- frontend/src/pages/Nginx/RedirectionHosts/TableWrapper.jsx | 2 +- frontend/src/pages/Nginx/Streams/TableWrapper.jsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/Nginx/DeadHosts/TableWrapper.jsx b/frontend/src/pages/Nginx/DeadHosts/TableWrapper.jsx index f91e8d6048..08e5125e03 100644 --- a/frontend/src/pages/Nginx/DeadHosts/TableWrapper.jsx +++ b/frontend/src/pages/Nginx/DeadHosts/TableWrapper.jsx @@ -14,7 +14,7 @@ import Table from "./Table"; export default function TableWrapper() { const queryClient = useQueryClient(); const [search, setSearch] = useState(""); - const [sorting, setSorting] = useState([]); + const [sorting, setSorting] = useState([{ id: "enabled", desc: false }]); const { isFetching, isLoading, isError, error, data } = useDeadHosts(["owner", "certificate"]); useEffect(() => { diff --git a/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.jsx b/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.jsx index 64ff78b1d4..a04fe8d92f 100644 --- a/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.jsx +++ b/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.jsx @@ -14,7 +14,7 @@ import Table from "./Table"; export default function TableWrapper() { const queryClient = useQueryClient(); const [search, setSearch] = useState(""); - const [sorting, setSorting] = useState([]); + const [sorting, setSorting] = useState([{ id: "enabled", desc: false }]); const { isFetching, isLoading, isError, error, data } = useProxyHosts(["owner", "access_lists", "certificate"]); useEffect(() => { diff --git a/frontend/src/pages/Nginx/RedirectionHosts/TableWrapper.jsx b/frontend/src/pages/Nginx/RedirectionHosts/TableWrapper.jsx index 8605c3dbe4..8167e1a30e 100644 --- a/frontend/src/pages/Nginx/RedirectionHosts/TableWrapper.jsx +++ b/frontend/src/pages/Nginx/RedirectionHosts/TableWrapper.jsx @@ -14,7 +14,7 @@ import Table from "./Table"; export default function TableWrapper() { const queryClient = useQueryClient(); const [search, setSearch] = useState(""); - const [sorting, setSorting] = useState([]); + const [sorting, setSorting] = useState([{ id: "enabled", desc: false }]); const { isFetching, isLoading, isError, error, data } = useRedirectionHosts(["owner", "certificate"]); useEffect(() => { diff --git a/frontend/src/pages/Nginx/Streams/TableWrapper.jsx b/frontend/src/pages/Nginx/Streams/TableWrapper.jsx index 98503a5f4c..d3be30f42f 100644 --- a/frontend/src/pages/Nginx/Streams/TableWrapper.jsx +++ b/frontend/src/pages/Nginx/Streams/TableWrapper.jsx @@ -14,7 +14,7 @@ import Table from "./Table"; export default function TableWrapper() { const queryClient = useQueryClient(); const [search, setSearch] = useState(""); - const [sorting, setSorting] = useState([]); + const [sorting, setSorting] = useState([{ id: "enabled", desc: false }]); const { isFetching, isLoading, isError, error, data } = useStreams(["owner", "certificate"]); useEffect(() => { From 717877db3feea710eb45b3dddc7da342b9d61d17 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Fri, 9 Oct 2026 21:21:13 +1000 Subject: [PATCH 20/62] Fix flakey cypress test --- test/cypress/e2e/api/AccessListHostToggle.cy.js | 4 ++++ test/cypress/plugins/backendApi/client.mjs | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/test/cypress/e2e/api/AccessListHostToggle.cy.js b/test/cypress/e2e/api/AccessListHostToggle.cy.js index b566a3a98f..ee1bee0dcc 100644 --- a/test/cypress/e2e/api/AccessListHostToggle.cy.js +++ b/test/cypress/e2e/api/AccessListHostToggle.cy.js @@ -192,7 +192,11 @@ describe('Access Lists across Proxy Host disable/enable', () => { expect(data).to.be.equal(true); }); + // Each delete signals an nginx reload and returns immediately. A request + // made while the workers are being swapped can be reset by the nginx + // serving the API (502 from squid), so let each reload settle first. [authListId, clientListId].forEach((id) => { + cy.wait(1500); cy.task('backendApiDelete', { token: token, path: `/api/nginx/access-lists/${id}`, diff --git a/test/cypress/plugins/backendApi/client.mjs b/test/cypress/plugins/backendApi/client.mjs index 78618abb26..5c0daa5c09 100644 --- a/test/cypress/plugins/backendApi/client.mjs +++ b/test/cypress/plugins/backendApi/client.mjs @@ -1,3 +1,4 @@ +import http from "node:http"; import axios from "axios"; import logger from "./logger.mjs"; @@ -8,6 +9,10 @@ const BackendApi = function (config, token) { this.axios = axios.create({ baseURL: config.baseUrl, timeout: 90000, + // Don't reuse sockets: a pooled keep-alive connection that squid or the + // backend has since closed fails with a 502 (ERR_READ_ERROR 104), which + // made tests flaky. + httpAgent: new http.Agent({ keepAlive: false }), }); }; From c874001f020e160d5b09b1647e0f84a41f4e37fb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 02:26:32 +0000 Subject: [PATCH 21/62] dep updates --- backend/package.json | 4 +-- backend/pnpm-lock.yaml | 58 ++++++++++++++++++----------------------- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 18 ++++++------- 4 files changed, 38 insertions(+), 44 deletions(-) diff --git a/backend/package.json b/backend/package.json index 04dda229e7..f8dda7f9d3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,14 +14,14 @@ "better-sqlite3": "13.0.3", "cookie-parser": "1.4.7", "dayjs": "1.11.23", - "express": "5.2.1", + "express": "5.3.0", "express-rate-limit": "8.7.1", "jsonwebtoken": "9.0.3", "knex": "3.3.0", "liquidjs": "10.30.0", "multer": "2.4.0", "mysql2": "3.24.5", - "objection": "3.1.5", + "objection": "3.5.0", "openid-client": "6.8.8", "otplib": "13.5.0", "pg": "8.23.1", diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index ad10b56342..f841547ddb 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -30,11 +30,11 @@ importers: specifier: 1.11.23 version: 1.11.23 express: - specifier: 5.2.1 - version: 5.2.1 + specifier: 5.3.0 + version: 5.3.0 express-rate-limit: specifier: 8.7.1 - version: 8.7.1(express@5.2.1) + version: 8.7.1(express@5.3.0) jsonwebtoken: specifier: 9.0.3 version: 9.0.3 @@ -51,8 +51,8 @@ importers: specifier: 3.24.5 version: 3.24.5(@types/node@26.6.4) objection: - specifier: 3.1.5 - version: 3.1.5(knex@3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.5(@types/node@26.6.4))(pg@8.23.1)) + specifier: 3.5.0 + version: 3.5.0(knex@3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.5(@types/node@26.6.4))(pg@8.23.1)) openid-client: specifier: 6.8.8 version: 6.8.8 @@ -64,7 +64,7 @@ importers: version: 8.23.1 swagger-ui-express: specifier: 5.0.1 - version: 5.0.1(express@5.2.1) + version: 5.0.1(express@5.3.0) undici: specifier: 8.11.2 version: 8.11.2 @@ -204,8 +204,8 @@ packages: ajv: optional: true - ajv-formats@2.1.1: - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: ajv: ^8.0.0 peerDependenciesMeta: @@ -340,14 +340,10 @@ packages: resolution: {integrity: sha512-g0S8KAD8qf4+V//pr3BfB1aBnARLXNz2Gx+jmHU0LEriUuoQUOPOulVquHKTJ8+EAIIO7fhseNDr9wK5Q9FKBQ==} engines: {node: '>=18'} - content-disposition@1.1.0: - resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + content-disposition@2.0.1: + resolution: {integrity: sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==} engines: {node: '>=18'} - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - content-type@2.1.0: resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} engines: {node: '>=18'} @@ -465,8 +461,8 @@ packages: peerDependencies: express: '>= 4.11' - express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + express@5.3.0: + resolution: {integrity: sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==} engines: {node: '>= 18'} fast-deep-equal@3.1.3: @@ -735,11 +731,11 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} - objection@3.1.5: - resolution: {integrity: sha512-Hx/ipAwXSuRBbOMWFKtRsAN0yITafqXtWB4OT4Z9wED7ty1h7bOnBdhLtcNus23GwLJqcMsRWdodL2p5GwlnfQ==} + objection@3.5.0: + resolution: {integrity: sha512-uHKovRPeQX/h6u3yyBB+rWary9Q/Phr6epgiotioqTuive4PjWOyRvjrRT9GALetQCNMvcrymXy5s44AbsQLpQ==} engines: {node: '>=14.0.0'} peerDependencies: - knex: '>=1.0.1' + knex: '>=2.3.0' on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} @@ -1122,7 +1118,7 @@ snapshots: optionalDependencies: ajv: 8.20.0 - ajv-formats@2.1.1(ajv@8.20.0): + ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 @@ -1255,9 +1251,7 @@ snapshots: normalize-path: 3.0.0 readable-stream: 4.7.0 - content-disposition@1.1.0: {} - - content-type@1.0.5: {} + content-disposition@2.0.1: {} content-type@2.1.0: {} @@ -1335,20 +1329,20 @@ snapshots: events@3.3.0: {} - express-rate-limit@8.7.1(express@5.2.1): + express-rate-limit@8.7.1(express@5.3.0): dependencies: debug: 4.4.3 - express: 5.2.1 + express: 5.3.0 ip-address: 10.7.3 transitivePeerDependencies: - supports-color - express@5.2.1: + express@5.3.0: dependencies: accepts: 2.0.0 body-parser: 2.3.0 - content-disposition: 1.1.0 - content-type: 1.0.5 + content-disposition: 2.0.1 + content-type: 2.1.0 cookie: 0.7.2 cookie-signature: 1.2.2 debug: 4.4.3 @@ -1611,10 +1605,10 @@ snapshots: object-inspect@1.13.4: {} - objection@3.1.5(knex@3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.5(@types/node@26.6.4))(pg@8.23.1)): + objection@3.5.0(knex@3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.5(@types/node@26.6.4))(pg@8.23.1)): dependencies: ajv: 8.20.0 - ajv-formats: 2.1.1(ajv@8.20.0) + ajv-formats: 3.0.1(ajv@8.20.0) db-errors: 0.2.3 knex: 3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.5(@types/node@26.6.4))(pg@8.23.1) @@ -1859,9 +1853,9 @@ snapshots: dependencies: '@scarf/scarf': 1.4.0 - swagger-ui-express@5.0.1(express@5.2.1): + swagger-ui-express@5.0.1(express@5.3.0): dependencies: - express: 5.2.1 + express: 5.3.0 swagger-ui-dist: 5.33.1 tar-stream@3.2.2: diff --git a/frontend/package.json b/frontend/package.json index c5b7a65ec1..bc2cd65a22 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,7 @@ "@tabler/core": "1.6.1", "@tabler/icons-react": "3.49.0", "@tanstack/react-query": "5.104.1", - "@tanstack/react-table": "9.2.6", + "@tanstack/react-table": "9.2.8", "clsx": "2.1.1", "country-flag-icons": "1.6.20", "date-fns": "4.4.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index ac9d3f5f49..d1396419b2 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -21,8 +21,8 @@ importers: specifier: 5.104.1 version: 5.104.1(react@19.3.0) '@tanstack/react-table': - specifier: 9.2.6 - version: 9.2.6(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + specifier: 9.2.8 + version: 9.2.8(react-dom@19.3.0(react@19.3.0))(react@19.3.0) clsx: specifier: 2.1.1 version: 2.1.1 @@ -447,8 +447,8 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/react-table@9.2.6': - resolution: {integrity: sha512-Wmt3sbzwz+5CtB0pqIxGw2FrNW9mF4AhJ9KnS1/VGZ8cDAEg51S4ixru2dnF0xQnY407kdYNLer3ms+n8zCN6w==} + '@tanstack/react-table@9.2.8': + resolution: {integrity: sha512-cJxgratJm3uMm2sXnmY3PCTvcvIEFleCJ2uSOM2nxCmIY8RWA1KjXt5fsWGlYisL7DODr2w9YpLRQ3i/7MdIsQ==} engines: {node: '>=20'} peerDependencies: react: '>=18' @@ -456,8 +456,8 @@ packages: '@tanstack/store@0.11.2': resolution: {integrity: sha512-sJ4mjol8uQsHV0gOJzzjwXfh2Fwm+Sz0+8deqiTm4jGbMdjzNSW+xZCFm0kUa870uhd8yi+DpKnZb5Kc4apa0Q==} - '@tanstack/table-core@9.2.6': - resolution: {integrity: sha512-t5LaLx8gDCgzjvwtZo5xtLklJP11D9zaKOW8q6jOCyuQYhkqpTK1heqMfgTQwDlhl0dU8f1X66Xurbj3jpCUSw==} + '@tanstack/table-core@9.2.8': + resolution: {integrity: sha512-M4D6fY5jnc7z7TaoD3Pjvz5R+8MbhzCGYdkcFf2T1AgjigwLinvFWZh6mtqfN016A7yGIroOL0GiTlVGsTh6Vg==} engines: {node: '>=20'} '@types/hoist-non-react-statics@3.3.7': @@ -1352,17 +1352,17 @@ snapshots: react-dom: 19.3.0(react@19.3.0) use-sync-external-store: 1.7.0(react@19.3.0) - '@tanstack/react-table@9.2.6(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + '@tanstack/react-table@9.2.8(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': dependencies: '@tanstack/react-store': 0.11.2(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - '@tanstack/table-core': 9.2.6 + '@tanstack/table-core': 9.2.8 react: 19.3.0 transitivePeerDependencies: - react-dom '@tanstack/store@0.11.2': {} - '@tanstack/table-core@9.2.6': + '@tanstack/table-core@9.2.8': dependencies: '@tanstack/store': 0.11.2 From 4d2c725513ffeb3e76bf4dac4b8972cb22d51c43 Mon Sep 17 00:00:00 2001 From: Zoey Date: Thu, 8 Oct 2026 21:54:46 +0200 Subject: [PATCH 22/62] experimental testing: apply nginx "QUIC performance optimization" PR likely to be reverted/removed soon from NPMplus Signed-off-by: Zoey --- Dockerfile | 3 +++ rootfs/usr/local/nginx/conf/nginx.conf | 6 ++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index d5f6b95b2b..2d014091d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -94,6 +94,9 @@ RUN git-clone-commit.sh https://github.com/nginx/nginx "$NGINX_VER" /src/nginx & git apply /src/nginx-ech-boringssl-awslc.patch && \ git apply /src/nginx-cert-compression-brotli.patch && \ \ + wget -q https://patch-diff.githubusercontent.com/raw/nginx/nginx/pull/1825.patch -O /src/nginx/1825.patch && \ + git apply /src/nginx/1825.patch && \ + \ git-clone-commit.sh https://github.com/HanadaLee/ngx_http_brotli_module "$NB_VER" /src/ngx_http_brotli_module && \ git-clone-commit.sh https://github.com/HanadaLee/ngx_http_unbrotli_filter_module "$NUB_VER" /src/ngx_http_unbrotli_filter_module && \ git-clone-commit.sh https://github.com/hsw/zstd-nginx-module "$ZNM_VER" /src/zstd-nginx-module && \ diff --git a/rootfs/usr/local/nginx/conf/nginx.conf b/rootfs/usr/local/nginx/conf/nginx.conf index f380a3d880..1ff7f864d6 100644 --- a/rootfs/usr/local/nginx/conf/nginx.conf +++ b/rootfs/usr/local/nginx/conf/nginx.conf @@ -52,11 +52,9 @@ http { log_not_found off; http2 on; - http3 on; - quic_gso on; - quic_retry on; client_body_buffer_size 1m; - http3_stream_buffer_size 1m; + + quic_retry on; ssl_dyn_rec_enable on; ssl_session_timeout 1d; From 73c13b4ffacc086d121368dd8b6e68c6fe510a1b Mon Sep 17 00:00:00 2001 From: Zoey Date: Sat, 10 Oct 2026 13:58:45 +0200 Subject: [PATCH 23/62] Revert "experimental testing: apply nginx "QUIC performance optimization" PR" This reverts commit 4d2c725513ffeb3e76bf4dac4b8972cb22d51c43. --- Dockerfile | 3 --- rootfs/usr/local/nginx/conf/nginx.conf | 6 ++++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2d014091d7..d5f6b95b2b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -94,9 +94,6 @@ RUN git-clone-commit.sh https://github.com/nginx/nginx "$NGINX_VER" /src/nginx & git apply /src/nginx-ech-boringssl-awslc.patch && \ git apply /src/nginx-cert-compression-brotli.patch && \ \ - wget -q https://patch-diff.githubusercontent.com/raw/nginx/nginx/pull/1825.patch -O /src/nginx/1825.patch && \ - git apply /src/nginx/1825.patch && \ - \ git-clone-commit.sh https://github.com/HanadaLee/ngx_http_brotli_module "$NB_VER" /src/ngx_http_brotli_module && \ git-clone-commit.sh https://github.com/HanadaLee/ngx_http_unbrotli_filter_module "$NUB_VER" /src/ngx_http_unbrotli_filter_module && \ git-clone-commit.sh https://github.com/hsw/zstd-nginx-module "$ZNM_VER" /src/zstd-nginx-module && \ diff --git a/rootfs/usr/local/nginx/conf/nginx.conf b/rootfs/usr/local/nginx/conf/nginx.conf index 1ff7f864d6..f380a3d880 100644 --- a/rootfs/usr/local/nginx/conf/nginx.conf +++ b/rootfs/usr/local/nginx/conf/nginx.conf @@ -52,9 +52,11 @@ http { log_not_found off; http2 on; - client_body_buffer_size 1m; - + http3 on; + quic_gso on; quic_retry on; + client_body_buffer_size 1m; + http3_stream_buffer_size 1m; ssl_dyn_rec_enable on; ssl_session_timeout 1d; From 56fac4846d4737cfa70513b02a0d2702457f120b Mon Sep 17 00:00:00 2001 From: Zoey Date: Sat, 10 Oct 2026 14:54:08 +0200 Subject: [PATCH 24/62] do not soft delete --- backend/internal/access-list.js | 29 +++------ backend/internal/certificate.js | 18 ++---- backend/internal/dead-host.js | 14 +---- backend/internal/host.js | 12 +--- backend/internal/proxy-host-access-list.js | 9 +-- backend/internal/proxy-host.js | 15 ++--- backend/internal/redirection-host.js | 14 +---- backend/internal/stream.js | 14 +---- backend/internal/token.js | 9 +-- backend/internal/user.js | 31 +++++---- backend/lib/access.js | 1 - backend/lib/helpers.js | 10 ++- .../migrations/20261010135653_hard_delete.js | 63 +++++++++++++++++++ backend/models/access_list.js | 6 -- backend/models/access_list_auth.js | 3 - backend/models/access_list_client.js | 3 - backend/models/auth.js | 3 - backend/models/certificate.js | 27 -------- backend/models/dead_host.js | 6 -- backend/models/proxy_host.js | 9 --- backend/models/redirection_host.js | 6 -- backend/models/stream.js | 6 -- backend/password-reset.js | 2 +- backend/setup.js | 25 ++------ frontend/src/pages/Access/Table.jsx | 9 ++- frontend/src/pages/AuditLog/Table.jsx | 7 ++- frontend/src/pages/Certificates/Table.jsx | 9 ++- frontend/src/pages/Nginx/DeadHosts/Table.jsx | 9 ++- frontend/src/pages/Nginx/ProxyHosts/Table.jsx | 9 ++- .../pages/Nginx/RedirectionHosts/Table.jsx | 9 ++- frontend/src/pages/Nginx/Streams/Table.jsx | 9 ++- frontend/translations/ui/en.json | 1 + frontend/translations/ui/nl.json | 1 + 33 files changed, 181 insertions(+), 217 deletions(-) create mode 100644 backend/migrations/20261010135653_hard_delete.js diff --git a/backend/internal/access-list.js b/backend/internal/access-list.js index 69f296426c..646d9d0fa2 100644 --- a/backend/internal/access-list.js +++ b/backend/internal/access-list.js @@ -206,15 +206,8 @@ const internalAccessList = { "npmplus_proxy_host_access_list.access_list_id", "access_list.id", ) - .leftJoin("proxy_host", function () { - this.on("proxy_host.id", "=", "npmplus_proxy_host_access_list.proxy_host_id").andOn( - "proxy_host.is_deleted", - "=", - 0, - ); - }) - .where("access_list.is_deleted", 0) - .andWhere("access_list.id", thisData.id) + .leftJoin("proxy_host", "proxy_host.id", "npmplus_proxy_host_access_list.proxy_host_id") + .where("access_list.id", thisData.id) .groupBy("access_list.id") .allowGraph("[items,clients,proxy_hosts.[certificate,access_lists.[clients,items]]]") .first(); @@ -259,8 +252,11 @@ const internalAccessList = { // 4. audit log // 1. update row to be deleted - await accessListModel.query().where("id", row.id).patch({ - is_deleted: 1, + await accessListModel.transaction(async (trx) => { + await trx("npmplus_proxy_host_access_list").where("access_list_id", row.id).delete(); + await accessListAuthModel.query(trx).where("access_list_id", row.id).delete(); + await accessListClientModel.query(trx).where("access_list_id", row.id).delete(); + await accessListModel.query(trx).deleteById(row.id); }); // 2. update any proxy hosts that were using it (ignoring permissions) @@ -367,14 +363,7 @@ const internalAccessList = { "npmplus_proxy_host_access_list.access_list_id", "access_list.id", ) - .leftJoin("proxy_host", function () { - this.on("proxy_host.id", "=", "npmplus_proxy_host_access_list.proxy_host_id").andOn( - "proxy_host.is_deleted", - "=", - 0, - ); - }) - .where("access_list.is_deleted", 0) + .leftJoin("proxy_host", "proxy_host.id", "npmplus_proxy_host_access_list.proxy_host_id") .groupBy("access_list.id") .allowGraph("[owner,items,clients]") .orderBy("access_list.name", "ASC"); @@ -405,7 +394,7 @@ const internalAccessList = { * @returns {Promise} */ getCount: async (userId, visibility) => { - const query = accessListModel.query().count("id as count").where("is_deleted", 0); + const query = accessListModel.query().count("id as count"); if (visibility !== "all") { query.andWhere("owner_user_id", userId); diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 4cec0c5320..4249efddba 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -73,10 +73,7 @@ const internalCertificate = { logger.error(err); } - const certificates = await certificateModel - .query() - .where("is_deleted", 0) - .andWhere("provider", "letsencrypt"); + const certificates = await certificateModel.query().where("provider", "letsencrypt"); if (certificates && certificates.length > 0) { const updatePromises = certificates.map(async (certificate) => { @@ -215,7 +212,7 @@ const internalCertificate = { */ get: async (access, data) => { access.can("certificates:view"); - const query = certificateModel.query().where("is_deleted", 0).andWhere("id", data.id).first(); + const query = certificateModel.query().where("id", data.id).first(); if (access.visibility !== "all") { query.andWhere("owner_user_id", access.token.getUserId(1)); @@ -313,17 +310,15 @@ const internalCertificate = { if ( (await hostModel .query() - .where("is_deleted", 0) - .andWhere((qb) => qb.where("certificate_id", row.id).orWhere("npmplus_mtls_certificate_id", row.id)) + .where("certificate_id", row.id) + .orWhere("npmplus_mtls_certificate_id", row.id) .resultSize()) > 0 ) { throw new error.ValidationError("Certificate is still in use"); } } - await certificateModel.query().where("id", row.id).patch({ - is_deleted: 1, - }); + await certificateModel.query().deleteById(row.id); // Add to audit log await internalAuditLog.add(access, { @@ -358,7 +353,6 @@ const internalCertificate = { const query = certificateModel .query() - .where("is_deleted", 0) .groupBy("id") .allowGraph( "[owner,proxy_hosts,redirection_hosts,dead_hosts,streams,mtls_proxy_hosts,mtls_redirection_hosts,mtls_dead_hosts,mtls_streams]", @@ -391,7 +385,7 @@ const internalCertificate = { * @returns {Promise} */ getCount: async (userId, visibility) => { - const query = certificateModel.query().count("id as count").where("is_deleted", 0); + const query = certificateModel.query().count("id as count"); if (visibility !== "all") { query.andWhere("owner_user_id", userId); diff --git a/backend/internal/dead-host.js b/backend/internal/dead-host.js index 9d1a1accd2..e6f12f9af7 100644 --- a/backend/internal/dead-host.js +++ b/backend/internal/dead-host.js @@ -170,12 +170,7 @@ const internalDeadHost = { access.can("dead_hosts:view"); - const query = deadHostModel - .query() - .where("is_deleted", 0) - .andWhere("id", thisData.id) - .allowGraph("[certificate]") - .first(); + const query = deadHostModel.query().where("id", thisData.id).allowGraph("[certificate]").first(); if (access.visibility !== "all") { query.andWhere("owner_user_id", access.token.getUserId(1)); @@ -208,9 +203,7 @@ const internalDeadHost = { throw new errs.ItemNotFoundError(data.id); } - await deadHostModel.query().where("id", row.id).patch({ - is_deleted: 1, - }); + await deadHostModel.query().deleteById(row.id); try { // Delete Nginx Config @@ -340,7 +333,6 @@ const internalDeadHost = { const query = deadHostModel .query() - .where("is_deleted", 0) .groupBy("id") .allowGraph("[owner,certificate]") .orderBy(castJsonIfNeed("domain_names"), "ASC"); @@ -371,7 +363,7 @@ const internalDeadHost = { * @returns {Promise} */ getCount: async (user_id, visibility) => { - const query = deadHostModel.query().count("id as count").where("is_deleted", 0); + const query = deadHostModel.query().count("id as count"); if (visibility !== "all") { query.andWhere("owner_user_id", user_id); diff --git a/backend/internal/host.js b/backend/internal/host.js index e427659483..3ee06709e0 100644 --- a/backend/internal/host.js +++ b/backend/internal/host.js @@ -67,19 +67,13 @@ const internalHost = { const promises = [ proxyHostModel .query() - .where("is_deleted", 0) - .andWhere("enabled", 1) + .where("enabled", 1) .andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`), redirectionHostModel .query() - .where("is_deleted", 0) - .andWhere("enabled", 1) - .andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`), - deadHostModel - .query() - .where("is_deleted", 0) - .andWhere("enabled", 1) + .where("enabled", 1) .andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`), + deadHostModel.query().where("enabled", 1).andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`), ]; const promises_results = await Promise.all(promises); diff --git a/backend/internal/proxy-host-access-list.js b/backend/internal/proxy-host-access-list.js index 470347eb60..b30ae7462b 100644 --- a/backend/internal/proxy-host-access-list.js +++ b/backend/internal/proxy-host-access-list.js @@ -322,11 +322,7 @@ const internalProxyHostAccessList = { return proxyHost; } - const rows = await accessListModel - .query() - .whereIn("id", allIds) - .andWhere("is_deleted", 0) - .withGraphFetched("[clients,items]"); + const rows = await accessListModel.query().whereIn("id", allIds).withGraphFetched("[clients,items]"); const byId = new Map(rows.map((row) => [row.id, row])); @@ -375,12 +371,11 @@ const internalProxyHostAccessList = { validateCustomSelection(location.npmplus_access_list_type, location.npmplus_access_list_ids); } } - // make sure no ACLs that are being uploaded have been soft deleted + // make sure no ACLs that are being uploaded have been deleted if (aclIds.size > 0) { const query = accessListModel .query() .whereIn("id", [...aclIds]) - .andWhere("is_deleted", 0) .select("id"); if (access.visibility !== "all") { diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js index f4eb4ddb00..7c6600be54 100644 --- a/backend/internal/proxy-host.js +++ b/backend/internal/proxy-host.js @@ -200,8 +200,7 @@ const internalProxyHost = { const query = proxyHostModel .query() - .where("is_deleted", 0) - .andWhere("id", thisData.id) + .where("id", thisData.id) .allowGraph("[access_lists.[clients,items],certificate]") .first(); @@ -236,11 +235,10 @@ const internalProxyHost = { throw new errs.ItemNotFoundError(data.id); } - await proxyHostModel.transaction((trx) => - proxyHostModel.query(trx).where("id", row.id).patch({ - is_deleted: 1, - }), - ); + await proxyHostModel.transaction(async (trx) => { + await trx("npmplus_proxy_host_access_list").where("proxy_host_id", row.id).delete(); + await proxyHostModel.query(trx).deleteById(row.id); + }); try { // Delete Nginx Config @@ -378,7 +376,6 @@ const internalProxyHost = { const query = proxyHostModel .query() - .where("is_deleted", 0) .groupBy("id") .allowGraph("[owner,access_lists,certificate]") .orderBy(castJsonIfNeed("domain_names"), "ASC"); @@ -409,7 +406,7 @@ const internalProxyHost = { * @returns {Promise} */ getCount: async (user_id, visibility) => { - const query = proxyHostModel.query().count("id as count").where("is_deleted", 0); + const query = proxyHostModel.query().count("id as count"); if (visibility !== "all") { query.andWhere("owner_user_id", user_id); diff --git a/backend/internal/redirection-host.js b/backend/internal/redirection-host.js index 6a84eadada..159152d30c 100644 --- a/backend/internal/redirection-host.js +++ b/backend/internal/redirection-host.js @@ -170,12 +170,7 @@ const internalRedirectionHost = { access.can("redirection_hosts:view"); - const query = redirectionHostModel - .query() - .where("is_deleted", 0) - .andWhere("id", thisData.id) - .allowGraph("[certificate]") - .first(); + const query = redirectionHostModel.query().where("id", thisData.id).allowGraph("[certificate]").first(); if (access.visibility !== "all") { query.andWhere("owner_user_id", access.token.getUserId(1)); @@ -208,9 +203,7 @@ const internalRedirectionHost = { throw new errs.ItemNotFoundError(data.id); } - await redirectionHostModel.query().where("id", row.id).patch({ - is_deleted: 1, - }); + await redirectionHostModel.query().deleteById(row.id); try { // Delete Nginx Config @@ -340,7 +333,6 @@ const internalRedirectionHost = { const query = redirectionHostModel .query() - .where("is_deleted", 0) .groupBy("id") .allowGraph("[owner,certificate]") .orderBy(castJsonIfNeed("domain_names"), "ASC"); @@ -371,7 +363,7 @@ const internalRedirectionHost = { * @returns {Promise} */ getCount: async (user_id, visibility) => { - const query = redirectionHostModel.query().count("id as count").where("is_deleted", 0); + const query = redirectionHostModel.query().count("id as count"); if (visibility !== "all") { query.andWhere("owner_user_id", user_id); diff --git a/backend/internal/stream.js b/backend/internal/stream.js index d6d020fac6..fb58a250a2 100644 --- a/backend/internal/stream.js +++ b/backend/internal/stream.js @@ -142,12 +142,7 @@ const internalStream = { access.can("streams:view"); - const query = streamModel - .query() - .where("is_deleted", 0) - .andWhere("id", thisData.id) - .allowGraph("[certificate]") - .first(); + const query = streamModel.query().where("id", thisData.id).allowGraph("[certificate]").first(); if (access.visibility !== "all") { query.andWhere("owner_user_id", access.token.getUserId(1)); @@ -180,9 +175,7 @@ const internalStream = { throw new errs.ItemNotFoundError(data.id); } - await streamModel.query().where("id", row.id).patch({ - is_deleted: 1, - }); + await streamModel.query().deleteById(row.id); try { // Delete Nginx Config @@ -304,7 +297,6 @@ const internalStream = { const query = streamModel .query() - .where("is_deleted", 0) .groupBy("id") .allowGraph("[owner,certificate]") .orderBy("incoming_port", "ASC"); @@ -338,7 +330,7 @@ const internalStream = { * @returns {Promise} */ getCount: async (user_id, visibility) => { - const query = streamModel.query().count("id as count").where("is_deleted", 0); + const query = streamModel.query().count("id as count"); if (visibility !== "all") { query.andWhere("owner_user_id", user_id); diff --git a/backend/internal/token.js b/backend/internal/token.js index f7731ec3fc..5b947e10af 100644 --- a/backend/internal/token.js +++ b/backend/internal/token.js @@ -26,7 +26,6 @@ export default { const user = await userModel .query() .where("email", data.identity.toLowerCase()) - .andWhere("is_deleted", 0) .andWhere("is_disabled", 0) .first(); @@ -98,7 +97,6 @@ export default { const user = await userModel .query() .where("email", data.identity.toLowerCase()) - .andWhere("is_deleted", 0) .andWhere("is_disabled", 0) .first(); @@ -194,12 +192,7 @@ export default { throw new errs.AuthError("Invalid challenge token"); } - const user = await userModel - .query() - .where("id", userId) - .andWhere("is_deleted", 0) - .andWhere("is_disabled", 0) - .first(); + const user = await userModel.query().where("id", userId).andWhere("is_disabled", 0).first(); if (!user || tokenData.iat <= user.npmplus_token_valid_after) { throw new errs.AuthError("Invalid challenge token"); } diff --git a/backend/internal/user.js b/backend/internal/user.js index 83afccdd08..a764443d43 100644 --- a/backend/internal/user.js +++ b/backend/internal/user.js @@ -91,6 +91,7 @@ const internalUser = { }, fetchGravatar: async (id, email, name) => { + for (const e of avatarExts) await rm(`/data/npmplus/gravatar/${id}.${e}`, { force: true }); if (process.env.DISABLE_GRAVATAR === "true") return "/images/default-avatar.jpg"; try { const hash = crypto.createHash("sha256").update(email.toLowerCase()).digest("hex"); @@ -114,7 +115,6 @@ const internalUser = { const ext = avatarExt(buffer); if (!ext) throw new Error("Unsupported image format"); - for (const e of avatarExts) await rm(`/data/npmplus/gravatar/${id}.${e}`, { force: true }); await writeFile(`/data/npmplus/gravatar/${id}.${ext}`, buffer); return `/images/gravatar/${id}.${ext}`; @@ -124,12 +124,19 @@ const internalUser = { } }, + deleteAvatarFiles: async (id) => { + for (const e of avatarExts) { + await rm(`/data/npmplus/avatar/${id}.${e}`, { force: true }); + await rm(`/data/npmplus/gravatar/${id}.${e}`, { force: true }); + } + }, + setAvatar: async (access, id, file) => { access.canUser(id); const ext = avatarExt(file?.buffer); if (!ext) throw new errs.ValidationError("Invalid avatar file type"); const user = await internalUser.get(access, { id }); - for (const e of avatarExts) await rm(`/data/npmplus/avatar/${user.id}.${e}`, { force: true }); + await internalUser.deleteAvatarFiles(user.id); await writeFile(`/data/npmplus/avatar/${user.id}.${ext}`, file.buffer); await userModel.query().patchAndFetchById(user.id, { avatar: `/images/avatar/${user.id}.${ext}` }); const savedUser = await internalUser.get(access, { id: user.id }); @@ -225,12 +232,7 @@ const internalUser = { access.canUser(thisData.id); - const query = userModel - .query() - .where("is_deleted", 0) - .andWhere("id", thisData.id) - .allowGraph("[permissions]") - .first(); + const query = userModel.query().where("id", thisData.id).allowGraph("[permissions]").first(); if (typeof thisData.expand !== "undefined" && thisData.expand !== null) { query.withGraphFetched(`[${thisData.expand.join(", ")}]`); @@ -255,7 +257,7 @@ const internalUser = { * @param user_id */ isEmailAvailable: async (email, user_id) => { - const query = userModel.query().where("email", "=", email.toLowerCase()).where("is_deleted", 0).first(); + const query = userModel.query().where("email", "=", email.toLowerCase()).first(); if (typeof user_id !== "undefined") { query.where("id", "!=", user_id); @@ -286,9 +288,12 @@ const internalUser = { throw new errs.PermissionError("You cannot delete yourself."); } - await userModel.query().where("id", user.id).patch({ - is_deleted: 1, + await userModel.transaction(async (trx) => { + await authModel.query(trx).where("user_id", user.id).delete(); + await userPermissionModel.query(trx).where("user_id", user.id).delete(); + await userModel.query(trx).deleteById(user.id); }); + await internalUser.deleteAvatarFiles(user.id); await internalAuditLog.add(access, { action: "deleted", @@ -310,7 +315,7 @@ const internalUser = { getCount: async (access, search_query) => { access.canAdmin(); - const query = userModel.query().count("id as count").where("is_deleted", 0).first(); + const query = userModel.query().count("id as count").first(); // Query is used for searching if (typeof search_query === "string") { @@ -332,7 +337,7 @@ const internalUser = { */ getAll: async (access, search_query) => { access.canAdmin(); - const query = userModel.query().where("is_deleted", 0).groupBy("id").orderBy("name", "ASC"); + const query = userModel.query().groupBy("id").orderBy("name", "ASC"); // Query is used for searching if (typeof search_query === "string") { diff --git a/backend/lib/access.js b/backend/lib/access.js index 01a05dba1a..1f91fd4ab1 100644 --- a/backend/lib/access.js +++ b/backend/lib/access.js @@ -39,7 +39,6 @@ export default function (tokenString) { const user = await userModel .query() .where("id", tokenData.attrs.id) - .andWhere("is_deleted", 0) .andWhere("is_disabled", 0) .allowGraph("[permissions]") .withGraphFetched("[permissions]") diff --git a/backend/lib/helpers.js b/backend/lib/helpers.js index bd1ef31f58..9707d371a8 100644 --- a/backend/lib/helpers.js +++ b/backend/lib/helpers.js @@ -66,9 +66,13 @@ const removeCertificateFields = (obj) => const jsonReplacer = (key, value) => { if (typeof value !== "string") return value; if (key === "password") return ""; - return ["certificate", "certificate_key", "dns_provider_credentials", "npmplus_dns_provider_credentials"].includes( - key, - ) + return [ + "certificate", + "certificate_key", + "dns_provider_credentials", + "npmplus_dns_provider_credentials", + "hint", + ].includes(key) ? undefined : value; }; diff --git a/backend/migrations/20261010135653_hard_delete.js b/backend/migrations/20261010135653_hard_delete.js new file mode 100644 index 0000000000..248bacdaef --- /dev/null +++ b/backend/migrations/20261010135653_hard_delete.js @@ -0,0 +1,63 @@ +import internalUser from "../internal/user.js"; +import { migrate as logger } from "../logger.js"; + +const migrateName = "hard_delete"; + +/** + * Migrate + * + * @see https://knexjs.org/guide/migrations.html#migration-api + * + * @param {Object} knex + * @returns {Promise} + */ +const up = async (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + await knex("npmplus_proxy_host_access_list") + .whereIn("proxy_host_id", knex("proxy_host").select("id").where("is_deleted", 1)) + .delete(); + + const accessListIds = await knex("access_list").where("is_deleted", 1).pluck("id"); + for (const table of ["npmplus_proxy_host_access_list", "access_list_auth", "access_list_client"]) { + await knex(table).whereIn("access_list_id", accessListIds).delete(); + } + + for (const table of [ + "proxy_host", + "redirection_host", + "dead_host", + "stream", + "access_list", + "certificate", + "auth", + ]) { + await knex(table).where("is_deleted", 1).delete(); + } + + logger.info(`[${migrateName}] deleted rows removed`); + + const userIds = await knex("user").where("is_deleted", 1).pluck("id"); + for (const table of ["auth", "user_permission"]) await knex(table).whereIn("user_id", userIds).delete(); + await knex("user").whereIn("id", userIds).delete(); + for (const id of userIds) await internalUser.deleteAvatarFiles(id); + + logger.info(`[${migrateName}] removed ${userIds.length} deleted users`); + + await knex("user").update({ nickname: "" }); + await knex("proxy_host").update({ access_list_id: 0 }); + + logger.info(`[${migrateName}] unused columns cleared`); +}; + +/** + * Undo Migrate + * + * @param {Object} _knex + * @returns {Promise} + */ +const down = (_knex) => { + throw new Error(`[${migrateName}] You can't migrate down this one.`); +}; + +export { down, up }; diff --git a/backend/models/access_list.js b/backend/models/access_list.js index 87fb271927..5bd7ae1d06 100644 --- a/backend/models/access_list.js +++ b/backend/models/access_list.js @@ -58,9 +58,6 @@ class AccessList extends Model { from: "access_list.owner_user_id", to: "user.id", }, - modify: (qb) => { - qb.where("user.is_deleted", 0); - }, }, items: { relation: Model.HasManyRelation, @@ -89,9 +86,6 @@ class AccessList extends Model { }, to: "proxy_host.id", }, - modify: (qb) => { - qb.where("proxy_host.is_deleted", 0); - }, }, }; } diff --git a/backend/models/access_list_auth.js b/backend/models/access_list_auth.js index df07467584..7554f8b346 100644 --- a/backend/models/access_list_auth.js +++ b/backend/models/access_list_auth.js @@ -47,9 +47,6 @@ class AccessListAuth extends Model { from: "access_list_auth.access_list_id", to: "access_list.id", }, - modify: (qb) => { - qb.where("access_list.is_deleted", 0); - }, }, }; } diff --git a/backend/models/access_list_client.js b/backend/models/access_list_client.js index da9d014374..eca36e2b86 100644 --- a/backend/models/access_list_client.js +++ b/backend/models/access_list_client.js @@ -47,9 +47,6 @@ class AccessListClient extends Model { from: "access_list_client.access_list_id", to: "access_list.id", }, - modify: (qb) => { - qb.where("access_list.is_deleted", 0); - }, }, }; } diff --git a/backend/models/auth.js b/backend/models/auth.js index 2d97d4dd84..0307d7bee3 100644 --- a/backend/models/auth.js +++ b/backend/models/auth.js @@ -85,9 +85,6 @@ class Auth extends Model { from: "auth.user_id", to: "user.id", }, - filter: { - is_deleted: 0, - }, }, }; } diff --git a/backend/models/certificate.js b/backend/models/certificate.js index e08eca551a..d3a960ab26 100644 --- a/backend/models/certificate.js +++ b/backend/models/certificate.js @@ -66,9 +66,6 @@ class Certificate extends Model { from: "certificate.owner_user_id", to: "user.id", }, - modify: (qb) => { - qb.where("user.is_deleted", 0); - }, }, proxy_hosts: { relation: Model.HasManyRelation, @@ -77,9 +74,6 @@ class Certificate extends Model { from: "certificate.id", to: "proxy_host.certificate_id", }, - modify: (qb) => { - qb.where("proxy_host.is_deleted", 0); - }, }, dead_hosts: { relation: Model.HasManyRelation, @@ -88,9 +82,6 @@ class Certificate extends Model { from: "certificate.id", to: "dead_host.certificate_id", }, - modify: (qb) => { - qb.where("dead_host.is_deleted", 0); - }, }, redirection_hosts: { relation: Model.HasManyRelation, @@ -99,9 +90,6 @@ class Certificate extends Model { from: "certificate.id", to: "redirection_host.certificate_id", }, - modify: (qb) => { - qb.where("redirection_host.is_deleted", 0); - }, }, streams: { relation: Model.HasManyRelation, @@ -110,9 +98,6 @@ class Certificate extends Model { from: "certificate.id", to: "stream.certificate_id", }, - modify: (qb) => { - qb.where("stream.is_deleted", 0); - }, }, mtls_proxy_hosts: { relation: Model.HasManyRelation, @@ -121,9 +106,6 @@ class Certificate extends Model { from: "certificate.id", to: "proxy_host.npmplus_mtls_certificate_id", }, - modify: (qb) => { - qb.where("proxy_host.is_deleted", 0); - }, }, mtls_dead_hosts: { relation: Model.HasManyRelation, @@ -132,9 +114,6 @@ class Certificate extends Model { from: "certificate.id", to: "dead_host.npmplus_mtls_certificate_id", }, - modify: (qb) => { - qb.where("dead_host.is_deleted", 0); - }, }, mtls_redirection_hosts: { relation: Model.HasManyRelation, @@ -143,9 +122,6 @@ class Certificate extends Model { from: "certificate.id", to: "redirection_host.npmplus_mtls_certificate_id", }, - modify: (qb) => { - qb.where("redirection_host.is_deleted", 0); - }, }, mtls_streams: { relation: Model.HasManyRelation, @@ -154,9 +130,6 @@ class Certificate extends Model { from: "certificate.id", to: "stream.npmplus_mtls_certificate_id", }, - modify: (qb) => { - qb.where("stream.is_deleted", 0); - }, }, }; } diff --git a/backend/models/dead_host.js b/backend/models/dead_host.js index 9f2f6d7fa5..8253b1476f 100644 --- a/backend/models/dead_host.js +++ b/backend/models/dead_host.js @@ -69,9 +69,6 @@ class DeadHost extends Model { from: "dead_host.owner_user_id", to: "user.id", }, - modify: (qb) => { - qb.where("user.is_deleted", 0); - }, }, certificate: { relation: Model.HasOneRelation, @@ -80,9 +77,6 @@ class DeadHost extends Model { from: "dead_host.certificate_id", to: "certificate.id", }, - modify: (qb) => { - qb.where("certificate.is_deleted", 0); - }, }, }; } diff --git a/backend/models/proxy_host.js b/backend/models/proxy_host.js index 5f137812ab..e619b93536 100644 --- a/backend/models/proxy_host.js +++ b/backend/models/proxy_host.js @@ -95,9 +95,6 @@ class ProxyHost extends Model { from: "proxy_host.owner_user_id", to: "user.id", }, - modify: (qb) => { - qb.where("user.is_deleted", 0); - }, }, access_lists: { relation: Model.ManyToManyRelation, @@ -110,9 +107,6 @@ class ProxyHost extends Model { }, to: "access_list.id", }, - modify: (qb) => { - qb.where("access_list.is_deleted", 0); - }, }, certificate: { relation: Model.HasOneRelation, @@ -121,9 +115,6 @@ class ProxyHost extends Model { from: "proxy_host.certificate_id", to: "certificate.id", }, - modify: (qb) => { - qb.where("certificate.is_deleted", 0); - }, }, }; } diff --git a/backend/models/redirection_host.js b/backend/models/redirection_host.js index 090222f9dc..6463d0c99d 100644 --- a/backend/models/redirection_host.js +++ b/backend/models/redirection_host.js @@ -70,9 +70,6 @@ class RedirectionHost extends Model { from: "redirection_host.owner_user_id", to: "user.id", }, - modify: (qb) => { - qb.where("user.is_deleted", 0); - }, }, certificate: { relation: Model.HasOneRelation, @@ -81,9 +78,6 @@ class RedirectionHost extends Model { from: "redirection_host.certificate_id", to: "certificate.id", }, - modify: (qb) => { - qb.where("certificate.is_deleted", 0); - }, }, }; } diff --git a/backend/models/stream.js b/backend/models/stream.js index 9f77214f63..c56fa550e5 100644 --- a/backend/models/stream.js +++ b/backend/models/stream.js @@ -65,9 +65,6 @@ class Stream extends Model { from: "stream.owner_user_id", to: "user.id", }, - modify: (qb) => { - qb.where("user.is_deleted", 0); - }, }, certificate: { relation: Model.HasOneRelation, @@ -76,9 +73,6 @@ class Stream extends Model { from: "stream.certificate_id", to: "certificate.id", }, - modify: (qb) => { - qb.where("certificate.is_deleted", 0); - }, }, }; } diff --git a/backend/password-reset.js b/backend/password-reset.js index dd2c46175b..1f13655cbc 100755 --- a/backend/password-reset.js +++ b/backend/password-reset.js @@ -43,7 +43,7 @@ try { const auth = db .prepare( - "SELECT auth.user_id FROM auth JOIN user ON user.id = auth.user_id WHERE auth.type = 'password' AND auth.is_deleted = 0 AND user.is_deleted = 0 AND user.email = ?", + "SELECT auth.user_id FROM auth JOIN user ON user.id = auth.user_id WHERE auth.type = 'password' AND user.email = ?", ) .get(EMAIL); diff --git a/backend/setup.js b/backend/setup.js index 0c3c743294..60241c3b74 100644 --- a/backend/setup.js +++ b/backend/setup.js @@ -85,7 +85,7 @@ const setupDefaultSettings = async () => { * @returns {Promise} */ const setupCertbotPlugins = async () => { - const certificates = await certificateModel.query().where("is_deleted", 0).andWhere("provider", "letsencrypt"); + const certificates = await certificateModel.query().where("provider", "letsencrypt"); if (certificates?.length > 0) { const plugins = []; @@ -121,8 +121,7 @@ const regenerateAllHosts = async () => { if (process.env.REGENERATE_ALL === "true") { const proxyHosts = await proxyModel .query() - .where("is_deleted", 0) - .andWhere("enabled", 1) + .where("enabled", 1) .withGraphFetched("[access_lists.[clients,items],certificate]"); if (proxyHosts?.length > 0) { @@ -137,11 +136,7 @@ const regenerateAllHosts = async () => { await internalNginx.bulkGenerateConfigs(proxyModel, "proxy_host", updatedProxyHosts, { skipReload: true }); } - const redirectionHosts = await redirectionModel - .query() - .where("is_deleted", 0) - .andWhere("enabled", 1) - .withGraphFetched("certificate"); + const redirectionHosts = await redirectionModel.query().where("enabled", 1).withGraphFetched("certificate"); if (redirectionHosts?.length > 0) { await internalNginx.bulkGenerateConfigs(redirectionModel, "redirection_host", redirectionHosts, { @@ -149,21 +144,13 @@ const regenerateAllHosts = async () => { }); } - const deadHosts = await deadModel - .query() - .where("is_deleted", 0) - .andWhere("enabled", 1) - .withGraphFetched("certificate"); + const deadHosts = await deadModel.query().where("enabled", 1).withGraphFetched("certificate"); if (deadHosts?.length > 0) { await internalNginx.bulkGenerateConfigs(deadModel, "dead_host", deadHosts, { skipReload: true }); } - const streamHosts = await streamModel - .query() - .where("is_deleted", 0) - .andWhere("enabled", 1) - .withGraphFetched("certificate"); + const streamHosts = await streamModel.query().where("enabled", 1).withGraphFetched("certificate"); if (streamHosts?.length > 0) { await internalNginx.bulkGenerateConfigs(streamModel, "stream", streamHosts, { skipReload: true }); @@ -210,7 +197,7 @@ const setupAio = async () => { }; export const isSetup = async () => { - const row = await userModel.query().select("id").where("is_deleted", 0).first(); + const row = await userModel.query().select("id").first(); return row?.id > 0; }; diff --git a/frontend/src/pages/Access/Table.jsx b/frontend/src/pages/Access/Table.jsx index 548621f979..cc6ec739e0 100644 --- a/frontend/src/pages/Access/Table.jsx +++ b/frontend/src/pages/Access/Table.jsx @@ -29,11 +29,16 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete, const columnHelper = createColumnHelper(); const columns = useMemo( () => [ - columnHelper.accessor((row) => row.owner.name, { + columnHelper.accessor((row) => row.owner?.name, { id: "owner", cell: (info) => { const value = info.row.original.owner; - return ; + return ( + + ); }, meta: { className: "w-1", diff --git a/frontend/src/pages/AuditLog/Table.jsx b/frontend/src/pages/AuditLog/Table.jsx index d2bc09975d..8927aa3157 100644 --- a/frontend/src/pages/AuditLog/Table.jsx +++ b/frontend/src/pages/AuditLog/Table.jsx @@ -31,7 +31,12 @@ export default function Table({ data, isFetching, onSelectItem }) { id: "avatar", cell: (info) => { const value = info.row.original.user; - return ; + return ( + + ); }, meta: { className: "w-1", diff --git a/frontend/src/pages/Certificates/Table.jsx b/frontend/src/pages/Certificates/Table.jsx index 556b24651f..b1515e0031 100644 --- a/frontend/src/pages/Certificates/Table.jsx +++ b/frontend/src/pages/Certificates/Table.jsx @@ -49,11 +49,16 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload, const columnHelper = createColumnHelper(); const columns = useMemo( () => [ - columnHelper.accessor((row) => row.owner.name, { + columnHelper.accessor((row) => row.owner?.name, { id: "owner", cell: (info) => { const value = info.row.original.owner; - return ; + return ( + + ); }, meta: { className: "w-1", diff --git a/frontend/src/pages/Nginx/DeadHosts/Table.jsx b/frontend/src/pages/Nginx/DeadHosts/Table.jsx index 62b983337b..d048401957 100644 --- a/frontend/src/pages/Nginx/DeadHosts/Table.jsx +++ b/frontend/src/pages/Nginx/DeadHosts/Table.jsx @@ -49,11 +49,16 @@ export default function Table({ const columnHelper = createColumnHelper(); const columns = useMemo( () => [ - columnHelper.accessor((row) => row.owner.name, { + columnHelper.accessor((row) => row.owner?.name, { id: "owner", cell: (info) => { const value = info.row.original.owner; - return ; + return ( + + ); }, meta: { className: "w-1", diff --git a/frontend/src/pages/Nginx/ProxyHosts/Table.jsx b/frontend/src/pages/Nginx/ProxyHosts/Table.jsx index 22b7b397a5..9d8b33ddd7 100644 --- a/frontend/src/pages/Nginx/ProxyHosts/Table.jsx +++ b/frontend/src/pages/Nginx/ProxyHosts/Table.jsx @@ -51,11 +51,16 @@ export default function Table({ const columnHelper = createColumnHelper(); const columns = useMemo( () => [ - columnHelper.accessor((row) => row.owner.name, { + columnHelper.accessor((row) => row.owner?.name, { id: "owner", cell: (info) => { const value = info.row.original.owner; - return ; + return ( + + ); }, meta: { className: "w-1", diff --git a/frontend/src/pages/Nginx/RedirectionHosts/Table.jsx b/frontend/src/pages/Nginx/RedirectionHosts/Table.jsx index 5bb39a29c9..33bfb7b481 100644 --- a/frontend/src/pages/Nginx/RedirectionHosts/Table.jsx +++ b/frontend/src/pages/Nginx/RedirectionHosts/Table.jsx @@ -49,11 +49,16 @@ export default function Table({ const columnHelper = createColumnHelper(); const columns = useMemo( () => [ - columnHelper.accessor((row) => row.owner.name, { + columnHelper.accessor((row) => row.owner?.name, { id: "owner", cell: (info) => { const value = info.row.original.owner; - return ; + return ( + + ); }, meta: { className: "w-1", diff --git a/frontend/src/pages/Nginx/Streams/Table.jsx b/frontend/src/pages/Nginx/Streams/Table.jsx index 3ac9f2fa06..75075cb1d5 100644 --- a/frontend/src/pages/Nginx/Streams/Table.jsx +++ b/frontend/src/pages/Nginx/Streams/Table.jsx @@ -49,11 +49,16 @@ export default function Table({ const columnHelper = createColumnHelper(); const columns = useMemo( () => [ - columnHelper.accessor((row) => row.owner.name, { + columnHelper.accessor((row) => row.owner?.name, { id: "owner", cell: (info) => { const value = info.row.original.owner; - return ; + return ( + + ); }, meta: { className: "w-1", diff --git a/frontend/translations/ui/en.json b/frontend/translations/ui/en.json index 5851ba51e1..80ae8bef28 100644 --- a/frontend/translations/ui/en.json +++ b/frontend/translations/ui/en.json @@ -321,6 +321,7 @@ "user.change-password": "Change Password", "user.confirm-password": "Confirm Password", "user.current-password": "Current Password", + "user.deleted": "Deleted User", "user.edit-profile": "Edit Profile", "user.full-name": "Full Name", "user.login-as": "Sign in as {name}", diff --git a/frontend/translations/ui/nl.json b/frontend/translations/ui/nl.json index ef8a83993b..8779a97b0f 100644 --- a/frontend/translations/ui/nl.json +++ b/frontend/translations/ui/nl.json @@ -321,6 +321,7 @@ "user.change-password": "Wachtwoord wijzigen", "user.confirm-password": "Wachtwoord bevestigen", "user.current-password": "Huidig wachtwoord", + "user.deleted": "Verwijderde gebruiker", "user.edit-profile": "Profiel bewerken", "user.full-name": "Volledige naam", "user.login-as": "Inloggen als {name}", From 85b0cb456b485f421cd9f5c754d3d32b3cfc443d Mon Sep 17 00:00:00 2001 From: Zoey Date: Sat, 10 Oct 2026 16:00:55 +0200 Subject: [PATCH 25/62] add more transactions --- backend/internal/access-list.js | 206 ++++++++++++++++--------------- backend/internal/backup-codes.js | 20 +-- backend/internal/mfa.js | 38 +++--- backend/internal/totp.js | 53 ++++---- backend/internal/user.js | 88 +++++++------ 5 files changed, 215 insertions(+), 190 deletions(-) diff --git a/backend/internal/access-list.js b/backend/internal/access-list.js index 646d9d0fa2..cd22826936 100644 --- a/backend/internal/access-list.js +++ b/backend/internal/access-list.js @@ -22,37 +22,41 @@ const internalAccessList = { throw new errs.ValidationError("New access list users need a password"); } - const row = await accessListModel.query().insertAndFetch({ - name: data.name, - satisfy_any: data.satisfy_any, - pass_auth: data.pass_auth, - owner_user_id: access.token.getUserId(1), + const row = await accessListModel.transaction(async (trx) => { + const insertedRow = await accessListModel.query(trx).insertAndFetch({ + name: data.name, + satisfy_any: data.satisfy_any, + pass_auth: data.pass_auth, + owner_user_id: access.token.getUserId(1), + }); + + // Items + await Promise.all( + (data.items ?? []).map(async (item) => + accessListAuthModel.query(trx).insert({ + access_list_id: insertedRow.id, + username: item.username, + password: await bcrypt.hash(item.password, 5), + }), + ), + ); + + // Clients + await Promise.all( + (data.clients ?? []).map((client) => + accessListClientModel.query(trx).insert({ + access_list_id: insertedRow.id, + address: client.address, + directive: client.directive, + }), + ), + ); + + return insertedRow; }); data.id = row.id; - // Items - await Promise.all( - (data.items ?? []).map(async (item) => - accessListAuthModel.query().insert({ - access_list_id: row.id, - username: item.username, - password: await bcrypt.hash(item.password, 5), - }), - ), - ); - - // Clients - await Promise.all( - (data.clients ?? []).map((client) => - accessListClientModel.query().insert({ - access_list_id: row.id, - address: client.address, - directive: client.directive, - }), - ), - ); - // re-fetch with expansions const freshRow = await internalAccessList.get(access, { id: data.id, @@ -97,61 +101,63 @@ const internalAccessList = { throw new errs.ValidationError("New access list users need a password"); } - // patch name if specified - if ( - typeof data.name !== "undefined" || - typeof data.satisfy_any !== "undefined" || - typeof data.pass_auth !== "undefined" - ) { - await accessListModel.query().where({ id: data.id }).patch({ - name: data.name, - satisfy_any: data.satisfy_any, - pass_auth: data.pass_auth, - }); - } + await accessListModel.transaction(async (trx) => { + // patch name if specified + if ( + typeof data.name !== "undefined" || + typeof data.satisfy_any !== "undefined" || + typeof data.pass_auth !== "undefined" + ) { + await accessListModel.query(trx).where({ id: data.id }).patch({ + name: data.name, + satisfy_any: data.satisfy_any, + pass_auth: data.pass_auth, + }); + } - // Check for items and add/update/remove them - if (typeof data.items !== "undefined" && data.items) { - // Items supplied with an empty password are kept, but their password is left untouched - const itemsToKeep = data.items.filter((item) => !item.password).map((item) => item.username); + // Check for items and add/update/remove them + if (typeof data.items !== "undefined" && data.items) { + // Items supplied with an empty password are kept, but their password is left untouched + const itemsToKeep = data.items.filter((item) => !item.password).map((item) => item.username); - const query = accessListAuthModel.query().delete().where("access_list_id", data.id); + const query = accessListAuthModel.query(trx).delete().where("access_list_id", data.id); - if (itemsToKeep.length > 0) { - query.andWhere("username", "NOT IN", itemsToKeep); - } - - await query; - // Add new items - await Promise.all( - data.items - .filter((item) => item.password) - .map(async (item) => - accessListAuthModel.query().insert({ - access_list_id: data.id, - username: item.username, - password: await bcrypt.hash(item.password, 5), - }), - ), - ); - } + if (itemsToKeep.length > 0) { + query.andWhere("username", "NOT IN", itemsToKeep); + } - // Check for clients and add/update/remove them - if (typeof data.clients !== "undefined" && data.clients) { - await accessListClientModel.query().delete().where("access_list_id", data.id); + await query; + // Add new items + await Promise.all( + data.items + .filter((item) => item.password) + .map(async (item) => + accessListAuthModel.query(trx).insert({ + access_list_id: data.id, + username: item.username, + password: await bcrypt.hash(item.password, 5), + }), + ), + ); + } - await Promise.all( - data.clients - .filter((client) => client.address) - .map((client) => - accessListClientModel.query().insert({ - access_list_id: data.id, - address: client.address, - directive: client.directive, - }), - ), - ); - } + // Check for clients and add/update/remove them + if (typeof data.clients !== "undefined" && data.clients) { + await accessListClientModel.query(trx).delete().where("access_list_id", data.id); + + await Promise.all( + data.clients + .filter((client) => client.address) + .map((client) => + accessListClientModel.query(trx).insert({ + access_list_id: data.id, + address: client.address, + directive: client.directive, + }), + ), + ); + } + }); // re-fetch with expansions const freshRow = await internalAccessList.get(access, { @@ -251,15 +257,6 @@ const internalAccessList = { // 3. reconfigure those hosts // 4. audit log - // 1. update row to be deleted - await accessListModel.transaction(async (trx) => { - await trx("npmplus_proxy_host_access_list").where("access_list_id", row.id).delete(); - await accessListAuthModel.query(trx).where("access_list_id", row.id).delete(); - await accessListClientModel.query(trx).where("access_list_id", row.id).delete(); - await accessListModel.query(trx).deleteById(row.id); - }); - - // 2. update any proxy hosts that were using it (ignoring permissions) const affectedHosts = (row.proxy_hosts || []).map((host) => { const updatedHost = { ...host }; // check in case something crazy happened. This should never be the case, but safeguard @@ -300,24 +297,31 @@ const internalAccessList = { }); const deletedRow = { ...row, proxy_hosts: undefined }; + await accessListModel.transaction(async (trx) => { + // 1. update row to be deleted + await trx("npmplus_proxy_host_access_list").where("access_list_id", row.id).delete(); + await accessListAuthModel.query(trx).where("access_list_id", row.id).delete(); + await accessListClientModel.query(trx).where("access_list_id", row.id).delete(); + await accessListModel.query(trx).deleteById(row.id); + // 2. update any proxy hosts that were using it (ignoring permissions) + await Promise.all( + affectedHosts.map(async (host) => { + await proxyHostModel.query(trx).patchAndFetchById(host.id, { + npmplus_access_list_ids: host.npmplus_access_list_ids, + npmplus_access_list_type: host.npmplus_access_list_type, + locations: host.locations, + }); + + return internalProxyHostAccessList.syncAccessListRelations(trx, host.id, host); + }), + ); + }); + try { - // 3. Write the changes to the database and the config + // 3. Write the changes to the config if (affectedHosts.length > 0) { - await proxyHostModel.transaction(async (trx) => { - await Promise.all( - affectedHosts.map(async (host) => { - await proxyHostModel.query(trx).patchAndFetchById(host.id, { - npmplus_access_list_ids: host.npmplus_access_list_ids, - npmplus_access_list_type: host.npmplus_access_list_type, - locations: host.locations, - }); - - return internalProxyHostAccessList.syncAccessListRelations(trx, host.id, host); - }), - ); - }); row.proxy_hosts = affectedHosts; - // step 4. Regenerate configs and htpasswd files + // Regenerate configs and htpasswd files // locations don't have accessList objects, only IDs, so populate it with the object itself row.proxy_hosts = await Promise.all( (row.proxy_hosts || []).map((host) => { diff --git a/backend/internal/backup-codes.js b/backend/internal/backup-codes.js index 448fd52111..5951adaa55 100644 --- a/backend/internal/backup-codes.js +++ b/backend/internal/backup-codes.js @@ -3,7 +3,7 @@ import bcrypt from "bcryptjs"; import { hash, verify } from "../lib/argon2.js"; import authModel from "../models/auth.js"; -const codesOf = (userId) => authModel.query().where("user_id", userId).andWhere("type", "backup_code"); +const codesOf = (userId, trx) => authModel.query(trx).where("user_id", userId).andWhere("type", "backup_code"); /** * Generate backup codes @@ -31,7 +31,7 @@ const internalBackupCodes = { * @param {number} userId * @returns {Promise} */ - count: async (userId) => await codesOf(userId).resultSize(), + count: async (userId, trx) => await codesOf(userId, trx).resultSize(), /** * Replace all backup codes of a user with a freshly generated set @@ -39,13 +39,15 @@ const internalBackupCodes = { * @param {number} userId * @returns {Promise} */ - create: async (userId) => { + create: async (userId, outerTrx) => { const { plain, hashed } = await generate(); - await codesOf(userId).delete(); - for (const secret of hashed) { - await authModel.query().insert({ user_id: userId, type: "backup_code", secret, meta: {} }); - } + await authModel.transaction(outerTrx, async (trx) => { + await codesOf(userId, trx).delete(); + for (const secret of hashed) { + await authModel.query(trx).insert({ user_id: userId, type: "backup_code", secret, meta: {} }); + } + }); return plain; }, @@ -56,8 +58,8 @@ const internalBackupCodes = { * @param {number} userId * @returns {Promise} */ - delete: async (userId) => { - await codesOf(userId).delete(); + delete: async (userId, trx) => { + await codesOf(userId, trx).delete(); }, /** diff --git a/backend/internal/mfa.js b/backend/internal/mfa.js index a9d561bab5..236be4926f 100644 --- a/backend/internal/mfa.js +++ b/backend/internal/mfa.js @@ -12,7 +12,7 @@ const internalMfa = { * @param {number} userId * @returns {Promise} */ - isAnyEnabled: async (userId) => await totp.isEnabled(userId), + isAnyEnabled: async (userId, trx) => await totp.isEnabled(userId, trx), /** * Get MFA status for user (all factors and backup codes) @@ -37,12 +37,12 @@ const internalMfa = { * @param {number} userId * @returns {Promise<{backup_codes: string[]} | null>} */ - ensureBackupCodes: async (userId) => { - if (await backupCodes.count(userId)) { + ensureBackupCodes: async (userId, trx) => { + if (await backupCodes.count(userId, trx)) { return null; } - return { backup_codes: await backupCodes.create(userId) }; + return { backup_codes: await backupCodes.create(userId, trx) }; }, /** @@ -54,8 +54,10 @@ const internalMfa = { * @returns {Promise<{backup_codes: string[] | null}>} */ enableTotp: async (access, userId, code) => { - await totp.enable(access, userId, code); - const codes = await internalMfa.ensureBackupCodes(userId); + let codes = null; + await totp.enable(access, userId, code, async (trx) => { + codes = await internalMfa.ensureBackupCodes(userId, trx); + }); return codes ?? { backup_codes: null }; }, @@ -78,11 +80,9 @@ const internalMfa = { throw new errs.ValidationError("Invalid verification code"); } - await totp.disable(access, userId); - - if (!(await internalMfa.isAnyEnabled(userId))) { - await backupCodes.delete(userId); - } + await totp.disable(access, userId, false, async (trx) => { + if (!(await internalMfa.isAnyEnabled(userId, trx))) await backupCodes.delete(userId, trx); + }); }, /** @@ -123,8 +123,7 @@ const internalMfa = { throw new errs.ValidationError("MFA is not enabled"); } - await totp.disable(access, userId, false); - await backupCodes.delete(userId); + await totp.disable(access, userId, true, (trx) => backupCodes.delete(userId, trx)); await internalAuditLog.add(access, { action: "updated", @@ -163,12 +162,13 @@ const internalMfa = { throw new errs.ValidationError("Invalid verification code"); } - const plain = await backupCodes.create(userId); - - await userModel - .query() - .where("id", userId) - .patch({ npmplus_token_valid_after: Math.floor(Date.now() / 1000) }); + const plain = await userModel.transaction(async (trx) => { + await userModel + .query(trx) + .where("id", userId) + .patch({ npmplus_token_valid_after: Math.floor(Date.now() / 1000) }); + return backupCodes.create(userId, trx); + }); await internalAuditLog.add(access, { action: "updated", diff --git a/backend/internal/totp.js b/backend/internal/totp.js index 32ae274031..9af356716e 100644 --- a/backend/internal/totp.js +++ b/backend/internal/totp.js @@ -15,8 +15,8 @@ const internalTotp = { * @param {number} userId * @returns {Promise} */ - isEnabled: async (userId) => - (await authModel.query().where("user_id", userId).andWhere("type", "totp").resultSize()) > 0, + isEnabled: async (userId, trx) => + (await authModel.query(trx).where("user_id", userId).andWhere("type", "totp").resultSize()) > 0, /** * Start TOTP setup - store pending secret @@ -43,8 +43,10 @@ const internalTotp = { secret, }); - await authModel.query().where("user_id", userId).andWhere("type", "totp_pending").delete(); - await authModel.query().insert({ user_id: userId, type: "totp_pending", secret, meta: {} }); + await authModel.transaction(async (trx) => { + await authModel.query(trx).where("user_id", userId).andWhere("type", "totp_pending").delete(); + await authModel.query(trx).insert({ user_id: userId, type: "totp_pending", secret, meta: {} }); + }); return { secret, otpauth_url }; }, @@ -57,7 +59,7 @@ const internalTotp = { * @param {string} code * @returns {Promise} */ - enable: async (access, userId, code) => { + enable: async (access, userId, code, onEnable) => { if (Number(userId) !== access.token.getUserId(0)) { throw new errs.PermissionError("TOTP can only be managed for your own account"); } @@ -83,21 +85,25 @@ const internalTotp = { throw new errs.ValidationError("Invalid verification code"); } - const enabled = await authModel - .query() - .findById(pending.id) - .andWhere("type", "totp_pending") - .patch({ type: "totp" }); + await authModel.transaction(async (trx) => { + const enabled = await authModel + .query(trx) + .findById(pending.id) + .andWhere("type", "totp_pending") + .patch({ type: "totp" }); - if (enabled !== 1) { - throw new errs.ValidationError("No pending TOTP setup found"); - } - usedSteps.set(pending.id, result.timeStep); + if (enabled !== 1) { + throw new errs.ValidationError("No pending TOTP setup found"); + } - await userModel - .query() - .where("id", userId) - .patch({ npmplus_token_valid_after: Math.floor(Date.now() / 1000) }); + await userModel + .query(trx) + .where("id", userId) + .patch({ npmplus_token_valid_after: Math.floor(Date.now() / 1000) }); + + await onEnable?.(trx); + }); + usedSteps.set(pending.id, result.timeStep); await internalAuditLog.add(access, { action: "updated", @@ -115,13 +121,16 @@ const internalTotp = { * * @param {Access} access * @param {number} userId - * @param {boolean} audit + * @param {boolean} skipAudit * @returns {Promise} */ - disable: async (access, userId, audit = true) => { - await authModel.query().where("user_id", userId).whereIn("type", ["totp", "totp_pending"]).delete(); + disable: async (access, userId, skipAudit, onDisable) => { + await authModel.transaction(async (trx) => { + await authModel.query(trx).where("user_id", userId).whereIn("type", ["totp", "totp_pending"]).delete(); + await onDisable?.(trx); + }); - if (audit) { + if (!skipAudit) { const user = await internalUser.get(access, { id: userId }); await internalAuditLog.add(access, { action: "updated", diff --git a/backend/internal/user.js b/backend/internal/user.js index a764443d43..20ccdcd719 100644 --- a/backend/internal/user.js +++ b/backend/internal/user.js @@ -50,28 +50,32 @@ const internalUser = { throw new errs.ValidationError(`Email address already in use - ${data.email}`); } - let user = await userModel.query().insertAndFetch(data); - if (auth) { - await authModel.query().insert({ - user_id: user.id, - type: auth.type, - secret: auth.secret, - meta: {}, + let user = await userModel.transaction(async (trx) => { + const insertedUser = await userModel.query(trx).insertAndFetch(data); + if (auth) { + await authModel.query(trx).insert({ + user_id: insertedUser.id, + type: auth.type, + secret: auth.secret, + meta: {}, + }); + } + + // Create permissions row as well + const isAdmin = data.roles.indexOf("admin") !== -1; + + await userPermissionModel.query(trx).insert({ + user_id: insertedUser.id, + visibility: isAdmin ? "all" : "user", + proxy_hosts: "hidden", + redirection_hosts: "hidden", + dead_hosts: "hidden", + streams: "hidden", + access_lists: "hidden", + certificates: "hidden", }); - } - // Create permissions row as well - const isAdmin = data.roles.indexOf("admin") !== -1; - - await userPermissionModel.query().insert({ - user_id: user.id, - visibility: isAdmin ? "all" : "user", - proxy_hosts: "hidden", - redirection_hosts: "hidden", - dead_hosts: "hidden", - streams: "hidden", - access_lists: "hidden", - certificates: "hidden", + return insertedUser; }); await userModel @@ -380,26 +384,32 @@ const internalUser = { }); } - const existing_auth = await authModel.query().where("user_id", user.id).andWhere("type", data.type).first(); - - if (existing_auth) { - await authModel.query().where("user_id", user.id).andWhere("type", data.type).patch({ - type: data.type, // This is required for the model to encrypt on save - secret: data.secret, - }); - } else { - await authModel.query().insert({ - user_id: user.id, - type: data.type, - secret: data.secret, - meta: {}, - }); - } + await userModel.transaction(async (trx) => { + const existing_auth = await authModel + .query(trx) + .where("user_id", user.id) + .andWhere("type", data.type) + .first(); + + if (existing_auth) { + await authModel.query(trx).where("user_id", user.id).andWhere("type", data.type).patch({ + type: data.type, // This is required for the model to encrypt on save + secret: data.secret, + }); + } else { + await authModel.query(trx).insert({ + user_id: user.id, + type: data.type, + secret: data.secret, + meta: {}, + }); + } - await userModel - .query() - .where("id", user.id) - .patch({ npmplus_token_valid_after: Math.floor(Date.now() / 1000) }); + await userModel + .query(trx) + .where("id", user.id) + .patch({ npmplus_token_valid_after: Math.floor(Date.now() / 1000) }); + }); await internalAuditLog.add(access, { action: "updated", From 0eaf315271759390528b71cd4dbd7557ed6ab93b Mon Sep 17 00:00:00 2001 From: Zoey Date: Sat, 10 Oct 2026 16:50:59 +0200 Subject: [PATCH 26/62] add german translatioms --- frontend/src/components/SiteFooter.jsx | 2 + frontend/translations/ui/de.json | 61 ++++++++++++++++++++++++++ frontend/translations/ui/en.json | 2 +- frontend/translations/ui/nl.json | 2 +- 4 files changed, 65 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/SiteFooter.jsx b/frontend/src/components/SiteFooter.jsx index 6ca3dd4835..77178d327e 100644 --- a/frontend/src/components/SiteFooter.jsx +++ b/frontend/src/components/SiteFooter.jsx @@ -1,3 +1,4 @@ +import { IconBrandGithub } from "@tabler/icons-react"; import { useCheckVersion } from "src/hooks"; import { T } from "src/locale"; @@ -22,6 +23,7 @@ export function SiteFooter() { rel="noopener" > + diff --git a/frontend/translations/ui/de.json b/frontend/translations/ui/de.json index 64adb87e23..868efd0b2a 100644 --- a/frontend/translations/ui/de.json +++ b/frontend/translations/ui/de.json @@ -2,7 +2,12 @@ "access-list": "Zugriffsliste", "access-list.access-count": "{count} {count, plural, one {Regel} other {Regeln}}", "access-list.auth-count": "{count} {count, plural, one {User} other {Users}}", + "access-list.custom": "Benutzerdefiniert", + "access-list.custom.subtitle": "Verwendet einen benutzerdefinierten Zugriffslistentyp", + "access-list.global": "Global", + "access-list.global.subtitle": "Verwendet dasselbe wie die globale Zugriffsliste", "access-list.help-rules-last": "Wenn mindestens eine Regel vorhanden ist, wird diese Regel zum Ablehnen aller Anfragen als letzte hinzugefügt.", + "access-list.help.rules-last": "Diese letzte Regel wird nur hinzugefügt, wenn mindestens eine andere Regel vorhanden ist", "access-list.help.rules-order": "Beachten Sie, dass die Anweisungen „Erlauben“ und „Verbieten“ in der Reihenfolge ihrer Definition angewendet werden.", "access-list.inherit": "Vom Proxy Host übernehmen", "access-list.inherit.subtitle": "Verwendet die Zugriffsliste des Proxy Hosts", @@ -13,10 +18,12 @@ "access-list.satisfy-any": "Satisfy Any", "access-list.subtitle": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Regel} other {Regeln}} - Erstellt: {date}", "access-lists": "Zugriffslisten", + "access-lists.count": "{count} {count, plural, one {Zugriffsliste} other {Zugriffslisten}}", "action.add": "Hinzufügen", "action.add-location": "Pfad hinzufügen", "action.allow": "Erlauben", "action.clear": "Leeren", + "action.clone": "Klonen", "action.close": "Schließen", "action.delete": "Löschen", "action.deny": "Verweigern", @@ -30,6 +37,8 @@ "action.view-details": "Details anzeigen", "auditlogs": "Protokolle", "auto": "Automatisch", + "avatar.remove": "Avatar entfernen", + "avatar.upload": "Avatar hochladen", "cancel": "Abbrechen", "certificate": "Zertifikat", "certificate.custom-certificate": "Zertifikat", @@ -42,6 +51,7 @@ "certificate.not-in-use": "Nicht in Benutzung", "certificate.renew": "Zertifikat erneuern", "certificates": "Zertifikate", + "certificates.count": "{count} {count, plural, one {Zertifikat} other {Zertifikate}}", "certificates.custom": "Benutzerdefiniertes Zertifikat", "certificates.custom.warning": "Mit einem Passwort geschützte Schlüsseldateien werden nicht unterstützt.", "certificates.dns.credentials": "Inhalt der Anmeldedaten-Datei", @@ -70,6 +80,7 @@ "column.authorization": "Genehmigung", "column.authorizations": "Genehmigungen", "column.custom-locations": "Benutzerdefinierte Pfade", + "column.description": "Beschreibung", "column.destination": "Ziel", "column.details": "Details", "column.email": "E-Mail", @@ -93,6 +104,8 @@ "created-on": "Erstelldatum: {date}", "dashboard": "Dashboard", "dead-host": "404 Host", + "dead-host.directory": "Gruppe", + "dead-host.no-directory": "Keine Gruppe", "dead-hosts": "404 Hosts", "dead-hosts.count": "{count} {count, plural, one {404 Host} other {404 Hosts}}", "disabled": "Deaktiviert", @@ -106,6 +119,10 @@ "domains.hsts-enabled": "HSTS aktiviert", "domains.hsts-subdomains": "HSTS Sub-domains", "domains.http2-support": "HTTP/2 Support", + "domains.http3-support": "HTTP/3 Support", + "domains.mtls-bypass-auth-request": "Auth Request bei mTLS überspringen", + "domains.mtls-verify-client-optional": "mTLS optional machen", + "domains.reuse-key": "Schlüssel wiederverwenden (nicht empfohlen)", "domains.trust-forwarded-proto": "Upstream Forwarded-Proto-Header vertrauen", "domains.use-dns": "Nutze DNS Challenge", "email-address": "E-Mail-Adresse", @@ -115,8 +132,10 @@ "error.access.at-least-one": "Entweder eine Genehmigung oder eine Zugriffsregel ist erforderlich.", "error.access.duplicate-usernames": "Autorisierung Benutzernamen müssen eindeutig sein", "error.invalid-auth": "Ungültige E-Mail-Adresse oder Passwort", + "error.invalid-code": "Ungültiger Verifizierungscode", "error.invalid-domain": "Ungültige Domain: {domain}", "error.invalid-email": "Ungültige E-Mail-Adresse", + "error.invalid-upstream-url": "Muss eine gültige URL mit Schema, Host und Port sein (ohne Pfad)", "error.max-character-length": "Die maximale Länge beträgt {max} Zeichen{max, plural, one {} other {s}}", "error.max-domains": "Zu viele Domains, maximal sind {max}", "error.maximum": "Maximum ist {max}", @@ -125,11 +144,23 @@ "error.passwords-must-match": "Passwörter müssen übereinstimmen", "error.required": "Dies ist erforderlich.", "expires.on": "Ablauf am: {date}", + "footer.github": "Repository auf Github", "footer.github-fork": "Fork me on Github", + "help": "Hilfe", + "host.auth-request": "Auth Request", + "host.auth-request-upstream": "Auth Request Upstream", "host.flags.block-exploits": "Gängige Exploits blockieren", "host.flags.cache-assets": "Cache Assets", + "host.flags.disable-crowdsec-appsec": "Crowdsec Appsec deaktivieren", + "host.flags.disable-request-buffering": "Request Buffering deaktivieren", + "host.flags.disable-response-buffering": "Response Buffering deaktivieren", + "host.flags.fancyindex": "fancyindex aktivieren", + "host.flags.hsts-enabled": "HSTS aktiviert (2 Jahre)", + "host.flags.hsts-subdomains": "HSTS Subdomains+Preload", "host.flags.preserve-path": "Pfad beibehalten", "host.flags.protocols": "Protokolle", + "host.flags.send-noindex": "noindex-Header senden und einige Bot-User-Agents blockieren", + "host.flags.upstream-compression": "Komprimierung durch Upstream aktivieren", "host.flags.websockets-upgrade": "Websockets Support", "host.forward-port": "Forward Port", "host.forward-scheme": "Schema", @@ -147,6 +178,7 @@ "login.totp-description": "Gib den Code aus deiner Authenticator-App ein", "login.totp-title": "MFA", "login.totp-verify": "Verifizieren", + "logout-other-devices": "Dadurch werden alle anderen Geräte ausgeloggt.", "logs": "Logs", "logs.channel.access": "Zugriff", "logs.channel.error": "Fehler", @@ -168,9 +200,11 @@ "mfa.regenerate-backup": "Backup-Codes neu generieren", "mfa.regenerate-instructions": "Gib einen Verifizierungscode ein, um neue Backup-Codes zu generieren. Deine alten Codes werden dadurch ungültig.", "mfa.title": "MFA", + "mtls-certificate": "mTLS-Zertifikat", "nginx-config.label": "Benutzerdefinierte Nginx Konfiguration", "nginx-config.placeholder": "# Geben Sie hier Ihre benutzerdefinierte Nginx-Konfiguration auf eigene Gefahr ein!", "no-permission-error": "Sie haben keinen Zugriff, um dies anzuzeigen.", + "no-tls": "Kein TLS", "notfound.action": "Zur Startseite", "notfound.content": "Es tut uns leid, aber die von Ihnen gesuchte Seite wurde nicht gefunden.", "notfound.title": "Ups... Sie haben eine Fehlerseite gefunden", @@ -180,6 +214,7 @@ "notification.object-enabled": "{object} wurde aktiviert", "notification.object-renewed": "{object} wurde erneuert", "notification.object-saved": "{object} wurde gespeichert", + "notification.object-updated": "{object} wurde aktualisiert", "notification.success": "Erfolgreich", "object.actions-title": "{object} #{id}", "object.add": "{object} hinzufügen", @@ -196,6 +231,7 @@ "offline": "Offline", "online": "Online", "options": "Optionen", + "or": "oder", "password": "Passwort", "password.generate": "Zufälliges Passwort generieren", "password.hide": "Passwort verstecken", @@ -207,13 +243,21 @@ "permissions.visibility.title": "Objektsichtbarkeit", "permissions.visibility.user": "Nur erstellte Elemente", "proxy-host": "Proxy Host", + "proxy-host.access-lists": "Zugriffslisten", + "proxy-host.directory": "Gruppe", "proxy-host.forward-host": "Forward Hostname / IP", + "proxy-host.forward-host-path": "Forward Hostname / IP / Pfad", + "proxy-host.global-access-lists": "Globale Zugriffslisten", + "proxy-host.location-no-trailing-slash-warning": "Ein Pfad ohne abschließenden Schrägstrich wird als Byte-Präfix und nicht als Pfadsegment abgeglichen. Das kann zu ungewollt vielen Treffern und/oder Path Traversal führen. Füge einen abschließenden Schrägstrich hinzu, außer du willst bewusst nach Präfix abgleichen.", + "proxy-host.no-directory": "Keine Gruppe", "proxy-hosts": "Proxy Hosts", "proxy-hosts.count": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}", "public": "Öffentlich", "redirection-host": "Redirection Host", + "redirection-host.directory": "Gruppe", "redirection-host.forward-domain": "Forward Domain", "redirection-host.forward-http-code": "HTTP Code", + "redirection-host.no-directory": "Keine Gruppe", "redirection-hosts": "Redirection Hosts", "redirection-hosts.count": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}", "redirection-hosts.http-code.300": "300 Mehrere Auswahlmöglichkeiten", @@ -235,17 +279,28 @@ "settings.default-site.html": "Benutzerdefinierte HTML", "settings.default-site.html.placeholder": "", "settings.default-site.redirect": "Weiterleitung", + "settings.default-site.status": "HTTP-Statuscode (leer lassen für 200)", "setup.preamble": "Beginnen Sie mit der Erstellung Ihres Administratorkontos.", "setup.title": "Willkommen!", "sign-in": "Anmelden", + "sign-in-with-oidc": "Mit OIDC anmelden", "ssl-certificate": "TLS-Zertifikate", "stream": "Stream", + "stream.description": "Beschreibung", + "stream.description.placeholder": "Optionale Beschreibung, um diesen Stream zu erkennen", + "stream.directory": "Gruppe", "stream.forward-host": "Forward Host", "stream.forward-host.placeholder": "example.com oder 10.0.0.1 oder 2001:db8:3333:4444:5555:6666:7777:8888", "stream.incoming-port": "Incoming Port", + "stream.no-directory": "Keine Gruppe", "streams": "Streams", "streams.count": "{count} {count, plural, one {Stream} other {Streams}}", + "streams.pp": "Proxy Protocol", + "streams.pp.off": "Aus", + "streams.pp.v1": "Proxy Protocol v1", + "streams.pp.v2": "Proxy Protocol v2", "streams.tcp": "TCP", + "streams.tls": "TLS zum Upstream", "streams.udp": "UDP", "test": "Test", "totp.disable": "TOTP deaktivieren", @@ -266,13 +321,19 @@ "user.change-password": "Passwort ändern", "user.confirm-password": "Passwort wiederholen", "user.current-password": "Aktuelles Passwort", + "user.deleted": "Gelöschtes Konto", "user.edit-profile": "Profil bearbeiten", "user.full-name": "Name", "user.login-as": "Einloggen als {name}", "user.logout": "Ausloggen", + "user.logout-everywhere": "Überall ausloggen", "user.mfa": "MFA", "user.new-password": "Neues Password", "user.nickname": "Nickname", + "user.reset-mfa": "MFA zurücksetzen", + "user.reset-mfa.content": "Dadurch wird MFA für dieses Konto ohne Wiederherstellungscode deaktiviert. Es kann danach erneut eingerichtet werden. Fortfahren?", + "user.revoke-sessions": "Sitzungen widerrufen", + "user.revoke-sessions.content": "Dadurch wird dieses Konto auf allen Geräten aus allen Sitzungen ausgeloggt. Fortfahren?", "user.set-password": "Passwort setzen", "user.set-permissions": "Berechtigungen für {name} setzen", "user.switch-dark": "Zum Dark Mode wechseln", diff --git a/frontend/translations/ui/en.json b/frontend/translations/ui/en.json index 80ae8bef28..273133a393 100644 --- a/frontend/translations/ui/en.json +++ b/frontend/translations/ui/en.json @@ -144,7 +144,7 @@ "error.passwords-must-match": "Passwords must match", "error.required": "This is required", "expires.on": "Expires: {date}", - "footer.github": "Repo on Github", + "footer.github": "Repository on Github", "footer.github-fork": "Fork me on Github", "help": "Help", "host.auth-request": "Auth Request", diff --git a/frontend/translations/ui/nl.json b/frontend/translations/ui/nl.json index 8779a97b0f..478f0c668e 100644 --- a/frontend/translations/ui/nl.json +++ b/frontend/translations/ui/nl.json @@ -144,7 +144,7 @@ "error.passwords-must-match": "Wachtwoorden moeten overeenkomen", "error.required": "Dit is verplicht", "expires.on": "Verloopt: {date}", - "footer.github": "Repo op Github", + "footer.github": "Repository op Github", "footer.github-fork": "Maak een Fork op Github", "help": "Hulp", "host.auth-request": "Auth-verzoeken", From 18773e971871e7fa92f4f07cdad54c83c9939e7e Mon Sep 17 00:00:00 2001 From: Zoey Date: Sat, 10 Oct 2026 17:48:17 +0200 Subject: [PATCH 27/62] small ui changes (navbar) --- frontend/src/components/NavLink.jsx | 4 +-- frontend/src/components/SiteHeader.jsx | 2 +- frontend/src/components/SiteMenu.jsx | 50 +++++++++++++++++++------- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/NavLink.jsx b/frontend/src/components/NavLink.jsx index 8bd2ef43c3..5c90b98d6b 100644 --- a/frontend/src/components/NavLink.jsx +++ b/frontend/src/components/NavLink.jsx @@ -1,6 +1,6 @@ import { useNavigate } from "react-router"; -export function NavLink({ children, to, href, isDropdownItem, onClick }) { +export function NavLink({ children, to, href, isDropdownItem, active, onClick }) { const navigate = useNavigate(); if (href) { @@ -19,7 +19,7 @@ export function NavLink({ children, to, href, isDropdownItem, onClick }) { return ( { e.preventDefault(); diff --git a/frontend/src/components/SiteHeader.jsx b/frontend/src/components/SiteHeader.jsx index 97d1e084f7..7e5e9be6a5 100644 --- a/frontend/src/components/SiteHeader.jsx +++ b/frontend/src/components/SiteHeader.jsx @@ -12,7 +12,7 @@ export function SiteHeader() { const { logout, logoutEverywhere } = useAuthState(); return ( -
+
-
- {item.items?.map((subitem, idx) => ( - - - {React.createElement(subitem.icon, { width: 18 })} - - - - ))} -
- - + +
+ {item.items?.map((subitem, idx) => ( + + + {React.createElement(subitem.icon, { width: 18 })} + + + + ))} +
+ ); export function SiteMenu() { @@ -195,19 +181,30 @@ export function SiteMenu() {
    - {[ - ...menuItems, - ...(user?.goaccess - ? [ - { - href: "/goaccess", - icon: IconChartBar, - label: "GoAccess", - permissionSection: ADMIN, - }, - ] - : []), - ].map((item) => getMenuItem(item, closeMenu, pathname))} + {menuGroups + .map((group) => + group.filter((item) => + item.goaccess + ? user?.goaccess + : hasPermission( + item.permissionSection, + item.permission || VIEW, + user?.permissions, + user?.roles, + ), + ), + ) + .filter((group) => group.length) + .map((group, idx) => ( + + {idx > 0 && ( +
  • +
    +
  • + )} + {group.map((item) => getMenuItem(item, closeMenu, pathname))} +
    + ))}
From cb73eda31d0b415e1c818213b91a4907635c6564 Mon Sep 17 00:00:00 2001 From: Zoey Date: Sat, 10 Oct 2026 19:08:54 +0200 Subject: [PATCH 30/62] add satisfy any warning --- frontend/src/components/Form/AccessFields.jsx | 44 +++++++++++++++++-- .../src/components/Form/LocationsFields.jsx | 1 + frontend/src/modals/ProxyHostModal.jsx | 1 + frontend/translations/ui/de.json | 1 + frontend/translations/ui/en.json | 1 + frontend/translations/ui/nl.json | 1 + 6 files changed, 46 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Form/AccessFields.jsx b/frontend/src/components/Form/AccessFields.jsx index 7d151747c4..188334af49 100644 --- a/frontend/src/components/Form/AccessFields.jsx +++ b/frontend/src/components/Form/AccessFields.jsx @@ -1,4 +1,12 @@ -import { IconArrowDown, IconArrowUp, IconLock, IconLockOpen2, IconWorld, IconX } from "@tabler/icons-react"; +import { + IconAlertTriangle, + IconArrowDown, + IconArrowUp, + IconLock, + IconLockOpen2, + IconWorld, + IconX, +} from "@tabler/icons-react"; import { useFormikContext } from "formik"; import { useState } from "react"; import Select, { components } from "react-select"; @@ -26,10 +34,18 @@ const TypeOption = (props) => ( ); -export function AccessFields({ initialAccessListType, location, initialAccessListIds, name, typeFieldName, onChange }) { +export function AccessFields({ + initialAccessListType, + location, + initialAccessListIds, + name, + typeFieldName, + onChange, + authRequest, +}) { const [values, setValues] = useState(initialAccessListIds || []); const [aclValue, setAclValue] = useState(initialAccessListType); - const { setFieldValue } = useFormikContext(); + const { values: formValues, setFieldValue } = useFormikContext(); const { isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]); const createDefaultItem = (item) => ({ @@ -133,6 +149,15 @@ export function AccessFields({ initialAccessListType, location, initialAccessLis applyUpdatedValues(newValues); }; + const acls = ( + { + custom: values, + global: formValues.npmplusAccessListType === "custom" ? formValues.npmplusAccessListIds : [], + }[aclValue] ?? [] + ) + .map((id) => data?.find((acl) => acl.id === id)) + .filter(Boolean); + return (
{isLoading ?
: null} @@ -235,6 +260,19 @@ export function AccessFields({ initialAccessListType, location, initialAccessLis )} ) : null} + {authRequest && + authRequest !== "none" && + acls[0]?.satisfyAny && + acls.some( + (acl) => + acl.items?.length > 0 || + acl.clients?.some((client) => client.address?.trim().toLowerCase() !== "all"), + ) && ( +

+ + +

+ )}
); } diff --git a/frontend/src/components/Form/LocationsFields.jsx b/frontend/src/components/Form/LocationsFields.jsx index f7f1d17bd7..4c41edf624 100644 --- a/frontend/src/components/Form/LocationsFields.jsx +++ b/frontend/src/components/Form/LocationsFields.jsx @@ -626,6 +626,7 @@ export function LocationsFields({ initialValues, name = "locations" }) { name={`locations[${idx}].npmplusAccessListIds`} typeFieldName={`locations[${idx}].npmplusAccessListType`} onChange={(changes) => handleAccessFieldsChange(idx, changes)} + authRequest={item.npmplusAuthRequest} />
diff --git a/frontend/src/modals/ProxyHostModal.jsx b/frontend/src/modals/ProxyHostModal.jsx index af6245d2df..999238c756 100644 --- a/frontend/src/modals/ProxyHostModal.jsx +++ b/frontend/src/modals/ProxyHostModal.jsx @@ -670,6 +670,7 @@ const ProxyHostModal = EasyModal.create(({ id, isClone = false, visible, remove initialAccessListIds={data?.npmplusAccessListIds || []} name="npmplusAccessListIds" typeFieldName="npmplusAccessListType" + authRequest={values.npmplusAuthRequest} />
diff --git a/frontend/translations/ui/de.json b/frontend/translations/ui/de.json index 868efd0b2a..eb9756d892 100644 --- a/frontend/translations/ui/de.json +++ b/frontend/translations/ui/de.json @@ -16,6 +16,7 @@ "access-list.public.subtitle": "Keine Authentifizierung erforderlich", "access-list.rule-source.placeholder": "192.168.1.100 oder 192.168.1.0/24 oder 2001:0db8::/32", "access-list.satisfy-any": "Satisfy Any", + "access-list.satisfy-any-warning": "\"{name}\" nutzt Satisfy Any. Das gilt auch für Auth Request: Eine passende Regel oder ein gültiger User überspringt es.", "access-list.subtitle": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Regel} other {Regeln}} - Erstellt: {date}", "access-lists": "Zugriffslisten", "access-lists.count": "{count} {count, plural, one {Zugriffsliste} other {Zugriffslisten}}", diff --git a/frontend/translations/ui/en.json b/frontend/translations/ui/en.json index 273133a393..0baa92ac27 100644 --- a/frontend/translations/ui/en.json +++ b/frontend/translations/ui/en.json @@ -16,6 +16,7 @@ "access-list.public.subtitle": "No basic auth required", "access-list.rule-source.placeholder": "192.168.1.100 or 192.168.1.0/24 or 2001:0db8::/32", "access-list.satisfy-any": "Satisfy Any", + "access-list.satisfy-any-warning": "\"{name}\" uses Satisfy Any, which also applies to Auth Request: a matching rule or a valid user skips it.", "access-list.subtitle": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Rule} other {Rules}} - Created: {date}", "access-lists": "Access Lists", "access-lists.count": "{count} {count, plural, one {Access List} other {Access Lists}}", diff --git a/frontend/translations/ui/nl.json b/frontend/translations/ui/nl.json index 478f0c668e..8bc0bc7dbc 100644 --- a/frontend/translations/ui/nl.json +++ b/frontend/translations/ui/nl.json @@ -16,6 +16,7 @@ "access-list.public.subtitle": "Geen basisautentificatie vereist", "access-list.rule-source.placeholder": "192.168.1.100 of 192.168.1.0/24 of 2001:0db8::/32", "access-list.satisfy-any": "Voldoen aan minstens een", + "access-list.satisfy-any-warning": "\"{name}\" gebruikt \"Voldoen aan minstens een\". Dit geldt ook voor auth-verzoeken: een overeenkomende regel of een geldige gebruiker slaat deze over.", "access-list.subtitle": "{users} {users, plural, one {Gebruiker} other {Gebruikers}}, {rules} {rules, plural, one {Regel} other {Regels}} - Aangemaakt: {date}", "access-lists": "Toegangslijsten", "access-lists.count": "{count} {count, plural, one {Toegangslijst} other {Toegangslijsten}}", From 406b1eace2e8540ba027c6f2a125fcf363d996be Mon Sep 17 00:00:00 2001 From: Zoey Date: Sat, 10 Oct 2026 19:28:01 +0200 Subject: [PATCH 31/62] use hooks for inital values --- frontend/src/api/backend/base.js | 2 +- frontend/src/api/backend/getToken.js | 4 +- frontend/src/api/backend/mfa.js | 4 +- frontend/src/api/backend/totp.js | 8 +-- frontend/src/api/backend/updateAccessList.js | 2 +- frontend/src/api/backend/updateAuth.js | 15 ++-- frontend/src/api/backend/updateDeadHost.js | 2 +- frontend/src/api/backend/updateProxyHost.js | 2 +- .../src/api/backend/updateRedirectionHost.js | 2 +- frontend/src/api/backend/updateStream.js | 2 +- frontend/src/api/backend/updateUser.js | 2 +- .../components/Form/AccessClientFields.jsx | 7 +- frontend/src/components/Form/AccessFields.jsx | 2 +- .../src/components/Form/BasicAuthFields.jsx | 2 +- .../src/components/Form/LocationsFields.jsx | 6 +- frontend/src/context/AuthContext.jsx | 4 +- frontend/src/hooks/useAccessList.js | 2 + frontend/src/hooks/useProxyHost.js | 3 +- frontend/src/hooks/useStream.js | 3 + frontend/src/modals/AccessListModal.jsx | 16 ++--- .../src/modals/CustomCertificateModal.jsx | 2 +- frontend/src/modals/DeadHostModal.jsx | 8 +-- frontend/src/modals/MfaModal.jsx | 6 +- frontend/src/modals/ProxyHostModal.jsx | 70 ++++++++----------- frontend/src/modals/RedirectionHostModal.jsx | 30 ++++---- frontend/src/modals/StreamModal.jsx | 13 ++-- frontend/src/modals/UserModal.jsx | 3 +- frontend/src/pages/Login/index.jsx | 2 +- frontend/src/pages/Settings/DefaultSite.jsx | 2 +- frontend/src/pages/Setup/index.jsx | 8 +-- 30 files changed, 111 insertions(+), 123 deletions(-) diff --git a/frontend/src/api/backend/base.js b/frontend/src/api/backend/base.js index e76072cf93..bd5ae5feb0 100644 --- a/frontend/src/api/backend/base.js +++ b/frontend/src/api/backend/base.js @@ -11,7 +11,7 @@ function buildUrl({ url, params }) { const baseUrl = `/api/${endpoint}`; const apiUrl = queryString.stringifyUrl({ url: baseUrl, - query: decamelizeKeys(params), + query: params, }); return apiUrl; } diff --git a/frontend/src/api/backend/getToken.js b/frontend/src/api/backend/getToken.js index e77bfd5d9f..fb3da95e9c 100644 --- a/frontend/src/api/backend/getToken.js +++ b/frontend/src/api/backend/getToken.js @@ -7,9 +7,9 @@ export async function getToken(identity, secret) { }); } -export async function verifyTotp(code) { +export async function verifyTotp(data) { return await api.post({ url: "/tokens/totp", - data: { code }, + data, }); } diff --git a/frontend/src/api/backend/mfa.js b/frontend/src/api/backend/mfa.js index cca05a487e..6a7cfc137e 100644 --- a/frontend/src/api/backend/mfa.js +++ b/frontend/src/api/backend/mfa.js @@ -6,10 +6,10 @@ export async function getMfaStatus(userId) { }); } -export async function regenerateBackupCodes(userId, code) { +export async function regenerateBackupCodes(userId, data) { return await api.post({ url: `/users/${userId}/mfa/backup-codes`, - data: { code }, + data, }); } diff --git a/frontend/src/api/backend/totp.js b/frontend/src/api/backend/totp.js index ec8ff4795d..bd2c1eec40 100644 --- a/frontend/src/api/backend/totp.js +++ b/frontend/src/api/backend/totp.js @@ -6,16 +6,16 @@ export async function startTotpSetup(userId) { }); } -export async function enableTotp(userId, code) { +export async function enableTotp(userId, data) { return await api.post({ url: `/users/${userId}/mfa/totp/enable`, - data: { code }, + data, }); } -export async function disableTotp(userId, code) { +export async function disableTotp(userId, data) { return await api.post({ url: `/users/${userId}/mfa/totp/disable`, - data: { code }, + data, }); } diff --git a/frontend/src/api/backend/updateAccessList.js b/frontend/src/api/backend/updateAccessList.js index 0cde55ee30..3da20fb935 100644 --- a/frontend/src/api/backend/updateAccessList.js +++ b/frontend/src/api/backend/updateAccessList.js @@ -2,7 +2,7 @@ import * as api from "./base"; export async function updateAccessList(item) { // Remove readonly fields - const { id, createdOn: _, modifiedOn: __, ...data } = item; + const { id, ...data } = item; return await api.put({ url: `/nginx/access-lists/${id}`, diff --git a/frontend/src/api/backend/updateAuth.js b/frontend/src/api/backend/updateAuth.js index 94d76e77ea..ada2af875f 100644 --- a/frontend/src/api/backend/updateAuth.js +++ b/frontend/src/api/backend/updateAuth.js @@ -1,17 +1,12 @@ import * as api from "./base"; export async function updateAuth(userId, newPassword, current) { - const data = { - type: "password", - current, - secret: newPassword, - }; - if (userId === "me") { - data.current = current; - } - return await api.put({ url: `/users/${userId}/auth`, - data, + data: { + type: "password", + current, + secret: newPassword, + }, }); } diff --git a/frontend/src/api/backend/updateDeadHost.js b/frontend/src/api/backend/updateDeadHost.js index 004aebd160..b8b16e1bda 100644 --- a/frontend/src/api/backend/updateDeadHost.js +++ b/frontend/src/api/backend/updateDeadHost.js @@ -2,7 +2,7 @@ import * as api from "./base"; export async function updateDeadHost(item) { // Remove readonly fields - const { id, createdOn: _, modifiedOn: __, ...data } = item; + const { id, ...data } = item; return await api.put({ url: `/nginx/dead-hosts/${id}`, diff --git a/frontend/src/api/backend/updateProxyHost.js b/frontend/src/api/backend/updateProxyHost.js index 95e6a30163..dbd9403f9f 100644 --- a/frontend/src/api/backend/updateProxyHost.js +++ b/frontend/src/api/backend/updateProxyHost.js @@ -2,7 +2,7 @@ import * as api from "./base"; export async function updateProxyHost(item) { // Remove readonly fields - const { id, createdOn: _, modifiedOn: __, ...data } = item; + const { id, ...data } = item; return await api.put({ url: `/nginx/proxy-hosts/${id}`, diff --git a/frontend/src/api/backend/updateRedirectionHost.js b/frontend/src/api/backend/updateRedirectionHost.js index 2e411f596c..b3b620f90f 100644 --- a/frontend/src/api/backend/updateRedirectionHost.js +++ b/frontend/src/api/backend/updateRedirectionHost.js @@ -2,7 +2,7 @@ import * as api from "./base"; export async function updateRedirectionHost(item) { // Remove readonly fields - const { id, createdOn: _, modifiedOn: __, ...data } = item; + const { id, ...data } = item; return await api.put({ url: `/nginx/redirection-hosts/${id}`, diff --git a/frontend/src/api/backend/updateStream.js b/frontend/src/api/backend/updateStream.js index f01a5af658..9a81651edc 100644 --- a/frontend/src/api/backend/updateStream.js +++ b/frontend/src/api/backend/updateStream.js @@ -2,7 +2,7 @@ import * as api from "./base"; export async function updateStream(item) { // Remove readonly fields - const { id, createdOn: _, modifiedOn: __, ...data } = item; + const { id, ...data } = item; return await api.put({ url: `/nginx/streams/${id}`, diff --git a/frontend/src/api/backend/updateUser.js b/frontend/src/api/backend/updateUser.js index a0ed6ebe95..e27adc7fb4 100644 --- a/frontend/src/api/backend/updateUser.js +++ b/frontend/src/api/backend/updateUser.js @@ -2,7 +2,7 @@ import * as api from "./base"; export async function updateUser(item) { // Remove readonly fields - const { id, createdOn: _, modifiedOn: __, ...data } = item; + const { id, ...data } = item; return await api.put({ url: `/users/${id}`, diff --git a/frontend/src/components/Form/AccessClientFields.jsx b/frontend/src/components/Form/AccessClientFields.jsx index 6dfec0e3b8..e01bda80ec 100644 --- a/frontend/src/components/Form/AccessClientFields.jsx +++ b/frontend/src/components/Form/AccessClientFields.jsx @@ -5,11 +5,10 @@ import { useState } from "react"; import { intl, T } from "src/locale"; export function AccessClientFields({ initialValues, name = "clients" }) { - const initial = initialValues || []; const [values, setValues] = useState( - initial.at(-1)?.address?.trim().toLowerCase() === "all" - ? initial - : [...initial, { directive: "deny", address: "all" }], + initialValues.at(-1)?.address?.trim().toLowerCase() === "all" + ? initialValues + : [...initialValues, { directive: "deny", address: "all" }], ); const { setFieldValue } = useFormikContext(); diff --git a/frontend/src/components/Form/AccessFields.jsx b/frontend/src/components/Form/AccessFields.jsx index 188334af49..efaf17de37 100644 --- a/frontend/src/components/Form/AccessFields.jsx +++ b/frontend/src/components/Form/AccessFields.jsx @@ -43,7 +43,7 @@ export function AccessFields({ onChange, authRequest, }) { - const [values, setValues] = useState(initialAccessListIds || []); + const [values, setValues] = useState(initialAccessListIds); const [aclValue, setAclValue] = useState(initialAccessListType); const { values: formValues, setFieldValue } = useFormikContext(); const { isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]); diff --git a/frontend/src/components/Form/BasicAuthFields.jsx b/frontend/src/components/Form/BasicAuthFields.jsx index 7d5c201add..9c3c363876 100644 --- a/frontend/src/components/Form/BasicAuthFields.jsx +++ b/frontend/src/components/Form/BasicAuthFields.jsx @@ -6,7 +6,7 @@ import { T } from "src/locale"; const blankItem = { username: "", password: "" }; export function BasicAuthFields({ initialValues, name = "items" }) { - const [values, setValues] = useState(initialValues || []); + const [values, setValues] = useState(initialValues); const [revealed, setRevealed] = useState({}); const { setFieldValue } = useFormikContext(); diff --git a/frontend/src/components/Form/LocationsFields.jsx b/frontend/src/components/Form/LocationsFields.jsx index 4c41edf624..7a6353176b 100644 --- a/frontend/src/components/Form/LocationsFields.jsx +++ b/frontend/src/components/Form/LocationsFields.jsx @@ -23,7 +23,7 @@ export function LocationsFields({ initialValues, name = "locations" }) { uiKey: nextUiKey.current++, }); - const [values, setValues] = useState((initialValues || []).map(createUiLocation)); + const [values, setValues] = useState(initialValues.map(createUiLocation)); const { setFieldValue } = useFormikContext(); const [advVisible, setAdvVisible] = useState([]); const [expanded, setExpanded] = useState([]); @@ -620,9 +620,9 @@ export function LocationsFields({ initialValues, name = "locations" }) { handleAccessFieldsChange(idx, changes)} diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index 51213bb5fe..d9f2c936df 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -27,11 +27,11 @@ function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }) { handleTokenUpdate(response); }; - const submitTotp = async (code) => { + const submitTotp = async (data) => { if (!totpChallenge) { throw new Error("No TOTP challenge pending"); } - const response = await verifyTotp(code); + const response = await verifyTotp(data); handleTokenUpdate(response); }; diff --git a/frontend/src/hooks/useAccessList.js b/frontend/src/hooks/useAccessList.js index 9667ea8ee0..889fb85cee 100644 --- a/frontend/src/hooks/useAccessList.js +++ b/frontend/src/hooks/useAccessList.js @@ -11,6 +11,8 @@ const fetchAccessList = (id, expand) => { name: "", satisfyAny: false, passAuth: false, + items: [], + clients: [], }); } return getAccessList(id, expand); diff --git a/frontend/src/hooks/useProxyHost.js b/frontend/src/hooks/useProxyHost.js index 9dc4d48511..3801eb1c99 100644 --- a/frontend/src/hooks/useProxyHost.js +++ b/frontend/src/hooks/useProxyHost.js @@ -12,6 +12,7 @@ const fetchProxyHost = (id) => { forwardHost: "", forwardPort: 0, npmplusAccessListIds: [], + locations: [], certificateId: 0, sslForced: false, advancedConfig: "", @@ -21,7 +22,7 @@ const fetchProxyHost = (id) => { npmplusMtlsVerifyClientOptional: false, npmplusMtlsBypassAuthRequest: false, npmplusHttp3Support: false, - forwardScheme: "", + forwardScheme: "http", enabled: true, hstsEnabled: false, hstsSubdomains: false, diff --git a/frontend/src/hooks/useStream.js b/frontend/src/hooks/useStream.js index 9c9ef3666d..517790f870 100644 --- a/frontend/src/hooks/useStream.js +++ b/frontend/src/hooks/useStream.js @@ -8,6 +8,9 @@ const fetchStream = (id) => { createdOn: "", modifiedOn: "", ownerUserId: 0, + incomingPort: "", + forwardingHost: "", + forwardingPort: "", tcpForwarding: true, udpForwarding: false, npmplusProxyProtocolForwarding: 0, diff --git a/frontend/src/modals/AccessListModal.jsx b/frontend/src/modals/AccessListModal.jsx index 34a3739248..f43cf6ec72 100644 --- a/frontend/src/modals/AccessListModal.jsx +++ b/frontend/src/modals/AccessListModal.jsx @@ -22,7 +22,7 @@ const AccessListModal = EasyModal.create(({ id, visible, remove }) => { const validate = (values) => { // either Auths or Clients must be defined - if (values.items?.length === 0 && values.clients?.length === 0) { + if (values.items.length === 0 && values.clients.length === 0) { return intl.formatMessage({ id: "error.access.at-least-one" }); } @@ -48,19 +48,19 @@ const AccessListModal = EasyModal.create(({ id, visible, remove }) => { setIsSubmitting(true); setErrorMsg(null); - const { ...payload } = { + const payload = { id: id === "new" ? undefined : id, ...values, }; // Filter out "items" to only use the "username" and "password" fields - payload.items = (values.items || []).map((i) => ({ + payload.items = values.items.map((i) => ({ username: i.username, password: i.password, })); // Filter out "clients" to only use the "directive" and "address" fields - payload.clients = (values.clients || []).map((i) => ({ + payload.clients = values.clients.map((i) => ({ directive: i.directive, address: i.address, })); @@ -95,8 +95,8 @@ const AccessListModal = EasyModal.create(({ id, visible, remove }) => { name: data?.name, satisfyAny: data?.satisfyAny, passAuth: data?.passAuth, - items: data?.items || [], - clients: data?.clients || [], + items: data?.items, + clients: data?.clients, }} onSubmit={onSubmit} > @@ -257,10 +257,10 @@ const AccessListModal = EasyModal.create(({ id, visible, remove }) => {
- +
- +
diff --git a/frontend/src/modals/CustomCertificateModal.jsx b/frontend/src/modals/CustomCertificateModal.jsx index ed4b46ee7a..47c662b362 100644 --- a/frontend/src/modals/CustomCertificateModal.jsx +++ b/frontend/src/modals/CustomCertificateModal.jsx @@ -39,7 +39,7 @@ const CustomCertificateModal = EasyModal.create(({ visible, remove, cert, provid // Validate await validateCertificate(formData); - if (isEdit && cert) { + if (isEdit) { // Upload the certificates to the exiting certificate await uploadCertificate(cert.id, formData); } else { diff --git a/frontend/src/modals/DeadHostModal.jsx b/frontend/src/modals/DeadHostModal.jsx index 961a4e734b..5f01f2adba 100644 --- a/frontend/src/modals/DeadHostModal.jsx +++ b/frontend/src/modals/DeadHostModal.jsx @@ -34,7 +34,7 @@ const DeadHostModal = EasyModal.create(({ id, visible, remove }) => { setIsSubmitting(true); setErrorMsg(null); - const { ...payload } = { + const payload = { id: id === "new" ? undefined : id, ...values, }; @@ -82,9 +82,9 @@ const DeadHostModal = EasyModal.create(({ id, visible, remove }) => { npmplusHttp3Support: data?.npmplusHttp3Support, hstsEnabled: data?.hstsEnabled, hstsSubdomains: data?.hstsSubdomains, - npmplusDirectory: data?.npmplusDirectory || "", - npmplusMtlsCertificateId: data?.npmplusMtlsCertificateId || 0, - npmplusMtlsVerifyClientOptional: data?.npmplusMtlsVerifyClientOptional || false, + npmplusDirectory: data?.npmplusDirectory, + npmplusMtlsCertificateId: data?.npmplusMtlsCertificateId, + npmplusMtlsVerifyClientOptional: data?.npmplusMtlsVerifyClientOptional, }} onSubmit={onSubmit} > diff --git a/frontend/src/modals/MfaModal.jsx b/frontend/src/modals/MfaModal.jsx index bf84afbbad..8b876f9e40 100644 --- a/frontend/src/modals/MfaModal.jsx +++ b/frontend/src/modals/MfaModal.jsx @@ -57,7 +57,7 @@ const MfaModal = EasyModal.create(({ id, visible, remove }) => { setError(null); setIsSubmitting(true); try { - const result = await enableTotp(id, values.code); + const result = await enableTotp(id, values); if (result.backupCodes) { setBackupCodes(result.backupCodes); setStep("backup"); @@ -74,7 +74,7 @@ const MfaModal = EasyModal.create(({ id, visible, remove }) => { setError(null); setIsSubmitting(true); try { - await disableTotp(id, values.code); + await disableTotp(id, values); setIsEnabled(false); setStep("status"); } catch (err) { @@ -87,7 +87,7 @@ const MfaModal = EasyModal.create(({ id, visible, remove }) => { setError(null); setIsSubmitting(true); try { - const result = await regenerateBackupCodes(id, values.code); + const result = await regenerateBackupCodes(id, values); setBackupCodes(result.backupCodes); setStep("backup"); } catch (err) { diff --git a/frontend/src/modals/ProxyHostModal.jsx b/frontend/src/modals/ProxyHostModal.jsx index 999238c756..7fbaee70fc 100644 --- a/frontend/src/modals/ProxyHostModal.jsx +++ b/frontend/src/modals/ProxyHostModal.jsx @@ -40,23 +40,15 @@ const ProxyHostModal = EasyModal.create(({ id, isClone = false, visible, remove // Set the unrestricted acls here (remove any data in their acl lists) const globalType = values.npmplusAccessListType; - let globalAclIds = values.npmplusAccessListIds || []; + let globalAclIds = values.npmplusAccessListIds; if (globalType === "public") { globalAclIds = []; } - const locations = (values.locations || []).map((loc) => { - const newLoc = { ...loc }; - if (loc.npmplusAccessListType === "global" || loc.npmplusAccessListType === "public") { - newLoc.npmplusAccessListIds = []; - } - return newLoc; - }); - const { ...payload } = { + const payload = { id: id === "new" || isClone ? undefined : id, ...values, npmplusAccessListIds: globalAclIds, - locations, forwardPort: values.forwardPort || null, }; @@ -97,36 +89,36 @@ const ProxyHostModal = EasyModal.create(({ id, isClone = false, visible, remove @@ -666,8 +658,8 @@ const ProxyHostModal = EasyModal.create(({ id, isClone = false, visible, remove
- +
{ setIsSubmitting(true); setErrorMsg(null); - const { ...payload } = { + const payload = { id: id === "new" ? undefined : id, ...values, }; @@ -77,22 +77,22 @@ const RedirectionHostModal = EasyModal.create(({ id, visible, remove }) => { diff --git a/frontend/src/modals/StreamModal.jsx b/frontend/src/modals/StreamModal.jsx index e4cbc8ca31..e52603c7c2 100644 --- a/frontend/src/modals/StreamModal.jsx +++ b/frontend/src/modals/StreamModal.jsx @@ -26,10 +26,9 @@ const StreamModal = EasyModal.create(({ id, visible, remove }) => { setIsSubmitting(true); setErrorMsg(null); - const { ...payload } = { + const payload = { id: id === "new" ? undefined : id, ...values, - forwardingPort: values.forwardingPort || null, }; setStream(payload, { @@ -76,11 +75,11 @@ const StreamModal = EasyModal.create(({ id, visible, remove }) => { npmplusProxyProtocolForwarding: data?.npmplusProxyProtocolForwarding, npmplusProxyTls: data?.npmplusProxyTls, certificateId: data?.certificateId, - npmplusAdvancedConfig: data?.npmplusAdvancedConfig || "", - npmplusDirectory: data?.npmplusDirectory || "", - npmplusMtlsCertificateId: data?.npmplusMtlsCertificateId || 0, - npmplusMtlsVerifyClientOptional: data?.npmplusMtlsVerifyClientOptional || false, - npmplusDescription: data?.npmplusDescription || "", + npmplusAdvancedConfig: data?.npmplusAdvancedConfig, + npmplusDirectory: data?.npmplusDirectory, + npmplusMtlsCertificateId: data?.npmplusMtlsCertificateId, + npmplusMtlsVerifyClientOptional: data?.npmplusMtlsVerifyClientOptional, + npmplusDescription: data?.npmplusDescription, }} onSubmit={onSubmit} > diff --git a/frontend/src/modals/UserModal.jsx b/frontend/src/modals/UserModal.jsx index 1e56cce5f3..36cda5e519 100644 --- a/frontend/src/modals/UserModal.jsx +++ b/frontend/src/modals/UserModal.jsx @@ -43,7 +43,7 @@ const UserModal = EasyModal.create(({ id, visible, remove }) => { setIsSubmitting(true); setErrorMsg(null); - const { ...payload } = { + const payload = { id: id === "new" ? undefined : id, roles: [], ...values, @@ -51,7 +51,6 @@ const UserModal = EasyModal.create(({ id, visible, remove }) => { if (data?.id === currentUser?.id) { // Prevent user from locking themselves out - delete payload.isDisabled; delete payload.roles; } else if (payload.isAdmin) { payload.roles = ["admin"]; diff --git a/frontend/src/pages/Login/index.jsx b/frontend/src/pages/Login/index.jsx index cafa712298..f2ae3581a5 100644 --- a/frontend/src/pages/Login/index.jsx +++ b/frontend/src/pages/Login/index.jsx @@ -18,7 +18,7 @@ function TotpForm() { const onSubmit = async (values, { setSubmitting, resetForm }) => { setFormErr(""); try { - await submitTotp(values.code); + await submitTotp(values); } catch (err) { if (err instanceof Error) { setFormErr(err.message); diff --git a/frontend/src/pages/Settings/DefaultSite.jsx b/frontend/src/pages/Settings/DefaultSite.jsx index dade947c44..a25c976cc1 100644 --- a/frontend/src/pages/Settings/DefaultSite.jsx +++ b/frontend/src/pages/Settings/DefaultSite.jsx @@ -24,7 +24,7 @@ export default function DefaultSite() { meta: { redirect: values.redirect, html: values.html, - status: values.status ? Number(values.status) : undefined, + status: values.status || undefined, }, }; diff --git a/frontend/src/pages/Setup/index.jsx b/frontend/src/pages/Setup/index.jsx index e2a9a50499..a873688d61 100644 --- a/frontend/src/pages/Setup/index.jsx +++ b/frontend/src/pages/Setup/index.jsx @@ -22,11 +22,9 @@ export default function Setup() { const { password, ...payload } = { ...values, - ...{ - auth: { - type: "password", - secret: values.password, - }, + auth: { + type: "password", + secret: values.password, }, }; From 7df378a82a48b6c04931aa3028ae1a1556b4ab52 Mon Sep 17 00:00:00 2001 From: Zoey Date: Sat, 10 Oct 2026 22:54:10 +0200 Subject: [PATCH 32/62] frontend: code cleanups --- frontend/src/api/backend/base.js | 8 +- frontend/src/api/backend/caseConvert.js | 5 +- frontend/src/api/backend/getUser.js | 3 +- frontend/src/api/backend/refreshToken.js | 2 +- frontend/src/components/EmptyData.jsx | 2 +- frontend/src/components/Flag.jsx | 10 +-- .../components/Form/AccessClientFields.jsx | 5 +- frontend/src/components/Form/AccessFields.jsx | 22 ++---- .../src/components/Form/BasicAuthFields.jsx | 3 - .../src/components/Form/DNSProviderFields.jsx | 21 ++--- .../src/components/Form/DirectoryField.jsx | 1 - .../src/components/Form/DomainNamesField.jsx | 10 +-- .../src/components/Form/LocationsFields.jsx | 5 +- .../components/Form/SSLCertificateField.jsx | 44 ++++------- .../src/components/Form/SSLOptionsFields.jsx | 42 +++------- frontend/src/components/HasPermission.jsx | 9 +-- frontend/src/components/LocalePicker.jsx | 2 +- frontend/src/components/NavLink.jsx | 4 +- frontend/src/components/SiteFooter.jsx | 2 +- frontend/src/components/SiteMenu.jsx | 27 +++---- .../Table/Formatter/AccessListformatter.jsx | 9 +-- .../Table/Formatter/CertificateFormatter.jsx | 2 +- .../Formatter/CertificateInUseFormatter.jsx | 7 +- .../Table/Formatter/DateFormatter.jsx | 15 +--- .../Table/Formatter/DomainsFormatter.jsx | 18 ++--- .../Table/Formatter/EventFormatter.jsx | 6 +- .../Table/Formatter/RolesFormatter.jsx | 7 +- .../Table/Formatter/StatusFormatter.jsx | 2 +- .../Formatter/ValueWithDateFormatter.jsx | 8 +- frontend/src/components/Table/TableBody.jsx | 2 +- frontend/src/hooks/useAccessList.js | 3 +- frontend/src/hooks/useAccessLists.js | 3 +- frontend/src/hooks/useAuditLog.js | 3 +- frontend/src/hooks/useAuditLogs.js | 3 +- frontend/src/hooks/useCertificate.js | 3 +- frontend/src/hooks/useCertificates.js | 3 +- frontend/src/hooks/useCheckVersion.js | 3 +- frontend/src/hooks/useDeadHost.js | 3 +- frontend/src/hooks/useDeadHosts.js | 3 +- frontend/src/hooks/useDirectorySuggestions.js | 2 +- frontend/src/hooks/useDnsProviders.js | 3 +- frontend/src/hooks/useHealth.js | 3 +- frontend/src/hooks/useHostReport.js | 3 +- frontend/src/hooks/useProxyHost.js | 3 +- frontend/src/hooks/useProxyHosts.js | 3 +- frontend/src/hooks/useRedirectionHost.js | 3 +- frontend/src/hooks/useRedirectionHosts.js | 3 +- frontend/src/hooks/useSetting.js | 3 +- frontend/src/hooks/useStream.js | 3 +- frontend/src/hooks/useStreams.js | 3 +- frontend/src/hooks/useUser.js | 3 +- frontend/src/hooks/useUsers.js | 3 +- frontend/src/locale/Utils.js | 18 ++--- frontend/src/modals/AccessListModal.jsx | 17 ++-- frontend/src/modals/ChangePasswordModal.jsx | 2 +- .../src/modals/CustomCertificateModal.jsx | 38 +-------- frontend/src/modals/DNSCertificateModal.jsx | 7 +- frontend/src/modals/DeadHostModal.jsx | 13 +-- frontend/src/modals/DeleteConfirmModal.jsx | 4 +- frontend/src/modals/EventDetailsModal.jsx | 8 +- frontend/src/modals/HTTPCertificateModal.jsx | 79 ++++++++----------- frontend/src/modals/MfaModal.jsx | 14 ++-- frontend/src/modals/PermissionsModal.jsx | 13 +-- frontend/src/modals/ProxyHostModal.jsx | 42 +++------- frontend/src/modals/ReachabilityModal.jsx | 74 ++++++++--------- frontend/src/modals/RedirectionHostModal.jsx | 16 ++-- frontend/src/modals/RenewCertificateModal.jsx | 12 +-- frontend/src/modals/SetPasswordModal.jsx | 2 +- frontend/src/modals/StreamModal.jsx | 25 ++---- frontend/src/modals/UserModal.jsx | 27 +++---- frontend/src/modules/Permissions.js | 2 +- frontend/src/modules/Validations.jsx | 41 +++------- frontend/src/notifications/Msg.jsx | 10 +-- frontend/src/pages/Access/TableWrapper.jsx | 6 +- frontend/src/pages/AuditLog/TableWrapper.jsx | 8 +- frontend/src/pages/Certificates/Table.jsx | 14 ++-- .../src/pages/Certificates/TableWrapper.jsx | 24 +++--- frontend/src/pages/Login/index.jsx | 16 +--- frontend/src/pages/Nginx/DeadHosts/Table.jsx | 2 +- .../pages/Nginx/DeadHosts/TableWrapper.jsx | 6 +- frontend/src/pages/Nginx/ProxyHosts/Table.jsx | 4 +- .../pages/Nginx/ProxyHosts/TableWrapper.jsx | 6 +- .../pages/Nginx/RedirectionHosts/Table.jsx | 2 +- .../Nginx/RedirectionHosts/TableWrapper.jsx | 6 +- frontend/src/pages/Nginx/Streams/Table.jsx | 4 +- .../src/pages/Nginx/Streams/TableWrapper.jsx | 8 +- frontend/src/pages/Settings/DefaultSite.jsx | 12 +-- frontend/src/pages/Setup/index.jsx | 36 ++++----- frontend/src/pages/Users/TableWrapper.jsx | 6 +- 89 files changed, 358 insertions(+), 634 deletions(-) diff --git a/frontend/src/api/backend/base.js b/frontend/src/api/backend/base.js index bd5ae5feb0..2ae934a880 100644 --- a/frontend/src/api/backend/base.js +++ b/frontend/src/api/backend/base.js @@ -7,7 +7,7 @@ export const queryClient = new QueryClient(); const contentTypeHeader = "Content-Type"; function buildUrl({ url, params }) { - const endpoint = url.replace(/^\/|\/$/g, ""); + const endpoint = url.slice(1); const baseUrl = `/api/${endpoint}`; const apiUrl = queryString.stringifyUrl({ url: baseUrl, @@ -17,9 +17,7 @@ function buildUrl({ url, params }) { } function buildBody(data) { - if (data) { - return JSON.stringify(decamelizeKeys(data)); - } + return JSON.stringify(decamelizeKeys(data)); } async function processResponse(response, reload = true) { @@ -48,7 +46,7 @@ export async function get({ url, params, reload }, abortController) { return processResponse(response, reload); } -export async function download({ url, params }, filename = "download.file") { +export async function download({ url, params }, filename) { const res = await fetch(buildUrl({ url, params })); if (!res.ok) await processResponse(res); const bl = await res.blob(); diff --git a/frontend/src/api/backend/caseConvert.js b/frontend/src/api/backend/caseConvert.js index c50df1d4d1..eac8df227b 100644 --- a/frontend/src/api/backend/caseConvert.js +++ b/frontend/src/api/backend/caseConvert.js @@ -1,5 +1,4 @@ const separatorPattern = /[-_\s]+(.)?/g; -const firstCharPattern = /^./; const upperBoundaryPattern = /(?=[A-Z])/; const mapKeys = (fn) => { @@ -12,7 +11,5 @@ const mapKeys = (fn) => { }; return walk; }; -export const camelizeKeys = mapKeys((s) => - s.replace(separatorPattern, (_, c) => (c ? c.toUpperCase() : "")).replace(firstCharPattern, (c) => c.toLowerCase()), -); +export const camelizeKeys = mapKeys((s) => s.replace(separatorPattern, (_, c) => c.toUpperCase())); export const decamelizeKeys = mapKeys((s) => s.split(upperBoundaryPattern).join("_").toLowerCase()); diff --git a/frontend/src/api/backend/getUser.js b/frontend/src/api/backend/getUser.js index eba3a27a40..c7a4815b36 100644 --- a/frontend/src/api/backend/getUser.js +++ b/frontend/src/api/backend/getUser.js @@ -1,9 +1,8 @@ import * as api from "./base"; export async function getUser(id, expand, params = {}) { - const userId = id ? id : "me"; return await api.get({ - url: `/users/${userId}`, + url: `/users/${id}`, params: { expand: expand?.join(","), ...params, diff --git a/frontend/src/api/backend/refreshToken.js b/frontend/src/api/backend/refreshToken.js index c7ed2ce64d..07e3e5dc63 100644 --- a/frontend/src/api/backend/refreshToken.js +++ b/frontend/src/api/backend/refreshToken.js @@ -1,6 +1,6 @@ import * as api from "./base"; -export async function refreshToken(reload = true) { +export async function refreshToken(reload) { return await api.get({ url: "/tokens", reload, diff --git a/frontend/src/components/EmptyData.jsx b/frontend/src/components/EmptyData.jsx index 9591372522..bcc62b6006 100644 --- a/frontend/src/components/EmptyData.jsx +++ b/frontend/src/components/EmptyData.jsx @@ -9,7 +9,7 @@ function EmptyData({ isFiltered, object, objects, - color = "primary", + color, customAddBtn, permissionSection, permission, diff --git a/frontend/src/components/Flag.jsx b/frontend/src/components/Flag.jsx index 90b4281786..d13a2d2c9b 100644 --- a/frontend/src/components/Flag.jsx +++ b/frontend/src/components/Flag.jsx @@ -1,20 +1,14 @@ import { IconWorld } from "@tabler/icons-react"; -import { hasFlag } from "country-flag-icons"; // @ts-expect-error Creating a typing for a subfolder is not easily possible import Flags from "country-flag-icons/react/3x2"; function Flag({ className, countryCode }) { - countryCode = countryCode.toUpperCase(); if (countryCode === "EN") { return ; } - if (hasFlag(countryCode)) { - const FlagElement = Flags[countryCode]; - return ; - } - console.error(`No flag for country ${countryCode} found!`); - return null; + const FlagElement = Flags[countryCode]; + return ; } export { Flag }; diff --git a/frontend/src/components/Form/AccessClientFields.jsx b/frontend/src/components/Form/AccessClientFields.jsx index e01bda80ec..55c14b0fa4 100644 --- a/frontend/src/components/Form/AccessClientFields.jsx +++ b/frontend/src/components/Form/AccessClientFields.jsx @@ -6,7 +6,7 @@ import { intl, T } from "src/locale"; export function AccessClientFields({ initialValues, name = "clients" }) { const [values, setValues] = useState( - initialValues.at(-1)?.address?.trim().toLowerCase() === "all" + initialValues.at(-1)?.address?.toLowerCase() === "all" ? initialValues : [...initialValues, { directive: "deny", address: "all" }], ); @@ -24,9 +24,6 @@ export function AccessClientFields({ initialValues, name = "clients" }) { const handleRemove = (idx) => { const newValues = values.toSpliced(idx, 1); - if (newValues.length === 1) { - newValues.splice(newValues.length - 1, 0, blankClient); - } setValues(newValues); setFormField(newValues); }; diff --git a/frontend/src/components/Form/AccessFields.jsx b/frontend/src/components/Form/AccessFields.jsx index efaf17de37..042d47916e 100644 --- a/frontend/src/components/Form/AccessFields.jsx +++ b/frontend/src/components/Form/AccessFields.jsx @@ -49,14 +49,14 @@ export function AccessFields({ const { isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]); const createDefaultItem = (item) => ({ - value: item.id || 0, + value: item.id, label: item.name, subLabel: intl.formatMessage( { id: "access-list.subtitle" }, { users: item?.items?.length, rules: item?.clients?.length, - date: item?.createdOn ? formatDateTime(item?.createdOn) : "N/A", + date: formatDateTime(item?.createdOn), }, ), meta: item, @@ -88,7 +88,7 @@ export function AccessFields({ }; const defaultOptions = data?.map(createDefaultItem) || []; - const valuesSet = new Set(values?.map((item) => item || 0) || []); + const valuesSet = new Set(values); const options = defaultOptions.filter((option) => !valuesSet.has(option.value)); const typeOptions = () => { @@ -110,16 +110,14 @@ export function AccessFields({ }; const onAccessListChange = (acl, idx) => { - const newValues = values.map((id, i) => (i === idx ? acl.id || 0 : id)); + const newValues = values.map((id, i) => (i === idx ? acl.id : id)); applyUpdatedValues(newValues); }; const handleAdd = () => { const newAccessOption = options[0]; - if (newAccessOption?.meta.id) { - const newValues = [...values, newAccessOption.meta.id]; - applyUpdatedValues(newValues); - } + const newValues = [...values, newAccessOption.meta.id]; + applyUpdatedValues(newValues); }; const handleMoveUp = (idx) => { @@ -181,7 +179,6 @@ export function AccessFields({ }), }} onChange={(e) => { - if (!e || Array.isArray(e)) return; const value = e.type; setAclValue(value); void setFieldValue(typeFieldName, value); @@ -194,7 +191,7 @@ export function AccessFields({ {!isLoading && !isError && aclValue === "custom" ? ( <> {values.map((item, idx) => ( -
+
handleToggleChange(e, field.name)} disabled={!hasCertificate} /> @@ -86,7 +72,7 @@ export function SSLOptionsFields({ handleToggleChange(e, field.name)} disabled={!hasCertificate} /> @@ -107,7 +93,7 @@ export function SSLOptionsFields({ handleToggleChange(e, field.name)} disabled={!hasCertificate || !sslForced} /> @@ -126,7 +112,7 @@ export function SSLOptionsFields({ handleToggleChange(e, field.name)} disabled={!hasCertificate || !hstsEnabled} /> @@ -150,9 +136,9 @@ export function SSLOptionsFields({ {({ field }) => (
) : null}
- {forHttp ? getHttpOptions() : null} + {getHttpOptions()} {newCertificate ? (
@@ -218,8 +204,7 @@ export function SSLOptionsFields({ handleToggleChange(e, field.name)} /> @@ -230,7 +215,6 @@ export function SSLOptionsFields({ )}
- {requireDomainNames ? : null} {dnsChallenge ? : null}
) : null} diff --git a/frontend/src/components/HasPermission.jsx b/frontend/src/components/HasPermission.jsx index 0163d2691d..d87a7ce753 100644 --- a/frontend/src/components/HasPermission.jsx +++ b/frontend/src/components/HasPermission.jsx @@ -4,14 +4,7 @@ import { useUser } from "src/hooks"; import { T } from "src/locale"; import { hasPermission } from "src/modules/Permissions"; -function HasPermission({ - section, - permission, - children, - hideError = false, - pageLoading = false, - loadingNoLogo = false, -}) { +function HasPermission({ section, permission, children, hideError, pageLoading, loadingNoLogo }) { const { data, isLoading } = useUser("me"); if (!section) { diff --git a/frontend/src/components/LocalePicker.jsx b/frontend/src/components/LocalePicker.jsx index 52ea36fd68..33e9357fc4 100644 --- a/frontend/src/components/LocalePicker.jsx +++ b/frontend/src/components/LocalePicker.jsx @@ -3,7 +3,7 @@ import { Flag } from "src/components"; import { changeLocale, getFlagCodeForLocale, localeOptions } from "src/locale"; import localeList from "translations/lang-list.json" with { type: "json" }; -function LocalePicker({ menuAlign = "start" }) { +function LocalePicker({ menuAlign }) { return (
{item.items?.map((subitem, idx) => ( - - - {React.createElement(subitem.icon, { width: 18 })} - - - + {React.createElement(subitem.icon, { width: 18 })} + + ))}
@@ -188,7 +185,7 @@ export function SiteMenu() { ? user?.goaccess : hasPermission( item.permissionSection, - item.permission || VIEW, + item.permission, user?.permissions, user?.roles, ), diff --git a/frontend/src/components/Table/Formatter/AccessListformatter.jsx b/frontend/src/components/Table/Formatter/AccessListformatter.jsx index bd05f9d95d..dd31e0f5d8 100644 --- a/frontend/src/components/Table/Formatter/AccessListformatter.jsx +++ b/frontend/src/components/Table/Formatter/AccessListformatter.jsx @@ -3,11 +3,10 @@ import Popover from "react-bootstrap/Popover"; import { intl } from "src/locale"; import { showAccessListModal, showProxyHostModal } from "src/modals"; -export function AccessListFormatter({ proxyHostId, access = [], type = "public", locations = [] }) { +export function AccessListFormatter({ proxyHostId, access, type, locations }) { const hasLocationAcls = - (locations || []).filter( - (loc) => loc.npmplusAccessListType === "custom" && (loc.npmplusAccessListIds || []).length > 0, - ).length > 0; + locations.filter((loc) => loc.npmplusAccessListType === "custom" && loc.npmplusAccessListIds.length > 0) + .length > 0; let triggerLabel = type === "custom" @@ -35,7 +34,7 @@ export function AccessListFormatter({ proxyHostId, access = [], type = "public", onClick={(e) => { e.preventDefault(); if (access.length === 1) { - showAccessListModal(access[0].id || 0); + showAccessListModal(access[0].id); } else { showProxyHostModal(proxyHostId); } diff --git a/frontend/src/components/Table/Formatter/CertificateFormatter.jsx b/frontend/src/components/Table/Formatter/CertificateFormatter.jsx index e119d210b4..3ceb3800a9 100644 --- a/frontend/src/components/Table/Formatter/CertificateFormatter.jsx +++ b/frontend/src/components/Table/Formatter/CertificateFormatter.jsx @@ -7,7 +7,7 @@ const providerTranslations = { }; export function certificateProviderTranslation(provider) { - return providerTranslations[provider] ?? provider; + return providerTranslations[provider]; } export function CertificateFormatter({ certificate }) { diff --git a/frontend/src/components/Table/Formatter/CertificateInUseFormatter.jsx b/frontend/src/components/Table/Formatter/CertificateInUseFormatter.jsx index 54b09e224c..aa0d87a16e 100644 --- a/frontend/src/components/Table/Formatter/CertificateInUseFormatter.jsx +++ b/frontend/src/components/Table/Formatter/CertificateInUseFormatter.jsx @@ -43,17 +43,12 @@ const getSectionStream = (items) => { ); }; -export function CertificateInUseFormatter({ proxyHosts = [], redirectionHosts = [], deadHosts = [], streams = [] }) { +export function CertificateInUseFormatter({ proxyHosts, redirectionHosts, deadHosts, streams }) { const totalCount = proxyHosts.length + redirectionHosts.length + deadHosts.length + streams.length; if (totalCount === 0) { return ; } - proxyHosts.sort(); - redirectionHosts.sort(); - deadHosts.sort(); - streams.sort(); - const popover = ( diff --git a/frontend/src/components/Table/Formatter/DateFormatter.jsx b/frontend/src/components/Table/Formatter/DateFormatter.jsx index 548b0172db..be743c384f 100644 --- a/frontend/src/components/Table/Formatter/DateFormatter.jsx +++ b/frontend/src/components/Table/Formatter/DateFormatter.jsx @@ -1,14 +1,7 @@ import cn from "clsx"; -import { differenceInDays, isPast } from "date-fns"; -import { formatDateTime, parseDate } from "src/locale"; +import { isPast, parseISO } from "date-fns"; +import { formatDateTime } from "src/locale"; -export function DateFormatter({ value, highlightPast, highlightNearlyExpired }) { - const d = parseDate(value); - const dateIsPast = d ? isPast(d) : false; - const days = d ? differenceInDays(d, new Date()) : 0; - const cl = cn({ - "text-danger": highlightPast && dateIsPast, - "text-warning": highlightNearlyExpired && !dateIsPast && days <= 30 && days >= 0, - }); - return {formatDateTime(value)}; +export function DateFormatter({ value }) { + return {formatDateTime(value)}; } diff --git a/frontend/src/components/Table/Formatter/DomainsFormatter.jsx b/frontend/src/components/Table/Formatter/DomainsFormatter.jsx index 2c8f00dd84..ffa96eb359 100644 --- a/frontend/src/components/Table/Formatter/DomainsFormatter.jsx +++ b/frontend/src/components/Table/Formatter/DomainsFormatter.jsx @@ -3,8 +3,6 @@ import { formatDateTime, T } from "src/locale"; const DomainLink = ({ domain, color }) => { // when domain contains a wildcard, make the link go nowhere. - // Apparently the domain can be null or undefined sometimes. - if (!domain) return null; let onClick; if (domain.includes("*")) { onClick = (e) => e.preventDefault(); @@ -26,14 +24,14 @@ const DomainLink = ({ domain, color }) => { export function DomainsFormatter({ domains, createdOn, niceName, provider, color }) { const elms = []; - if ((!domains || domains.length === 0) && !niceName) { + if (domains.length === 0 && !niceName) { elms.push( Unknown , ); } - if (!domains || (niceName && provider !== "letsencrypt")) { + if (niceName && provider !== "letsencrypt") { elms.push( {niceName} @@ -41,18 +39,14 @@ export function DomainsFormatter({ domains, createdOn, niceName, provider, color ); } - if (domains) { - elms.push(...domains.map((domain) => )); - } + elms.push(...domains.map((domain) => )); return (
{...elms}
- {createdOn ? ( -
- -
- ) : null} +
+ +
); } diff --git a/frontend/src/components/Table/Formatter/EventFormatter.jsx b/frontend/src/components/Table/Formatter/EventFormatter.jsx index ff6b4bb4d5..17104bae77 100644 --- a/frontend/src/components/Table/Formatter/EventFormatter.jsx +++ b/frontend/src/components/Table/Formatter/EventFormatter.jsx @@ -19,15 +19,15 @@ const getEventValue = (event) => { case "proxy-host": case "redirection-host": case "dead-host": - return event.meta?.domainNames?.join(", ") || "N/A"; + return event.meta?.domainNames?.join(", "); case "stream": - return event.meta?.incomingPort || "N/A"; + return event.meta?.incomingPort; case "certificate": return event.meta?.domainNames?.join(", ") || event.meta?.niceName || "N/A"; case "setting": return event.meta?.id; default: - return `UNKNOWN EVENT TYPE: ${event.objectType}`; + break; } }; diff --git a/frontend/src/components/Table/Formatter/RolesFormatter.jsx b/frontend/src/components/Table/Formatter/RolesFormatter.jsx index ffa9343326..6c48aeaccf 100644 --- a/frontend/src/components/Table/Formatter/RolesFormatter.jsx +++ b/frontend/src/components/Table/Formatter/RolesFormatter.jsx @@ -1,13 +1,12 @@ import { T } from "src/locale"; export function RolesFormatter({ roles }) { - const r = roles || []; - if (r.length === 0) { - r[0] = "standard-user"; + if (roles.length === 0) { + roles[0] = "standard-user"; } return ( <> - {r.map((role) => ( + {roles.map((role) => ( diff --git a/frontend/src/components/Table/Formatter/StatusFormatter.jsx b/frontend/src/components/Table/Formatter/StatusFormatter.jsx index 3d87a406ee..2671e83cae 100644 --- a/frontend/src/components/Table/Formatter/StatusFormatter.jsx +++ b/frontend/src/components/Table/Formatter/StatusFormatter.jsx @@ -14,7 +14,7 @@ export function StatusFormatter({ enabled, nginxOnline, nginxErr }) { } return ( - + diff --git a/frontend/src/components/Table/Formatter/ValueWithDateFormatter.jsx b/frontend/src/components/Table/Formatter/ValueWithDateFormatter.jsx index bf31c6d646..2204575b0b 100644 --- a/frontend/src/components/Table/Formatter/ValueWithDateFormatter.jsx +++ b/frontend/src/components/Table/Formatter/ValueWithDateFormatter.jsx @@ -6,11 +6,9 @@ export function ValueWithDateFormatter({ value, createdOn, disabled }) {
{value}
- {createdOn ? ( -
- -
- ) : null} +
+ +
); } diff --git a/frontend/src/components/Table/TableBody.jsx b/frontend/src/components/Table/TableBody.jsx index d06b8c691d..7a0983613d 100644 --- a/frontend/src/components/Table/TableBody.jsx +++ b/frontend/src/components/Table/TableBody.jsx @@ -51,7 +51,7 @@ function TableBody(props) { colSpan={colSpan} className="subheader fw-bold text-secondary py-2 border-bottom bg-surface-secondary" > - {renderGroupLabel ? renderGroupLabel(key) : key} + {renderGroupLabel(key)} {groups.get(key)?.map(renderRow)} diff --git a/frontend/src/hooks/useAccessList.js b/frontend/src/hooks/useAccessList.js index 889fb85cee..b808de8102 100644 --- a/frontend/src/hooks/useAccessList.js +++ b/frontend/src/hooks/useAccessList.js @@ -18,12 +18,11 @@ const fetchAccessList = (id, expand) => { return getAccessList(id, expand); }; -const useAccessList = (id, expand, options = {}) => { +const useAccessList = (id, expand) => { return useQuery({ queryKey: ["access-list", id, expand], queryFn: () => fetchAccessList(id, expand), staleTime: 60 * 1000, // 1 minute - ...options, }); }; diff --git a/frontend/src/hooks/useAccessLists.js b/frontend/src/hooks/useAccessLists.js index 28451e865a..e1f2561072 100644 --- a/frontend/src/hooks/useAccessLists.js +++ b/frontend/src/hooks/useAccessLists.js @@ -3,12 +3,11 @@ import { getAccessLists } from "src/api/backend"; const fetchAccessLists = (expand) => getAccessLists(expand); -const useAccessLists = (expand, options = {}) => +const useAccessLists = (expand) => useQuery({ queryKey: ["access-lists", { expand }], queryFn: () => fetchAccessLists(expand), staleTime: 60 * 1000, - ...options, }); export { useAccessLists }; diff --git a/frontend/src/hooks/useAuditLog.js b/frontend/src/hooks/useAuditLog.js index cc5af3ecb0..f22c830aa9 100644 --- a/frontend/src/hooks/useAuditLog.js +++ b/frontend/src/hooks/useAuditLog.js @@ -3,12 +3,11 @@ import { getAuditLog } from "src/api/backend"; const fetchAuditLog = (id) => getAuditLog(id, ["user"]); -const useAuditLog = (id, options = {}) => { +const useAuditLog = (id) => { return useQuery({ queryKey: ["audit-log", id], queryFn: () => fetchAuditLog(id), staleTime: 5 * 60 * 1000, // 5 minutes - ...options, }); }; diff --git a/frontend/src/hooks/useAuditLogs.js b/frontend/src/hooks/useAuditLogs.js index 4295249f66..4f281caad0 100644 --- a/frontend/src/hooks/useAuditLogs.js +++ b/frontend/src/hooks/useAuditLogs.js @@ -3,12 +3,11 @@ import { getAuditLogs } from "src/api/backend"; const fetchAuditLogs = (expand) => getAuditLogs(expand); -const useAuditLogs = (expand, options = {}) => +const useAuditLogs = (expand) => useQuery({ queryKey: ["audit-logs", { expand }], queryFn: () => fetchAuditLogs(expand), staleTime: 10 * 1000, - ...options, }); export { useAuditLogs }; diff --git a/frontend/src/hooks/useCertificate.js b/frontend/src/hooks/useCertificate.js index 519c197519..84445d2280 100644 --- a/frontend/src/hooks/useCertificate.js +++ b/frontend/src/hooks/useCertificate.js @@ -3,12 +3,11 @@ import { getCertificate } from "src/api/backend"; const fetchCertificate = (id) => getCertificate(id); -const useCertificate = (id, options = {}) => { +const useCertificate = (id) => { return useQuery({ queryKey: ["certificate", id], queryFn: () => fetchCertificate(id), staleTime: 60 * 1000, // 1 minute - ...options, }); }; diff --git a/frontend/src/hooks/useCertificates.js b/frontend/src/hooks/useCertificates.js index 7b9506c27f..cf3628891a 100644 --- a/frontend/src/hooks/useCertificates.js +++ b/frontend/src/hooks/useCertificates.js @@ -3,12 +3,11 @@ import { getCertificates } from "src/api/backend"; const fetchCertificates = (expand) => getCertificates(expand); -const useCertificates = (expand, options = {}) => +const useCertificates = (expand) => useQuery({ queryKey: ["certificates", { expand }], queryFn: () => fetchCertificates(expand), staleTime: 60 * 1000, - ...options, }); export { useCertificates }; diff --git a/frontend/src/hooks/useCheckVersion.js b/frontend/src/hooks/useCheckVersion.js index 33abfb38fb..24153e2d6e 100644 --- a/frontend/src/hooks/useCheckVersion.js +++ b/frontend/src/hooks/useCheckVersion.js @@ -3,7 +3,7 @@ import { checkVersion } from "src/api/backend"; const fetchVersion = () => checkVersion(); -const useCheckVersion = (options = {}) => { +const useCheckVersion = () => { return useQuery({ queryKey: ["version-check"], queryFn: fetchVersion, @@ -11,7 +11,6 @@ const useCheckVersion = (options = {}) => { retry: 5, refetchInterval: 30 * 1000, // 30 seconds staleTime: 5 * 60 * 1000, // 5 mins - ...options, }); }; diff --git a/frontend/src/hooks/useDeadHost.js b/frontend/src/hooks/useDeadHost.js index 80174aae00..d9fe4651ef 100644 --- a/frontend/src/hooks/useDeadHost.js +++ b/frontend/src/hooks/useDeadHost.js @@ -24,12 +24,11 @@ const fetchDeadHost = (id) => { return getDeadHost(id); }; -const useDeadHost = (id, options = {}) => { +const useDeadHost = (id) => { return useQuery({ queryKey: ["dead-host", id], queryFn: () => fetchDeadHost(id), staleTime: 60 * 1000, // 1 minute - ...options, }); }; diff --git a/frontend/src/hooks/useDeadHosts.js b/frontend/src/hooks/useDeadHosts.js index cb14dd7d03..76a81a86d8 100644 --- a/frontend/src/hooks/useDeadHosts.js +++ b/frontend/src/hooks/useDeadHosts.js @@ -3,12 +3,11 @@ import { getDeadHosts } from "src/api/backend"; const fetchDeadHosts = (expand) => getDeadHosts(expand); -const useDeadHosts = (expand, options = {}) => +const useDeadHosts = (expand) => useQuery({ queryKey: ["dead-hosts", { expand }], queryFn: () => fetchDeadHosts(expand), staleTime: 60 * 1000, - ...options, }); export { useDeadHosts }; diff --git a/frontend/src/hooks/useDirectorySuggestions.js b/frontend/src/hooks/useDirectorySuggestions.js index 645388840b..835d3952e6 100644 --- a/frontend/src/hooks/useDirectorySuggestions.js +++ b/frontend/src/hooks/useDirectorySuggestions.js @@ -1,6 +1,6 @@ import { useMemo } from "react"; -const getDirectory = (item) => item?.npmplusDirectory || ""; +const getDirectory = (item) => item?.npmplusDirectory; const useDirectorySuggestions = (items) => useMemo( diff --git a/frontend/src/hooks/useDnsProviders.js b/frontend/src/hooks/useDnsProviders.js index 67d597f880..b12f584558 100644 --- a/frontend/src/hooks/useDnsProviders.js +++ b/frontend/src/hooks/useDnsProviders.js @@ -3,12 +3,11 @@ import { getCertificateDNSProviders } from "src/api/backend"; const fetchDnsProviders = () => getCertificateDNSProviders(); -const useDnsProviders = (options = {}) => +const useDnsProviders = () => useQuery({ queryKey: ["dns-providers"], queryFn: () => fetchDnsProviders(), staleTime: 300 * 1000, - ...options, }); export { useDnsProviders }; diff --git a/frontend/src/hooks/useHealth.js b/frontend/src/hooks/useHealth.js index 849a6cee5a..03c81f0477 100644 --- a/frontend/src/hooks/useHealth.js +++ b/frontend/src/hooks/useHealth.js @@ -3,7 +3,7 @@ import { getHealth } from "src/api/backend"; const fetchHealth = () => getHealth(); -const useHealth = (options = {}) => { +const useHealth = () => { return useQuery({ queryKey: ["health"], queryFn: fetchHealth, @@ -11,7 +11,6 @@ const useHealth = (options = {}) => { retry: 5, refetchInterval: 15 * 1000, // 15 seconds staleTime: 14 * 1000, // 14 seconds - ...options, }); }; diff --git a/frontend/src/hooks/useHostReport.js b/frontend/src/hooks/useHostReport.js index cc4b92967a..9a8169e4ec 100644 --- a/frontend/src/hooks/useHostReport.js +++ b/frontend/src/hooks/useHostReport.js @@ -3,7 +3,7 @@ import { getHostsReport } from "src/api/backend"; const fetchHostReport = () => getHostsReport(); -const useHostReport = (options = {}) => { +const useHostReport = () => { return useQuery({ queryKey: ["host-report"], queryFn: fetchHostReport, @@ -11,7 +11,6 @@ const useHostReport = (options = {}) => { retry: 5, refetchInterval: 15 * 1000, // 15 seconds staleTime: 14 * 1000, // 14 seconds - ...options, }); }; diff --git a/frontend/src/hooks/useProxyHost.js b/frontend/src/hooks/useProxyHost.js index 3801eb1c99..518481904d 100644 --- a/frontend/src/hooks/useProxyHost.js +++ b/frontend/src/hooks/useProxyHost.js @@ -41,12 +41,11 @@ const fetchProxyHost = (id) => { return getProxyHost(id); }; -const useProxyHost = (id, options = {}) => { +const useProxyHost = (id) => { return useQuery({ queryKey: ["proxy-host", id], queryFn: () => fetchProxyHost(id), staleTime: 60 * 1000, // 1 minute - ...options, }); }; diff --git a/frontend/src/hooks/useProxyHosts.js b/frontend/src/hooks/useProxyHosts.js index ea1ae0b61c..93f85686bd 100644 --- a/frontend/src/hooks/useProxyHosts.js +++ b/frontend/src/hooks/useProxyHosts.js @@ -3,12 +3,11 @@ import { getProxyHosts } from "src/api/backend"; const fetchProxyHosts = (expand) => getProxyHosts(expand); -const useProxyHosts = (expand, options = {}) => +const useProxyHosts = (expand) => useQuery({ queryKey: ["proxy-hosts", { expand }], queryFn: () => fetchProxyHosts(expand), staleTime: 60 * 1000, - ...options, }); export { useProxyHosts }; diff --git a/frontend/src/hooks/useRedirectionHost.js b/frontend/src/hooks/useRedirectionHost.js index f237869331..157f7a8389 100644 --- a/frontend/src/hooks/useRedirectionHost.js +++ b/frontend/src/hooks/useRedirectionHost.js @@ -28,12 +28,11 @@ const fetchRedirectionHost = (id) => { return getRedirectionHost(id); }; -const useRedirectionHost = (id, options = {}) => { +const useRedirectionHost = (id) => { return useQuery({ queryKey: ["redirection-host", id], queryFn: () => fetchRedirectionHost(id), staleTime: 60 * 1000, // 1 minute - ...options, }); }; diff --git a/frontend/src/hooks/useRedirectionHosts.js b/frontend/src/hooks/useRedirectionHosts.js index 7770ccf774..0f21c76792 100644 --- a/frontend/src/hooks/useRedirectionHosts.js +++ b/frontend/src/hooks/useRedirectionHosts.js @@ -3,12 +3,11 @@ import { getRedirectionHosts } from "src/api/backend"; const fetchRedirectionHosts = (expand) => getRedirectionHosts(expand); -const useRedirectionHosts = (expand, options = {}) => +const useRedirectionHosts = (expand) => useQuery({ queryKey: ["redirection-hosts", { expand }], queryFn: () => fetchRedirectionHosts(expand), staleTime: 60 * 1000, - ...options, }); export { useRedirectionHosts }; diff --git a/frontend/src/hooks/useSetting.js b/frontend/src/hooks/useSetting.js index 26926902ba..41e3c1360b 100644 --- a/frontend/src/hooks/useSetting.js +++ b/frontend/src/hooks/useSetting.js @@ -3,12 +3,11 @@ import { getSetting, updateSetting } from "src/api/backend"; const fetchSetting = (id) => getSetting(id); -const useSetting = (id, options = {}) => { +const useSetting = (id) => { return useQuery({ queryKey: ["setting", id], queryFn: () => fetchSetting(id), staleTime: 60 * 1000, // 1 minute - ...options, }); }; diff --git a/frontend/src/hooks/useStream.js b/frontend/src/hooks/useStream.js index 517790f870..0177d8cee5 100644 --- a/frontend/src/hooks/useStream.js +++ b/frontend/src/hooks/useStream.js @@ -27,12 +27,11 @@ const fetchStream = (id) => { return getStream(id); }; -const useStream = (id, options = {}) => { +const useStream = (id) => { return useQuery({ queryKey: ["stream", id], queryFn: () => fetchStream(id), staleTime: 60 * 1000, // 1 minute - ...options, }); }; diff --git a/frontend/src/hooks/useStreams.js b/frontend/src/hooks/useStreams.js index c69416dfcc..153e709c90 100644 --- a/frontend/src/hooks/useStreams.js +++ b/frontend/src/hooks/useStreams.js @@ -3,12 +3,11 @@ import { getStreams } from "src/api/backend"; const fetchStreams = (expand) => getStreams(expand); -const useStreams = (expand, options = {}) => +const useStreams = (expand) => useQuery({ queryKey: ["streams", { expand }], queryFn: () => fetchStreams(expand), staleTime: 60 * 1000, - ...options, }); export { useStreams }; diff --git a/frontend/src/hooks/useUser.js b/frontend/src/hooks/useUser.js index 83d27ec5b9..d2b96b3a06 100644 --- a/frontend/src/hooks/useUser.js +++ b/frontend/src/hooks/useUser.js @@ -17,12 +17,11 @@ const fetchUser = (id) => { return getUser(id, ["permissions"]); }; -const useUser = (id, options = {}) => { +const useUser = (id) => { return useQuery({ queryKey: ["user", id], queryFn: () => fetchUser(id), staleTime: 60 * 1000, // 1 minute - ...options, }); }; diff --git a/frontend/src/hooks/useUsers.js b/frontend/src/hooks/useUsers.js index 4dbe5ae5ff..e76a5082f2 100644 --- a/frontend/src/hooks/useUsers.js +++ b/frontend/src/hooks/useUsers.js @@ -3,12 +3,11 @@ import { getUsers } from "src/api/backend"; const fetchUsers = (expand) => getUsers(expand); -const useUsers = (expand, options = {}) => +const useUsers = (expand) => useQuery({ queryKey: ["users", { expand }], queryFn: () => fetchUsers(expand), staleTime: 60 * 1000, - ...options, }); export { useUsers }; diff --git a/frontend/src/locale/Utils.js b/frontend/src/locale/Utils.js index 4d4db22eb5..f6d7d99d0c 100644 --- a/frontend/src/locale/Utils.js +++ b/frontend/src/locale/Utils.js @@ -1,16 +1,9 @@ -import { fromUnixTime, intlFormat, parseISO } from "date-fns"; +import { intlFormat, parseISO } from "date-fns"; import { currentLocale } from "./IntlProvider.jsx"; -const parseDate = (value) => { - const date = typeof value === "number" ? fromUnixTime(value) : parseISO(`${value}`); - return Number.isNaN(date.getTime()) ? null : date; -}; - -const formatDateTime = (value) => { - const d = parseDate(value); - if (!d) return `${value}`; - return intlFormat( - d, +const formatDateTime = (value) => + intlFormat( + parseISO(value), { dateStyle: "medium", timeStyle: "medium", @@ -18,6 +11,5 @@ const formatDateTime = (value) => { }, { locale: currentLocale }, ); -}; -export { formatDateTime, parseDate }; +export { formatDateTime }; diff --git a/frontend/src/modals/AccessListModal.jsx b/frontend/src/modals/AccessListModal.jsx index f43cf6ec72..391f51f9b3 100644 --- a/frontend/src/modals/AccessListModal.jsx +++ b/frontend/src/modals/AccessListModal.jsx @@ -83,13 +83,13 @@ const AccessListModal = EasyModal.create(({ id, visible, remove }) => { return ( - {!isLoading && error && ( + {error && ( - {error?.message || "Unknown error"} + {error?.message} )} {isLoading && } - {!isLoading && data && ( + {data && ( { - setErrorMsg(null)} - dismissible - > + setErrorMsg(null)} dismissible> {errorMsg}
@@ -176,9 +171,7 @@ const AccessListModal = EasyModal.create(({ id, visible, remove }) => { {form.errors.name ? (
- {form.errors.name && form.touched.name - ? form.errors.name - : null} + {form.touched.name ? form.errors.name : null}
) : null}
diff --git a/frontend/src/modals/ChangePasswordModal.jsx b/frontend/src/modals/ChangePasswordModal.jsx index a86c41687f..0f4184662f 100644 --- a/frontend/src/modals/ChangePasswordModal.jsx +++ b/frontend/src/modals/ChangePasswordModal.jsx @@ -59,7 +59,7 @@ const ChangePasswordModal = EasyModal.create(({ id, visible, remove }) => { - setError(null)} dismissible> + setError(null)} dismissible> {error} diff --git a/frontend/src/modals/CustomCertificateModal.jsx b/frontend/src/modals/CustomCertificateModal.jsx index 47c662b362..91d7e728da 100644 --- a/frontend/src/modals/CustomCertificateModal.jsx +++ b/frontend/src/modals/CustomCertificateModal.jsx @@ -87,12 +87,7 @@ const CustomCertificateModal = EasyModal.create(({ visible, remove, cert, provid - setErrorMsg(null)} - dismissible - > + setErrorMsg(null)} dismissible> {errorMsg}
@@ -120,9 +115,7 @@ const CustomCertificateModal = EasyModal.create(({ visible, remove, cert, provid {form.errors.niceName ? (
- {form.errors.niceName && form.touched.niceName - ? form.errors.niceName - : null} + {form.touched.niceName ? form.errors.niceName : null}
) : null}
@@ -141,22 +134,9 @@ const CustomCertificateModal = EasyModal.create(({ visible, remove, cert, provid autoComplete="off" className="form-control" onChange={(event) => { - form.setFieldValue( - field.name, - event.currentTarget.files?.length - ? event.currentTarget.files[0] - : null, - ); + form.setFieldValue(field.name, event.currentTarget.files[0]); }} /> - - {form.errors.certificate ? ( -
- {form.errors.certificate && form.touched.certificate - ? form.errors.certificate - : null} -
- ) : null}
)} @@ -176,20 +156,10 @@ const CustomCertificateModal = EasyModal.create(({ visible, remove, cert, provid onChange={(event) => { form.setFieldValue( field.name, - event.currentTarget.files?.length - ? event.currentTarget.files[0] - : null, + event.currentTarget.files[0], ); }} /> - - {form.errors.certificateKey ? ( -
- {form.errors.certificateKey && form.touched.certificateKey - ? form.errors.certificateKey - : null} -
- ) : null} )} diff --git a/frontend/src/modals/DNSCertificateModal.jsx b/frontend/src/modals/DNSCertificateModal.jsx index fc03d9ca04..189e70a2a0 100644 --- a/frontend/src/modals/DNSCertificateModal.jsx +++ b/frontend/src/modals/DNSCertificateModal.jsx @@ -68,12 +68,7 @@ const DNSCertificateModal = EasyModal.create(({ visible, remove }) => { - setErrorMsg(null)} - dismissible - > + setErrorMsg(null)} dismissible> {errorMsg}
diff --git a/frontend/src/modals/DeadHostModal.jsx b/frontend/src/modals/DeadHostModal.jsx index 5f01f2adba..3e45187f6d 100644 --- a/frontend/src/modals/DeadHostModal.jsx +++ b/frontend/src/modals/DeadHostModal.jsx @@ -66,13 +66,13 @@ const DeadHostModal = EasyModal.create(({ id, visible, remove }) => { return ( - {!isLoading && error && ( + {error && ( - {error?.message || "Unknown error"} + {error?.message} )} {isLoading && } - {!isLoading && data && ( + {data && ( { - setErrorMsg(null)} - dismissible - > + setErrorMsg(null)} dismissible> {errorMsg}
diff --git a/frontend/src/modals/DeleteConfirmModal.jsx b/frontend/src/modals/DeleteConfirmModal.jsx index 5e9422690a..606197fa4b 100644 --- a/frontend/src/modals/DeleteConfirmModal.jsx +++ b/frontend/src/modals/DeleteConfirmModal.jsx @@ -24,7 +24,7 @@ const DeleteConfirmModal = EasyModal.create( await onConfirm(); remove(); // invalidate caches as requested - await Promise.all((invalidations ?? []).map((inv) => queryClient.invalidateQueries({ queryKey: inv }))); + await Promise.all(invalidations.map((inv) => queryClient.invalidateQueries({ queryKey: inv }))); } catch (err) { setError(); } @@ -37,7 +37,7 @@ const DeleteConfirmModal = EasyModal.create( {tTitle ? : title} - setError(null)} dismissible> + setError(null)} dismissible> {error}
diff --git a/frontend/src/modals/EventDetailsModal.jsx b/frontend/src/modals/EventDetailsModal.jsx index 50d1a359de..2f65f70ca9 100644 --- a/frontend/src/modals/EventDetailsModal.jsx +++ b/frontend/src/modals/EventDetailsModal.jsx @@ -14,13 +14,13 @@ const EventDetailsModal = EasyModal.create(({ id, visible, remove }) => { return ( - {!isLoading && error && ( + {error && ( - {error?.message || "Unknown error"} + {error?.message} )} {isLoading && } - {!isLoading && data && ( + {data && ( <> @@ -30,7 +30,7 @@ const EventDetailsModal = EasyModal.create(({ id, visible, remove }) => {
- +
diff --git a/frontend/src/modals/HTTPCertificateModal.jsx b/frontend/src/modals/HTTPCertificateModal.jsx index bbcf1eefa9..9a68720993 100644 --- a/frontend/src/modals/HTTPCertificateModal.jsx +++ b/frontend/src/modals/HTTPCertificateModal.jsx @@ -66,47 +66,43 @@ const HTTPCertificateModal = EasyModal.create(({ visible, remove }) => { const toggleClasses = "form-check-input"; const toggleEnabled = cn(toggleClasses, "bg-cyan"); - const parseTestResults = () => { - if (!testResults) return null; + const parseTestResults = () => ( + <> + {testResults.map((testResult) => { + const { domain, status } = testResult; + let messageComponent; - return ( - <> - {testResults.map((testResult) => { - const { domain, status } = testResult; - let messageComponent; - - switch (status) { - case "ok": - messageComponent = ; - break; - case "no-host": - messageComponent = ; - break; - case "failed": - messageComponent = ; - break; - case "404": - messageComponent = ; - break; - case "wrong-data": - messageComponent = ; - break; - default: { - const code = status.slice(6); - messageComponent = ; - break; - } + switch (status) { + case "ok": + messageComponent = ; + break; + case "no-host": + messageComponent = ; + break; + case "failed": + messageComponent = ; + break; + case "404": + messageComponent = ; + break; + case "wrong-data": + messageComponent = ; + break; + default: { + const code = status.slice(6); + messageComponent = ; + break; } + } - return ( -

- {domain}: {messageComponent} -

- ); - })} - - ); - }; + return ( +

+ {domain}: {messageComponent} +

+ ); + })} + + ); return ( @@ -126,12 +122,7 @@ const HTTPCertificateModal = EasyModal.create(({ visible, remove }) => { - setErrorMsg(null)} - dismissible - > + setErrorMsg(null)} dismissible> {errorMsg}
diff --git a/frontend/src/modals/MfaModal.jsx b/frontend/src/modals/MfaModal.jsx index 8b876f9e40..e44512bc4a 100644 --- a/frontend/src/modals/MfaModal.jsx +++ b/frontend/src/modals/MfaModal.jsx @@ -31,7 +31,7 @@ const MfaModal = EasyModal.create(({ id, visible, remove }) => { setBackupCodesRemaining(status.backupCodesRemaining); setStep("status"); } catch (err) { - setError(err.message || "Failed to load MFA status"); + setError(err.message); setStep("status"); } }, [id]); @@ -48,7 +48,7 @@ const MfaModal = EasyModal.create(({ id, visible, remove }) => { setSetupData(data); setStep("setup"); } catch (err) { - setError(err.message || "Failed to start TOTP setup"); + setError(err.message); } setIsSubmitting(false); }; @@ -65,7 +65,7 @@ const MfaModal = EasyModal.create(({ id, visible, remove }) => { await loadStatus(); } } catch (err) { - setError(err.message || "Failed to enable TOTP"); + setError(err.message); } setIsSubmitting(false); }; @@ -78,7 +78,7 @@ const MfaModal = EasyModal.create(({ id, visible, remove }) => { setIsEnabled(false); setStep("status"); } catch (err) { - setError(err.message || "Failed to disable TOTP"); + setError(err.message); } setIsSubmitting(false); }; @@ -91,7 +91,7 @@ const MfaModal = EasyModal.create(({ id, visible, remove }) => { setBackupCodes(result.backupCodes); setStep("backup"); } catch (err) { - setError(err.message || "Failed to regenerate backup codes"); + setError(err.message); } setIsSubmitting(false); }; @@ -149,7 +149,7 @@ const MfaModal = EasyModal.create(({ id, visible, remove }) => { ); } - if (step === "setup" && setupData) { + if (step === "setup") { return (
@@ -411,7 +411,7 @@ const MfaModal = EasyModal.create(({ id, visible, remove }) => { - setError(null)} dismissible> + setError(null)} dismissible> {error} {renderContent()} diff --git a/frontend/src/modals/PermissionsModal.jsx b/frontend/src/modals/PermissionsModal.jsx index 606d1b498f..4d26c93f80 100644 --- a/frontend/src/modals/PermissionsModal.jsx +++ b/frontend/src/modals/PermissionsModal.jsx @@ -134,13 +134,13 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }) => { return ( - {!isLoading && error && ( + {error && ( - {error?.message || "Unknown error"} + {error?.message} )} {isLoading && } - {!isLoading && data && ( + {data && ( { - setErrorMsg(null)} - dismissible - > + setErrorMsg(null)} dismissible> {errorMsg}
diff --git a/frontend/src/modals/ProxyHostModal.jsx b/frontend/src/modals/ProxyHostModal.jsx index 7fbaee70fc..b989bee76d 100644 --- a/frontend/src/modals/ProxyHostModal.jsx +++ b/frontend/src/modals/ProxyHostModal.jsx @@ -16,15 +16,14 @@ import { SSLCertificateField, SSLOptionsFields, } from "src/components"; -import { useDirectorySuggestions, useProxyHost, useProxyHosts, useSetProxyHost, useUser } from "src/hooks"; +import { useDirectorySuggestions, useProxyHost, useProxyHosts, useSetProxyHost } from "src/hooks"; import { intl, T } from "src/locale"; import EasyModal from "src/modules/easyModal"; import { MANAGE, PROXY_HOSTS } from "src/modules/Permissions"; import { showTabOfInvalid, validateNumber, validateUpstreamUrl } from "src/modules/Validations"; import { showObjectSuccess } from "src/notifications"; -const ProxyHostModal = EasyModal.create(({ id, isClone = false, visible, remove }) => { - const { data: currentUser, isLoading: userIsLoading, error: userError } = useUser("me"); +const ProxyHostModal = EasyModal.create(({ id, isClone, visible, remove }) => { const { data, isLoading, error } = useProxyHost(id); const { data: allProxyHosts } = useProxyHosts(); const suggestions = useDirectorySuggestions(allProxyHosts); @@ -79,13 +78,13 @@ const ProxyHostModal = EasyModal.create(({ id, isClone = false, visible, remove return ( - {!isLoading && (error || userError) && ( + {error && ( - {error?.message || userError?.message || "Unknown error"} + {error?.message} )} - {isLoading || (userIsLoading && )} - {!isLoading && !userIsLoading && data && currentUser && ( + {isLoading && } + {data && ( - setErrorMsg(null)} - dismissible - > + setErrorMsg(null)} dismissible> {errorMsg}
@@ -256,7 +250,7 @@ const ProxyHostModal = EasyModal.create(({ id, isClone = false, visible, remove
- {({ field, form }) => ( + {({ field }) => (
)}
@@ -300,8 +285,7 @@ const ProxyHostModal = EasyModal.create(({ id, isClone = false, visible, remove {form.errors.forwardPort ? (
- {form.errors.forwardPort && - form.touched.forwardPort + {form.touched.forwardPort ? form.errors.forwardPort : null}
@@ -635,9 +619,7 @@ const ProxyHostModal = EasyModal.create(({ id, isClone = false, visible, remove {form.errors .npmplusAuthRequestUpstream ? (
- {form.errors - .npmplusAuthRequestUpstream && - form.touched + {form.touched .npmplusAuthRequestUpstream ? form.errors .npmplusAuthRequestUpstream @@ -737,7 +719,7 @@ const ProxyHostModal = EasyModal.create(({ id, isClone = false, visible, remove ); }); -const showProxyHostModal = (id, isClone = false) => { +const showProxyHostModal = (id, isClone) => { EasyModal.show(ProxyHostModal, { id, isClone }); }; diff --git a/frontend/src/modals/ReachabilityModal.jsx b/frontend/src/modals/ReachabilityModal.jsx index 9a2b42c755..5a8b3fc4bb 100644 --- a/frontend/src/modals/ReachabilityModal.jsx +++ b/frontend/src/modals/ReachabilityModal.jsx @@ -28,47 +28,43 @@ const ReachabilityModal = EasyModal.create(({ visible, remove, domains }) => { void handleTest(); }, [domains]); - const parseTestResults = () => { - if (!testResults) return null; + const parseTestResults = () => ( + <> + {testResults.map((testResult) => { + const { domain, status } = testResult; + let messageComponent; - return ( - <> - {testResults.map((testResult) => { - const { domain, status } = testResult; - let messageComponent; - - switch (status) { - case "ok": - messageComponent = ; - break; - case "no-host": - messageComponent = ; - break; - case "failed": - messageComponent = ; - break; - case "404": - messageComponent = ; - break; - case "wrong-data": - messageComponent = ; - break; - default: { - const code = status.slice(6); - messageComponent = ; - break; - } + switch (status) { + case "ok": + messageComponent = ; + break; + case "no-host": + messageComponent = ; + break; + case "failed": + messageComponent = ; + break; + case "404": + messageComponent = ; + break; + case "wrong-data": + messageComponent = ; + break; + default: { + const code = status.slice(6); + messageComponent = ; + break; } + } - return ( -

- {domain}: {messageComponent} -

- ); - })} - - ); - }; + return ( +

+ {domain}: {messageComponent} +

+ ); + })} + + ); return ( @@ -78,7 +74,7 @@ const ReachabilityModal = EasyModal.create(({ visible, remove, domains }) => { - setErrorMsg(null)} dismissible> + setErrorMsg(null)} dismissible> {errorMsg}
diff --git a/frontend/src/modals/RedirectionHostModal.jsx b/frontend/src/modals/RedirectionHostModal.jsx index 79c8989d51..1a2941496b 100644 --- a/frontend/src/modals/RedirectionHostModal.jsx +++ b/frontend/src/modals/RedirectionHostModal.jsx @@ -67,13 +67,13 @@ const RedirectionHostModal = EasyModal.create(({ id, visible, remove }) => { return ( - {!isLoading && error && ( + {error && ( - {error?.message || "Unknown error"} + {error?.message} )} {isLoading && } - {!isLoading && data && ( + {data && ( { - setErrorMsg(null)} - dismissible - > + setErrorMsg(null)} dismissible> {errorMsg}
@@ -210,8 +205,7 @@ const RedirectionHostModal = EasyModal.create(({ id, visible, remove }) => { {form.errors.forwardDomainName ? (
- {form.errors.forwardDomainName && - form.touched.forwardDomainName + {form.touched.forwardDomainName ? form.errors.forwardDomainName : null}
diff --git a/frontend/src/modals/RenewCertificateModal.jsx b/frontend/src/modals/RenewCertificateModal.jsx index ff261e30b4..a057a7d5dc 100644 --- a/frontend/src/modals/RenewCertificateModal.jsx +++ b/frontend/src/modals/RenewCertificateModal.jsx @@ -21,7 +21,7 @@ const RenewCertificateModal = EasyModal.create(({ id, visible, remove }) => { const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { - if (!data || !isFresh || isSubmitting) return; + if (!data || !isFresh) return; setIsFresh(false); setIsSubmitting(true); @@ -47,7 +47,7 @@ const RenewCertificateModal = EasyModal.create(({ id, visible, remove }) => { setIsSubmitting(false); } })(); - }, [id, data, isFresh, isSubmitting, remove, queryClient]); + }, [id, data, isFresh, remove, queryClient]); return ( @@ -57,16 +57,16 @@ const RenewCertificateModal = EasyModal.create(({ id, visible, remove }) => { - + {errorMsg} {isLoading && } - {!isLoading && error && ( + {error && ( - {error?.message || "Unknown error"} + {error?.message} )} - {data && isSubmitting && !errorMsg ?

Please wait ...

: null} + {isSubmitting ?

Please wait ...

: null}
@@ -189,16 +182,14 @@ const UserModal = EasyModal.create(({ id, visible, remove }) => { {form.errors.email ? (
- {form.errors.email && form.touched.email - ? form.errors.email - : null} + {form.touched.email ? form.errors.email : null}
) : null}
)}
- {currentUser && data && currentUser?.id !== data?.id ? ( + {currentUser?.id !== data?.id ? (

diff --git a/frontend/src/modules/Permissions.js b/frontend/src/modules/Permissions.js index ebf0061195..3615ff98db 100644 --- a/frontend/src/modules/Permissions.js +++ b/frontend/src/modules/Permissions.js @@ -19,6 +19,6 @@ const hasPermission = (section, perm, userPerms, roles) => { return acceptable.indexOf(v) !== -1; }; -const isAdmin = (roles) => roles?.includes("admin") || false; +const isAdmin = (roles) => roles?.includes("admin"); export { hasPermission, isAdmin }; diff --git a/frontend/src/modules/Validations.jsx b/frontend/src/modules/Validations.jsx index e2bb0a0835..c0c7f31db6 100644 --- a/frontend/src/modules/Validations.jsx +++ b/frontend/src/modules/Validations.jsx @@ -5,32 +5,23 @@ const domainPattern = /^(?!.*:[0-9]+$).+$/; const ipv4Pattern = /^[0-9.]+$/; const upstreamUrlPattern = /^https?:\/\/([^/:]+|\[[a-fA-F0-9:]+\]):[0-9]+$/; -const validateString = (minLength = 0, maxLength = 0) => { - if (minLength <= 0 && maxLength <= 0) { - // this doesn't require translation - console.error("validateString() must be called with a min or max or both values in order to work!"); - } - - return (value) => { - if (minLength && (typeof value === "undefined" || value.length === 0)) { +const validateString = + (minLength = 0, maxLength = 0) => + (value) => { + if (minLength && value.length === 0) { return intl.formatMessage({ id: "error.required" }); } - if (minLength && value.length < minLength) { + if (value.length < minLength) { return intl.formatMessage({ id: "error.min-character-length" }, { min: minLength }); } - if (maxLength && (typeof value === "undefined" || value.length > maxLength)) { + if (maxLength && value.length > maxLength) { return intl.formatMessage({ id: "error.max-character-length" }, { max: maxLength }); } }; -}; - -const validateNumber = (min = -1, max = -1) => { - if (min === -1 && max === -1) { - // this doesn't require translation - console.error("validateNumber() must be called with a min or max or both values in order to work!"); - } - return (value) => { +const validateNumber = + (min = -1, max = -1) => + (value) => { const int = Number(value); if (min > -1 && !int) { return intl.formatMessage({ id: "error.required" }); @@ -42,7 +33,6 @@ const validateNumber = (min = -1, max = -1) => { return intl.formatMessage({ id: "error.maximum" }, { max }); } }; -}; const validateEmail = () => (value) => { if (value.length === 0) { @@ -53,9 +43,9 @@ const validateEmail = () => (value) => { } }; -const validateDomain = (allowWildcards = false) => { +const validateDomain = (allowWildcards) => { return (d) => { - const dom = d.trim().toLowerCase(); + const dom = d.trim(); if (!allowWildcards) { // Block wildcards @@ -79,7 +69,7 @@ const validateDomain = (allowWildcards = false) => { }; }; -const validateDomains = (allowWildcards, maxDomains) => { +const validateDomains = (allowWildcards) => { const vDom = validateDomain(allowWildcards); return (value) => { @@ -87,13 +77,8 @@ const validateDomains = (allowWildcards, maxDomains) => { return intl.formatMessage({ id: "error.required" }); } - // Deny if the list of domains is hit - if (maxDomains && value?.length >= maxDomains) { - return intl.formatMessage({ id: "error.max-domains" }, { max: maxDomains }); - } - // validate each domain - for (const domain of value ?? []) { + for (const domain of value) { if (!vDom(domain)) { return intl.formatMessage({ id: "error.invalid-domain" }, { domain }); } diff --git a/frontend/src/notifications/Msg.jsx b/frontend/src/notifications/Msg.jsx index 1735323f67..5d4a52259c 100644 --- a/frontend/src/notifications/Msg.jsx +++ b/frontend/src/notifications/Msg.jsx @@ -2,7 +2,7 @@ import { IconCheck, IconExclamationCircle } from "@tabler/icons-react"; import cn from "clsx"; function Msg({ data }) { - const cns = cn("toast", "show", data.type || null); + const cns = cn("toast", "show", data.type); let icon = null; switch (data.type) { @@ -25,11 +25,9 @@ function Msg({ data }) { data-bs-autohide="false" data-bs-toggle="toast" > - {data.title && ( -
- {icon} {data.title} -
- )} +
+ {icon} {data.title} +
{data.message}

); diff --git a/frontend/src/pages/Access/TableWrapper.jsx b/frontend/src/pages/Access/TableWrapper.jsx index 6aa9f08bff..fa0d261f65 100644 --- a/frontend/src/pages/Access/TableWrapper.jsx +++ b/frontend/src/pages/Access/TableWrapper.jsx @@ -26,11 +26,11 @@ export default function TableWrapper() { } if (isError) { - return {error?.message || "Unknown error"}; + return {error?.message}; } let filtered = null; - if (search && data) { + if (search) { filtered = data?.filter((item) => item.name.toLowerCase().includes(search)); } @@ -82,7 +82,7 @@ export default function TableWrapper() { showAccessListModal(id)} onDelete={(id) => { const accessList = data?.find((item) => item.id === id); diff --git a/frontend/src/pages/AuditLog/TableWrapper.jsx b/frontend/src/pages/AuditLog/TableWrapper.jsx index d0150916ae..24ab28cce1 100644 --- a/frontend/src/pages/AuditLog/TableWrapper.jsx +++ b/frontend/src/pages/AuditLog/TableWrapper.jsx @@ -23,14 +23,14 @@ export default function TableWrapper() { } if (isError) { - return {error?.message || "Unknown error"}; + return {error?.message}; } let filtered = null; - if (search && data) { + if (search) { filtered = data.filter((item) => { - const metaText = JSON.stringify(item.meta || {}).toLowerCase(); - const value = [item.objectType, item.action, metaText].filter(Boolean).join(" ").toLowerCase(); + const metaText = JSON.stringify(item.meta).toLowerCase(); + const value = [item.objectType, item.action, metaText].join(" "); return value.includes(search); }); diff --git a/frontend/src/pages/Certificates/Table.jsx b/frontend/src/pages/Certificates/Table.jsx index b1515e0031..e077f1504c 100644 --- a/frontend/src/pages/Certificates/Table.jsx +++ b/frontend/src/pages/Certificates/Table.jsx @@ -74,7 +74,7 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload, domains={value.domainNames} createdOn={value.createdOn} niceName={value.niceName} - provider={value.provider || ""} + provider={value.provider} /> ); }, @@ -87,7 +87,7 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload, cell: (info) => { const r = info.row.original; if (r.provider === "letsencrypt") { - if (r.npmplusDnsChallenge && r.npmplusDnsProvider) { + if (r.npmplusDnsChallenge) { return ( <> – {r.npmplusDnsProvider} @@ -106,7 +106,7 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload, columnHelper.accessor((row) => row.expiresOn, { id: "expiresOn", header: intl.formatMessage({ id: "column.expires" }), - cell: (info) => , + cell: (info) => , }), columnHelper.accessor(isInUse, { id: "inUse", @@ -115,10 +115,10 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload, const r = info.row.original; return ( ); }, diff --git a/frontend/src/pages/Certificates/TableWrapper.jsx b/frontend/src/pages/Certificates/TableWrapper.jsx index 89d4ce30f6..f72b82a325 100644 --- a/frontend/src/pages/Certificates/TableWrapper.jsx +++ b/frontend/src/pages/Certificates/TableWrapper.jsx @@ -44,7 +44,7 @@ export default function TableWrapper() { } if (isError) { - return {error?.message || "Unknown error"}; + return {error?.message}; } const handleDownload = async (id) => { @@ -56,7 +56,7 @@ export default function TableWrapper() { }; let filtered = null; - if (search && data) { + if (search) { filtered = data?.filter( (item) => item.domainNames.some((domain) => domain.toLowerCase().includes(search)) || @@ -152,7 +152,7 @@ export default function TableWrapper() {
{certificate.npmplusDnsProvider ? ` – ${certificate.npmplusDnsProvider}` : null} - {certificate.expiresOn ? ( -
- -
- ) : null} +
+ +
) : null, }); diff --git a/frontend/src/pages/Login/index.jsx b/frontend/src/pages/Login/index.jsx index f2ae3581a5..5fd6ea3187 100644 --- a/frontend/src/pages/Login/index.jsx +++ b/frontend/src/pages/Login/index.jsx @@ -20,9 +20,7 @@ function TotpForm() { try { await submitTotp(values); } catch (err) { - if (err instanceof Error) { - setFormErr(err.message); - } + setFormErr(err.message); resetForm(); } setSubmitting(false); @@ -40,9 +38,7 @@ function TotpForm() {

- {formErr !== "" && ( - {intl.formatMessage({ id: formErr, defaultMessage: formErr })} - )} + {formErr !== "" && {intl.formatMessage({ id: formErr })}} {({ isSubmitting }) => (
@@ -117,9 +113,7 @@ function PasswordForm() { try { await login(values.email, values.password); } catch (err) { - if (err instanceof Error) { - setFormErr(err.message); - } + setFormErr(err.message); } setSubmitting(false); }; @@ -140,9 +134,7 @@ function PasswordForm() {

- {formErr !== "" && ( - {intl.formatMessage({ id: formErr, defaultMessage: formErr })} - )} + {formErr !== "" && {intl.formatMessage({ id: formErr })}} {health.data?.password && ( {error?.message || "Unknown error"}; + return {error?.message}; } const handleDisableToggle = async (id, enabled) => { @@ -42,7 +42,7 @@ export default function TableWrapper() { }; let filtered = null; - if (search && data) { + if (search) { filtered = data?.filter((item) => { const directory = getDirectory(item).toLowerCase(); return ( @@ -55,7 +55,7 @@ export default function TableWrapper() { const groupingActive = displayedHosts.some(getDirectory); const sharedTableProps = { - isFiltered: Boolean(search), + isFiltered: search, isFetching, sorting, onSortingChange: setSorting, diff --git a/frontend/src/pages/Nginx/ProxyHosts/Table.jsx b/frontend/src/pages/Nginx/ProxyHosts/Table.jsx index 9d8b33ddd7..eabae5ac2b 100644 --- a/frontend/src/pages/Nginx/ProxyHosts/Table.jsx +++ b/frontend/src/pages/Nginx/ProxyHosts/Table.jsx @@ -93,7 +93,7 @@ export default function Table({ }), columnHelper.accessor( (row) => { - const accessLists = row.accessLists || []; + const { accessLists } = row; const triggerLabel = intl.formatMessage({ id: row.npmplusAccessListType === "custom" ? "access-list.custom" : "access-list.public", }); @@ -227,7 +227,7 @@ export default function Table({ isFetching, }, enableSortingRemoval: false, - state: sorting ? { sorting } : undefined, + state: { sorting }, onSortingChange, }); diff --git a/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.jsx b/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.jsx index a04fe8d92f..097a51a478 100644 --- a/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.jsx +++ b/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.jsx @@ -29,7 +29,7 @@ export default function TableWrapper() { } if (isError) { - return {error?.message || "Unknown error"}; + return {error?.message}; } const handleDisableToggle = async (id, enabled) => { @@ -42,7 +42,7 @@ export default function TableWrapper() { }; let filtered = null; - if (search && data) { + if (search) { filtered = data?.filter((item) => { const directory = getDirectory(item).toLowerCase(); return ( @@ -58,7 +58,7 @@ export default function TableWrapper() { const groupingActive = displayedHosts.some(getDirectory); const sharedTableProps = { - isFiltered: Boolean(search), + isFiltered: search, isFetching, sorting, onSortingChange: setSorting, diff --git a/frontend/src/pages/Nginx/RedirectionHosts/Table.jsx b/frontend/src/pages/Nginx/RedirectionHosts/Table.jsx index 33bfb7b481..0c3fa68935 100644 --- a/frontend/src/pages/Nginx/RedirectionHosts/Table.jsx +++ b/frontend/src/pages/Nginx/RedirectionHosts/Table.jsx @@ -194,7 +194,7 @@ export default function Table({ isFetching, }, enableSortingRemoval: false, - state: sorting ? { sorting } : undefined, + state: { sorting }, onSortingChange, }); diff --git a/frontend/src/pages/Nginx/RedirectionHosts/TableWrapper.jsx b/frontend/src/pages/Nginx/RedirectionHosts/TableWrapper.jsx index 8167e1a30e..178c6124a5 100644 --- a/frontend/src/pages/Nginx/RedirectionHosts/TableWrapper.jsx +++ b/frontend/src/pages/Nginx/RedirectionHosts/TableWrapper.jsx @@ -29,7 +29,7 @@ export default function TableWrapper() { } if (isError) { - return {error?.message || "Unknown error"}; + return {error?.message}; } const handleDisableToggle = async (id, enabled) => { @@ -42,7 +42,7 @@ export default function TableWrapper() { }; let filtered = null; - if (search && data) { + if (search) { filtered = data?.filter((item) => { const directory = getDirectory(item).toLowerCase(); return ( @@ -57,7 +57,7 @@ export default function TableWrapper() { const groupingActive = displayedHosts.some(getDirectory); const sharedTableProps = { - isFiltered: Boolean(search), + isFiltered: search, isFetching, sorting, onSortingChange: setSorting, diff --git a/frontend/src/pages/Nginx/Streams/Table.jsx b/frontend/src/pages/Nginx/Streams/Table.jsx index 75075cb1d5..b64f363a38 100644 --- a/frontend/src/pages/Nginx/Streams/Table.jsx +++ b/frontend/src/pages/Nginx/Streams/Table.jsx @@ -72,7 +72,7 @@ export default function Table({ return ; }, }), - columnHelper.accessor((row) => row.npmplusDescription || "", { + columnHelper.accessor((row) => row.npmplusDescription, { id: "npmplusDescription", header: intl.formatMessage({ id: "column.description" }), cell: (info) => { @@ -244,7 +244,7 @@ export default function Table({ isFetching, }, enableSortingRemoval: false, - state: sorting ? { sorting } : undefined, + state: { sorting }, onSortingChange, }); diff --git a/frontend/src/pages/Nginx/Streams/TableWrapper.jsx b/frontend/src/pages/Nginx/Streams/TableWrapper.jsx index d3be30f42f..3e620061e4 100644 --- a/frontend/src/pages/Nginx/Streams/TableWrapper.jsx +++ b/frontend/src/pages/Nginx/Streams/TableWrapper.jsx @@ -29,7 +29,7 @@ export default function TableWrapper() { } if (isError) { - return {error?.message || "Unknown error"}; + return {error?.message}; } const handleDisableToggle = async (id, enabled) => { @@ -42,14 +42,14 @@ export default function TableWrapper() { }; let filtered = null; - if (search && data) { + if (search) { filtered = data?.filter((item) => { const directory = getDirectory(item).toLowerCase(); return ( `${item.incomingPort}`.includes(search) || `${item.forwardingPort}`.includes(search) || item.forwardingHost.includes(search) || - item.npmplusDescription?.toLowerCase().includes(search.toLowerCase()) || + item.npmplusDescription?.toLowerCase().includes(search) || directory.includes(search) ); }); @@ -59,7 +59,7 @@ export default function TableWrapper() { const groupingActive = displayedStreams.some(getDirectory); const sharedTableProps = { - isFiltered: Boolean(search), + isFiltered: search, isFetching, sorting, onSortingChange: setSorting, diff --git a/frontend/src/pages/Settings/DefaultSite.jsx b/frontend/src/pages/Settings/DefaultSite.jsx index a25c976cc1..598a2a1038 100644 --- a/frontend/src/pages/Settings/DefaultSite.jsx +++ b/frontend/src/pages/Settings/DefaultSite.jsx @@ -40,7 +40,7 @@ export default function DefaultSite() { }); }; - if (!isLoading && error) { + if (error) { return (
@@ -75,7 +75,7 @@ export default function DefaultSite() { {({ values }) => (
- setErrorMsg(null)} dismissible> + setErrorMsg(null)} dismissible> {errorMsg} @@ -204,9 +204,7 @@ export default function DefaultSite() { {form.errors.redirect ? (
- {form.errors.redirect && form.touched.redirect - ? form.errors.redirect - : null} + {form.touched.redirect ? form.errors.redirect : null}
) : null}
@@ -236,9 +234,7 @@ export default function DefaultSite() { {form.errors.html ? (
- {form.errors.html && form.touched.html - ? form.errors.html - : null} + {form.touched.html ? form.errors.html : null}
) : null}
diff --git a/frontend/src/pages/Setup/index.jsx b/frontend/src/pages/Setup/index.jsx index a873688d61..14dc0508b8 100644 --- a/frontend/src/pages/Setup/index.jsx +++ b/frontend/src/pages/Setup/index.jsx @@ -30,21 +30,17 @@ export default function Setup() { try { const user = await createSetupUser(payload); - if (user?.id) { - if (health.data?.password === false) { - window.location.href = "/api/oidc"; - return; - } - try { - await login(user.email, password); - // Trigger a Health change - await queryClient.refetchQueries({ queryKey: ["health"] }); - // window.location.reload(); - } catch (err) { - setErrorMsg(err.message); - } - } else { - setErrorMsg("cannot_create_user"); + if (health.data?.password === false) { + window.location.href = "/api/oidc"; + return; + } + try { + await login(user.email, password); + // Trigger a Health change + await queryClient.refetchQueries({ queryKey: ["health"] }); + // window.location.reload(); + } catch (err) { + setErrorMsg(err.message); } } catch (err) { setErrorMsg(err.message); @@ -63,7 +59,7 @@ export default function Setup() {
- setErrorMsg(null)} dismissible> + setErrorMsg(null)} dismissible> {errorMsg} {form.errors.name ? (
- {form.errors.name && form.touched.name - ? form.errors.name - : null} + {form.touched.name ? form.errors.name : null}
) : null}
@@ -134,9 +128,7 @@ export default function Setup() { {form.errors.email ? (
- {form.errors.email && form.touched.email - ? form.errors.email - : null} + {form.touched.email ? form.errors.email : null}
) : null} diff --git a/frontend/src/pages/Users/TableWrapper.jsx b/frontend/src/pages/Users/TableWrapper.jsx index 9334daa17a..ab27bc238b 100644 --- a/frontend/src/pages/Users/TableWrapper.jsx +++ b/frontend/src/pages/Users/TableWrapper.jsx @@ -28,7 +28,7 @@ export default function TableWrapper() { } if (isError) { - return {error?.message || "Unknown error"}; + return {error?.message}; } const handleDisableToggle = async (id, enabled) => { @@ -41,7 +41,7 @@ export default function TableWrapper() { }; let filtered = null; - if (search && data) { + if (search) { filtered = data?.filter( (item) => item.name.toLowerCase().includes(search) || item.email.toLowerCase().includes(search), ); @@ -83,7 +83,7 @@ export default function TableWrapper() {
showUserModal(id)} From 5ab361c1259a17a0e83c7ace083d0056d33585c9 Mon Sep 17 00:00:00 2001 From: Zoey Date: Sat, 10 Oct 2026 22:56:22 +0200 Subject: [PATCH 33/62] add default for custom locations --- .../src/components/Form/LocationsFields.jsx | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/Form/LocationsFields.jsx b/frontend/src/components/Form/LocationsFields.jsx index 5799688e44..eb0886fbcf 100644 --- a/frontend/src/components/Form/LocationsFields.jsx +++ b/frontend/src/components/Form/LocationsFields.jsx @@ -19,18 +19,6 @@ import { upstreamUrlPattern } from "src/modules/Validations"; export function LocationsFields({ initialValues, name = "locations" }) { const nextUiKey = useRef(0); const createUiLocation = (item) => ({ - ...item, - uiKey: nextUiKey.current++, - }); - - const [values, setValues] = useState(initialValues.map(createUiLocation)); - const { setFieldValue } = useFormikContext(); - const [advVisible, setAdvVisible] = useState([]); - const [expanded, setExpanded] = useState([]); - const [filter, setFilter] = useState(""); - const scrollToKey = useRef(null); - - const blankItem = { npmplusEnabled: true, path: "", locationType: "", @@ -50,12 +38,21 @@ export function LocationsFields({ initialValues, name = "locations" }) { npmplusAuthRequestUpstream: "", npmplusAccessListType: "global", id: null, - }; + ...item, + uiKey: nextUiKey.current++, + }); + + const [values, setValues] = useState(initialValues.map(createUiLocation)); + const { setFieldValue } = useFormikContext(); + const [advVisible, setAdvVisible] = useState([]); + const [expanded, setExpanded] = useState([]); + const [filter, setFilter] = useState(""); + const scrollToKey = useRef(null); const toggle = (key, list, set) => set(list.includes(key) ? list.filter((i) => i !== key) : [...list, key]); const handleAdd = () => { - const item = createUiLocation(blankItem); + const item = createUiLocation(); const newValues = [...values, item]; setValues(newValues); setExpanded([...expanded, item.uiKey]); @@ -116,7 +113,7 @@ export function LocationsFields({ initialValues, name = "locations" }) { const isOpen = (item) => expanded.includes(item.uiKey); - const locationLabel = (item) => `${item.locationType ?? ""}${item.path}`; + const locationLabel = (item) => `${item.locationType}${item.path}`; const forwardSummary = ({ forwardScheme, forwardHost, forwardPort }) => { if (!forwardHost || forwardScheme === "empty") return ""; @@ -244,10 +241,10 @@ export function LocationsFields({ initialValues, name = "locations" }) { handleChange(idx, "npmplusEnabled", e.target.checked)} /> @@ -592,7 +589,7 @@ export function LocationsFields({ initialValues, name = "locations" }) { className={`form-control ${item.npmplusAuthRequestUpstream && !upstreamUrlPattern.test(item.npmplusAuthRequestUpstream) ? "is-invalid" : ""}`} placeholder="keep empty to reuse env value" pattern="^https?://([^/:]+|\[[a-fA-F0-9:]+\]):[0-9]+$" - value={item.npmplusAuthRequestUpstream || ""} + value={item.npmplusAuthRequestUpstream} onChange={(e) => handleChange( idx, From 06b02f77319cf3e0a536a389bc59b78ed11c5afa Mon Sep 17 00:00:00 2001 From: Alexis Loskoutoff Date: Sun, 11 Oct 2026 21:53:42 +0200 Subject: [PATCH 34/62] Add missing FR translations --- frontend/src/locale/src/fr.json | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/frontend/src/locale/src/fr.json b/frontend/src/locale/src/fr.json index 0911eedc39..cbdf3cd1b2 100644 --- a/frontend/src/locale/src/fr.json +++ b/frontend/src/locale/src/fr.json @@ -71,6 +71,12 @@ "access-list.help.rules-order": { "defaultMessage": "Notez que les directives autoriser et refuser seront appliquées dans l'ordre où elles sont définies." }, + "access-list.inherit": { + "defaultMessage": "Hériter de l'hôte proxy" + }, + "access-list.inherit.subtitle": { + "defaultMessage": "Utilise la liste d'accès de l'hôte proxy" + }, "access-list.pass-auth": { "defaultMessage": "Transmettre l'authentification au serveur en amont" }, @@ -101,6 +107,9 @@ "action.allow": { "defaultMessage": "Autoriser" }, + "action.clear": { + "defaultMessage": "Effacer" + }, "action.close": { "defaultMessage": "Fermer" }, @@ -122,6 +131,9 @@ "action.enable": { "defaultMessage": "Activer" }, + "action.logs": { + "defaultMessage": "Journaux" + }, "action.permissions": { "defaultMessage": "Permissions" }, @@ -266,6 +278,9 @@ "column.email": { "defaultMessage": "e-mail" }, + "column.error": { + "defaultMessage": "Erreurs" + }, "column.event": { "defaultMessage": "Évènement" }, @@ -461,6 +476,12 @@ "loading": { "defaultMessage": "Chargement…" }, + "location.advanced-config": { + "defaultMessage": "Configuration Nginx personnalisée" + }, + "location.filter": { + "defaultMessage": "Filtrer par chemin ou destination" + }, "login.2fa-code": { "defaultMessage": "Code de vérification" }, @@ -479,6 +500,51 @@ "login.title": { "defaultMessage": "Connectez-vous à votre compte" }, + "logs": { + "defaultMessage": "Journaux" + }, + "logs.channel.access": { + "defaultMessage": "Accès" + }, + "logs.channel.error": { + "defaultMessage": "Erreurs" + }, + "logs.download": { + "defaultMessage": "Télécharger" + }, + "logs.empty": { + "defaultMessage": "Aucune entrée pour le moment." + }, + "logs.level": { + "defaultMessage": "Niveau" + }, + "logs.level.all": { + "defaultMessage": "Tous les niveaux" + }, + "logs.lines": { + "defaultMessage": "Lignes" + }, + "logs.live": { + "defaultMessage": "En direct" + }, + "logs.paused": { + "defaultMessage": "En pause" + }, + "logs.refresh": { + "defaultMessage": "Actualiser" + }, + "logs.search-placeholder": { + "defaultMessage": "Rechercher dans les journaux…" + }, + "logs.source": { + "defaultMessage": "Source" + }, + "logs.source.system": { + "defaultMessage": "Système" + }, + "logs.truncated-warning": { + "defaultMessage": "Seule la partie la plus récente de ce fichier est affichée." + }, "nginx-config.label": { "defaultMessage": "Configuration Nginx personnalisée" }, From 41713715ca6da426049e54531f41e4e0c270f877 Mon Sep 17 00:00:00 2001 From: Alexis Loskoutoff Date: Sun, 11 Oct 2026 22:55:39 +0200 Subject: [PATCH 35/62] Improve existing french translations --- frontend/src/locale/src/fr.json | 64 ++++++++++++++++----------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/frontend/src/locale/src/fr.json b/frontend/src/locale/src/fr.json index cbdf3cd1b2..88add74bd7 100644 --- a/frontend/src/locale/src/fr.json +++ b/frontend/src/locale/src/fr.json @@ -1,6 +1,6 @@ { "2fa.backup-codes-remaining": { - "defaultMessage": "Codes de secours restants : {count}" + "defaultMessage": "Codes de secours restants : {count}" }, "2fa.backup-warning": { "defaultMessage": "Sauvegardez ces codes de secours dans un endroit sûr. Chaque code ne peut être utilisé qu'une seule fois." @@ -90,10 +90,10 @@ "defaultMessage": "192.168.1.100 ou 192.168.1.0/24 ou 2001:0db8::/32" }, "access-list.satisfy-any": { - "defaultMessage": "Valide n'importe quelle règle" + "defaultMessage": "Satisfaire au moins une condition" }, "access-list.subtitle": { - "defaultMessage": "{utilisateurs} {utilisateurs, plural, one {Utilisateur} other {Utilisateurs}}, {règles} {règles, plural, one {Règle} other {Règles}} - Crée : {date}" + "defaultMessage": "{users} {users, plural, one {Utilisateur} other {Utilisateurs}}, {rules} {rules, plural, one {Règle} other {Règles}} - Créée le {date}" }, "access-lists": { "defaultMessage": "Listes d'accès" @@ -102,7 +102,7 @@ "defaultMessage": "Ajouter" }, "action.add-location": { - "defaultMessage": "Ajouter localisation" + "defaultMessage": "Ajouter un emplacement" }, "action.allow": { "defaultMessage": "Autoriser" @@ -141,7 +141,7 @@ "defaultMessage": "Renouveler" }, "action.view-details": { - "defaultMessage": "Voir les Détails" + "defaultMessage": "Voir les détails" }, "auditlogs": { "defaultMessage": "Journaux d'audit" @@ -159,7 +159,7 @@ "defaultMessage": "Certificat" }, "certificate.custom-certificate-key": { - "defaultMessage": "Clé du Certificat" + "defaultMessage": "Clé du certificat" }, "certificate.custom-intermediate": { "defaultMessage": "Certificat intermédiaire" @@ -180,7 +180,7 @@ "defaultMessage": "Non utilisé" }, "certificate.renew": { - "defaultMessage": "Renouveler Certificat" + "defaultMessage": "Renouveler le certificat" }, "certificates": { "defaultMessage": "Certificats" @@ -201,7 +201,7 @@ "defaultMessage": "Ces données seront stockées en clair dans la base de données et dans un fichier !" }, "certificates.dns.propagation-seconds": { - "defaultMessage": "Propagation Seconds" + "defaultMessage": "Délai de propagation (secondes)" }, "certificates.dns.propagation-seconds-note": { "defaultMessage": "Laisser vide pour utiliser la valeur par défaut du plugin. Nombre de secondes à attendre pour la propagation DNS." @@ -228,10 +228,10 @@ "defaultMessage": "Votre serveur est accessible et la création de certificats devrait être possible." }, "certificates.http.reachability-other": { - "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il a renvoyé un code d'état inattendu {code}. S'agit-il du serveur NPM ? Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." + "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il a renvoyé un code d'état inattendu {code}. S'agit-il du serveur NPM ? Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." }, "certificates.http.reachability-wrong-data": { - "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il a renvoyé des données inattendues. S'agit-il du serveur NPM ? Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." + "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il a renvoyé des données inattendues. S'agit-il du serveur NPM ? Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." }, "certificates.http.test-results": { "defaultMessage": "Résultats du test" @@ -267,7 +267,7 @@ "defaultMessage": "Autorisations" }, "column.custom-locations": { - "defaultMessage": "Emplacement personnalisé" + "defaultMessage": "Emplacements personnalisés" }, "column.destination": { "defaultMessage": "Destination" @@ -309,13 +309,13 @@ "defaultMessage": "Règles" }, "column.satisfy": { - "defaultMessage": "Valide" + "defaultMessage": "Conditions requises" }, "column.satisfy-all": { - "defaultMessage": "All" + "defaultMessage": "Toutes" }, "column.satisfy-any": { - "defaultMessage": "Any" + "defaultMessage": "Au moins une" }, "column.scheme": { "defaultMessage": "Schéma" @@ -330,7 +330,7 @@ "defaultMessage": "Statut" }, "created-on": { - "defaultMessage": "Créé : {date}" + "defaultMessage": "Création : {date}" }, "dashboard": { "defaultMessage": "Tableau de bord" @@ -357,10 +357,10 @@ "defaultMessage": "Commencez à écrire pour ajouter un domaine…" }, "domain-names.wildcards-not-permitted": { - "defaultMessage": "Les Wildcards ne sont pas permises dans ce cas" + "defaultMessage": "Les wildcards ne sont pas permises dans ce cas" }, "domain-names.wildcards-not-supported": { - "defaultMessage": "Les Wildcards ne sont pas prises en charge par cette autorité de certification." + "defaultMessage": "Les wildcards ne sont pas prises en charge par cette autorité de certification." }, "domains.advanced": { "defaultMessage": "Avancé" @@ -390,7 +390,7 @@ "defaultMessage": "Aucun résultat trouvé" }, "empty-subtitle": { - "defaultMessage": "Pourquoi n'en créez-vous pas un ?" + "defaultMessage": "Pourquoi n'en créez-vous pas un ?" }, "enabled": { "defaultMessage": "Activé" @@ -405,7 +405,7 @@ "defaultMessage": "Adresse e-mail ou mot de passe invalide" }, "error.invalid-domain": { - "defaultMessage": "Domaine invalide : {domain}" + "defaultMessage": "Domaine invalide : {domain}" }, "error.invalid-email": { "defaultMessage": "Adresse e-mail invalide" @@ -432,16 +432,16 @@ "defaultMessage": "Ceci est obligatoire" }, "expires.on": { - "defaultMessage": "Expire : {date}" + "defaultMessage": "Expire le {date}" }, "footer.github-fork": { - "defaultMessage": "Forkez-moi sur Github" + "defaultMessage": "Forkez-moi sur GitHub" }, "host.flags.block-exploits": { "defaultMessage": "Bloquer les exploits courants" }, "host.flags.cache-assets": { - "defaultMessage": "Ressources du cache" + "defaultMessage": "Mise en cache des ressources" }, "host.flags.preserve-path": { "defaultMessage": "Préserver le chemin" @@ -450,10 +450,10 @@ "defaultMessage": "Protocoles" }, "host.flags.websockets-upgrade": { - "defaultMessage": "Prise en charge de Websockets" + "defaultMessage": "Prise en charge de WebSockets" }, "host.forward-port": { - "defaultMessage": "Port de redirection" + "defaultMessage": "Port de destination" }, "host.forward-scheme": { "defaultMessage": "Schéma" @@ -549,7 +549,7 @@ "defaultMessage": "Configuration Nginx personnalisée" }, "nginx-config.placeholder": { - "defaultMessage": "# Mettez ici votre configuration Nginx personnalisé à vos risques et périls !" + "defaultMessage": "# Mettez ici votre configuration Nginx personnalisée à vos risques et périls !" }, "no-permission-error": { "defaultMessage": "Vous n'avez pas la permission de voir ce contenu." @@ -663,7 +663,7 @@ "defaultMessage": "Hôte proxy" }, "proxy-host.forward-host": { - "defaultMessage": "Nom d'hôte de redirection / IP" + "defaultMessage": "Nom d'hôte / IP de destination" }, "proxy-hosts": { "defaultMessage": "Hôtes proxy" @@ -672,7 +672,7 @@ "defaultMessage": "{count} {count, plural, one {Hôte proxy} other {Hôtes proxy}}" }, "public": { - "defaultMessage": "Publique" + "defaultMessage": "Public" }, "redirection-host": { "defaultMessage": "Hôte de redirection" @@ -735,7 +735,7 @@ "defaultMessage": "Page de félicitations" }, "settings.default-site.description": { - "defaultMessage": "ce qu'il faut afficher lorsqu'un hôte inconnu est détecté par Nginx" + "defaultMessage": "Ce qu'il faut afficher lorsqu'un hôte inconnu est détecté par Nginx" }, "settings.default-site.html": { "defaultMessage": "HTML personnalisé" @@ -762,7 +762,7 @@ "defaultMessage": "Stream" }, "stream.forward-host": { - "defaultMessage": "Hôte destinataire" + "defaultMessage": "Hôte de destination" }, "stream.forward-host.placeholder": { "defaultMessage": "example.com ou 10.0.0.1 ou 2001:db8:3333:4444:5555:6666:7777:8888" @@ -786,7 +786,7 @@ "defaultMessage": "Test" }, "update-available": { - "defaultMessage": "Mise à jour disponible : {latestVersion}" + "defaultMessage": "Mise à jour disponible : {latestVersion}" }, "user": { "defaultMessage": "Utilisateur" @@ -825,10 +825,10 @@ "defaultMessage": "Définir les autorisations pour {name}" }, "user.switch-dark": { - "defaultMessage": "Passer au mode Sombre" + "defaultMessage": "Passer au mode sombre" }, "user.switch-light": { - "defaultMessage": "Passer au mode Lumineux" + "defaultMessage": "Passer au mode clair" }, "user.two-factor": { "defaultMessage": "Auth. à deux facteurs" From 0c995660e8e299c64dfb1e2e5e7a18afed4014ef Mon Sep 17 00:00:00 2001 From: Zoey Date: Mon, 12 Oct 2026 15:23:39 +0200 Subject: [PATCH 36/62] support cu_ for auth request upstreams --- README.md | 1 + .../schema/components/proxy-host-object.json | 2 +- backend/templates/proxy_host.conf | 110 ++++++++++++------ .../src/components/Form/LocationsFields.jsx | 2 +- frontend/src/modals/ProxyHostModal.jsx | 2 +- frontend/src/modules/Validations.jsx | 2 +- frontend/translations/ui/de.json | 2 +- frontend/translations/ui/en.json | 2 +- frontend/translations/ui/nl.json | 2 +- rootfs/usr/local/bin/envs.sh | 24 ++-- 10 files changed, 93 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index f55b5ca82f..ccde29705e 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,7 @@ upstream cu_mybackend { ``` 3. Configure your proxy host/stream like always in the UI, but set the hostname to the exact name of your upstream block (e.g. `cu_mybackend`) and leave the forward port field empty (ports are defined inside the upstream block) - The `cu_` prefix (short for **c**ustom **u**pstream) is required: NPMplus uses it to detect that the hostname refers to a custom upstream block and skips generating its own upstream block for it + - This also works for auth request upstreams (env or UI field), use the scheme and the name of your upstream block without port (e.g. `http://cu_myauth`), SNI and Host header are then set to the requested host like for proxy hosts, so the servers inside the upstream block need to point directly to the auth service and not to NPMplus itself ## Encrypted Client Hello (ECH) diff --git a/backend/schema/components/proxy-host-object.json b/backend/schema/components/proxy-host-object.json index 40e3a3c661..e9ad74898a 100644 --- a/backend/schema/components/proxy-host-object.json +++ b/backend/schema/components/proxy-host-object.json @@ -125,7 +125,7 @@ }, "npmplus_auth_request_upstream": { "type": "string", - "pattern": "^(https?://([^/:]+|\\[[a-fA-F0-9:]+\\]):[0-9]+)?$", + "pattern": "^(https?://(([^/:]+|\\[[a-fA-F0-9:]+\\]):[0-9]+|cu_[^/:]+))?$", "maxLength": 255, "example": "" }, diff --git a/backend/templates/proxy_host.conf b/backend/templates/proxy_host.conf index 1f942a5496..29b7add832 100644 --- a/backend/templates/proxy_host.conf +++ b/backend/templates/proxy_host.conf @@ -8,6 +8,7 @@ {% assign auth_request_anubis_upstream_resolved = auth_request_anubis_upstream_resolved | default: env.AUTH_REQUEST_ANUBIS_UPSTREAM %} {% assign auth_request_anubis_upstream_host = auth_request_anubis_upstream_resolved | replace: "https://", "" | replace: "http://", "" %} + {% assign auth_request_anubis_upstream_prefix = auth_request_anubis_upstream_host | slice: 0, 3 %} {% if auth_request_anubis_upstream_resolved contains "https://" %}{% assign auth_request_anubis_upstream_scheme = "https://" %}{% else %}{% assign auth_request_anubis_upstream_scheme = "http://" %}{% endif %} {% if auth_request_anubis_upstream_host contains "]:" %} {% assign auth_request_anubis_upstream_sni_name = auth_request_anubis_upstream_host | split: "]:" | first | append: "]" %} @@ -16,21 +17,27 @@ {% endif %} {% assign auth_request_anubis_upstream_port = auth_request_anubis_upstream_host | split: ":" | last %} {% assign auth_request_anubis_upstream_host_header = auth_request_anubis_upstream_sni_name %} - {% if auth_request_anubis_upstream_scheme == "http://" and auth_request_anubis_upstream_port != "80" %} + {% if auth_request_anubis_upstream_prefix == "cu_" %} + {% assign auth_request_anubis_upstream_sni_name = "$host" %} + {% assign auth_request_anubis_upstream_host_header = "$host$is_request_port$request_port" %} + {% elsif auth_request_anubis_upstream_scheme == "http://" and auth_request_anubis_upstream_port != "80" %} {% assign auth_request_anubis_upstream_host_header = auth_request_anubis_upstream_host %} {% elsif auth_request_anubis_upstream_scheme == "https://" and auth_request_anubis_upstream_port != "443" %} {% assign auth_request_anubis_upstream_host_header = auth_request_anubis_upstream_host %} {% endif %} {% if npmplus_auth_request == "anubis" or create_anubis_locations %} - upstream auth_request_anubis_{{ id }} { - zone auth_request_anubis_{{ id }} 128k; - server {{ auth_request_anubis_upstream_host }} resolve; - } + {% if auth_request_anubis_upstream_prefix != "cu_" %} + upstream auth_request_anubis_{{ id }} { + zone auth_request_anubis_{{ id }} 128k; + server {{ auth_request_anubis_upstream_host }} resolve; + } + {% endif %} {% endif %} {% assign auth_request_tinyauth_upstream_resolved = auth_request_tinyauth_upstream_resolved | default: env.AUTH_REQUEST_TINYAUTH_UPSTREAM %} {% assign auth_request_tinyauth_upstream_host = auth_request_tinyauth_upstream_resolved | replace: "https://", "" | replace: "http://", "" %} + {% assign auth_request_tinyauth_upstream_prefix = auth_request_tinyauth_upstream_host | slice: 0, 3 %} {% if auth_request_tinyauth_upstream_resolved contains "https://" %}{% assign auth_request_tinyauth_upstream_scheme = "https://" %}{% else %}{% assign auth_request_tinyauth_upstream_scheme = "http://" %}{% endif %} {% if auth_request_tinyauth_upstream_host contains "]:" %} {% assign auth_request_tinyauth_upstream_sni_name = auth_request_tinyauth_upstream_host | split: "]:" | first | append: "]" %} @@ -39,21 +46,27 @@ {% endif %} {% assign auth_request_tinyauth_upstream_port = auth_request_tinyauth_upstream_host | split: ":" | last %} {% assign auth_request_tinyauth_upstream_host_header = auth_request_tinyauth_upstream_sni_name %} - {% if auth_request_tinyauth_upstream_scheme == "http://" and auth_request_tinyauth_upstream_port != "80" %} + {% if auth_request_tinyauth_upstream_prefix == "cu_" %} + {% assign auth_request_tinyauth_upstream_sni_name = "$host" %} + {% assign auth_request_tinyauth_upstream_host_header = "$host$is_request_port$request_port" %} + {% elsif auth_request_tinyauth_upstream_scheme == "http://" and auth_request_tinyauth_upstream_port != "80" %} {% assign auth_request_tinyauth_upstream_host_header = auth_request_tinyauth_upstream_host %} {% elsif auth_request_tinyauth_upstream_scheme == "https://" and auth_request_tinyauth_upstream_port != "443" %} {% assign auth_request_tinyauth_upstream_host_header = auth_request_tinyauth_upstream_host %} {% endif %} {% if npmplus_auth_request == "tinyauth" or create_tinyauth_locations %} - upstream auth_request_tinyauth_{{ id }} { - zone auth_request_tinyauth_{{ id }} 128k; - server {{ auth_request_tinyauth_upstream_host }} resolve; - } + {% if auth_request_tinyauth_upstream_prefix != "cu_" %} + upstream auth_request_tinyauth_{{ id }} { + zone auth_request_tinyauth_{{ id }} 128k; + server {{ auth_request_tinyauth_upstream_host }} resolve; + } + {% endif %} {% endif %} {% assign auth_request_oauth2proxy_upstream_resolved = auth_request_oauth2proxy_upstream_resolved | default: env.AUTH_REQUEST_OAUTH2PROXY_UPSTREAM %} {% assign auth_request_oauth2proxy_upstream_host = auth_request_oauth2proxy_upstream_resolved | replace: "https://", "" | replace: "http://", "" %} + {% assign auth_request_oauth2proxy_upstream_prefix = auth_request_oauth2proxy_upstream_host | slice: 0, 3 %} {% if auth_request_oauth2proxy_upstream_resolved contains "https://" %}{% assign auth_request_oauth2proxy_upstream_scheme = "https://" %}{% else %}{% assign auth_request_oauth2proxy_upstream_scheme = "http://" %}{% endif %} {% if auth_request_oauth2proxy_upstream_host contains "]:" %} {% assign auth_request_oauth2proxy_upstream_sni_name = auth_request_oauth2proxy_upstream_host | split: "]:" | first | append: "]" %} @@ -62,21 +75,27 @@ {% endif %} {% assign auth_request_oauth2proxy_upstream_port = auth_request_oauth2proxy_upstream_host | split: ":" | last %} {% assign auth_request_oauth2proxy_upstream_host_header = auth_request_oauth2proxy_upstream_sni_name %} - {% if auth_request_oauth2proxy_upstream_scheme == "http://" and auth_request_oauth2proxy_upstream_port != "80" %} + {% if auth_request_oauth2proxy_upstream_prefix == "cu_" %} + {% assign auth_request_oauth2proxy_upstream_sni_name = "$host" %} + {% assign auth_request_oauth2proxy_upstream_host_header = "$host$is_request_port$request_port" %} + {% elsif auth_request_oauth2proxy_upstream_scheme == "http://" and auth_request_oauth2proxy_upstream_port != "80" %} {% assign auth_request_oauth2proxy_upstream_host_header = auth_request_oauth2proxy_upstream_host %} {% elsif auth_request_oauth2proxy_upstream_scheme == "https://" and auth_request_oauth2proxy_upstream_port != "443" %} {% assign auth_request_oauth2proxy_upstream_host_header = auth_request_oauth2proxy_upstream_host %} {% endif %} {% if npmplus_auth_request == "oauth2proxy" or create_oauth2proxy_locations %} - upstream auth_request_oauth2proxy_{{ id }} { - zone auth_request_oauth2proxy_{{ id }} 128k; - server {{ auth_request_oauth2proxy_upstream_host }} resolve; - } + {% if auth_request_oauth2proxy_upstream_prefix != "cu_" %} + upstream auth_request_oauth2proxy_{{ id }} { + zone auth_request_oauth2proxy_{{ id }} 128k; + server {{ auth_request_oauth2proxy_upstream_host }} resolve; + } + {% endif %} {% endif %} {% assign auth_request_voidauth_upstream_resolved = auth_request_voidauth_upstream_resolved | default: env.AUTH_REQUEST_VOIDAUTH_UPSTREAM %} {% assign auth_request_voidauth_upstream_host = auth_request_voidauth_upstream_resolved | replace: "https://", "" | replace: "http://", "" %} + {% assign auth_request_voidauth_upstream_prefix = auth_request_voidauth_upstream_host | slice: 0, 3 %} {% if auth_request_voidauth_upstream_resolved contains "https://" %}{% assign auth_request_voidauth_upstream_scheme = "https://" %}{% else %}{% assign auth_request_voidauth_upstream_scheme = "http://" %}{% endif %} {% if auth_request_voidauth_upstream_host contains "]:" %} {% assign auth_request_voidauth_upstream_sni_name = auth_request_voidauth_upstream_host | split: "]:" | first | append: "]" %} @@ -85,21 +104,27 @@ {% endif %} {% assign auth_request_voidauth_upstream_port = auth_request_voidauth_upstream_host | split: ":" | last %} {% assign auth_request_voidauth_upstream_host_header = auth_request_voidauth_upstream_sni_name %} - {% if auth_request_voidauth_upstream_scheme == "http://" and auth_request_voidauth_upstream_port != "80" %} + {% if auth_request_voidauth_upstream_prefix == "cu_" %} + {% assign auth_request_voidauth_upstream_sni_name = "$host" %} + {% assign auth_request_voidauth_upstream_host_header = "$host$is_request_port$request_port" %} + {% elsif auth_request_voidauth_upstream_scheme == "http://" and auth_request_voidauth_upstream_port != "80" %} {% assign auth_request_voidauth_upstream_host_header = auth_request_voidauth_upstream_host %} {% elsif auth_request_voidauth_upstream_scheme == "https://" and auth_request_voidauth_upstream_port != "443" %} {% assign auth_request_voidauth_upstream_host_header = auth_request_voidauth_upstream_host %} {% endif %} {% if npmplus_auth_request == "voidauth" or create_voidauth_locations %} - upstream auth_request_voidauth_{{ id }} { - zone auth_request_voidauth_{{ id }} 128k; - server {{ auth_request_voidauth_upstream_host }} resolve; - } + {% if auth_request_voidauth_upstream_prefix != "cu_" %} + upstream auth_request_voidauth_{{ id }} { + zone auth_request_voidauth_{{ id }} 128k; + server {{ auth_request_voidauth_upstream_host }} resolve; + } + {% endif %} {% endif %} {% assign auth_request_authelia_upstream_resolved = auth_request_authelia_upstream_resolved | default: env.AUTH_REQUEST_AUTHELIA_UPSTREAM %} {% assign auth_request_authelia_upstream_host = auth_request_authelia_upstream_resolved | replace: "https://", "" | replace: "http://", "" %} + {% assign auth_request_authelia_upstream_prefix = auth_request_authelia_upstream_host | slice: 0, 3 %} {% if auth_request_authelia_upstream_resolved contains "https://" %}{% assign auth_request_authelia_upstream_scheme = "https://" %}{% else %}{% assign auth_request_authelia_upstream_scheme = "http://" %}{% endif %} {% if auth_request_authelia_upstream_host contains "]:" %} {% assign auth_request_authelia_upstream_sni_name = auth_request_authelia_upstream_host | split: "]:" | first | append: "]" %} @@ -108,21 +133,27 @@ {% endif %} {% assign auth_request_authelia_upstream_port = auth_request_authelia_upstream_host | split: ":" | last %} {% assign auth_request_authelia_upstream_host_header = auth_request_authelia_upstream_sni_name %} - {% if auth_request_authelia_upstream_scheme == "http://" and auth_request_authelia_upstream_port != "80" %} + {% if auth_request_authelia_upstream_prefix == "cu_" %} + {% assign auth_request_authelia_upstream_sni_name = "$host" %} + {% assign auth_request_authelia_upstream_host_header = "$host$is_request_port$request_port" %} + {% elsif auth_request_authelia_upstream_scheme == "http://" and auth_request_authelia_upstream_port != "80" %} {% assign auth_request_authelia_upstream_host_header = auth_request_authelia_upstream_host %} {% elsif auth_request_authelia_upstream_scheme == "https://" and auth_request_authelia_upstream_port != "443" %} {% assign auth_request_authelia_upstream_host_header = auth_request_authelia_upstream_host %} {% endif %} {% if npmplus_auth_request == "authelia" or create_authelia_locations %} - upstream auth_request_authelia_{{ id }} { - zone auth_request_authelia_{{ id }} 128k; - server {{ auth_request_authelia_upstream_host }} resolve; - } + {% if auth_request_authelia_upstream_prefix != "cu_" %} + upstream auth_request_authelia_{{ id }} { + zone auth_request_authelia_{{ id }} 128k; + server {{ auth_request_authelia_upstream_host }} resolve; + } + {% endif %} {% endif %} {% assign auth_request_authentik_upstream_resolved = auth_request_authentik_upstream_resolved | default: env.AUTH_REQUEST_AUTHENTIK_UPSTREAM %} {% assign auth_request_authentik_upstream_host = auth_request_authentik_upstream_resolved | replace: "https://", "" | replace: "http://", "" %} + {% assign auth_request_authentik_upstream_prefix = auth_request_authentik_upstream_host | slice: 0, 3 %} {% if auth_request_authentik_upstream_resolved contains "https://" %}{% assign auth_request_authentik_upstream_scheme = "https://" %}{% else %}{% assign auth_request_authentik_upstream_scheme = "http://" %}{% endif %} {% if auth_request_authentik_upstream_host contains "]:" %} {% assign auth_request_authentik_upstream_sni_name = auth_request_authentik_upstream_host | split: "]:" | first | append: "]" %} @@ -131,17 +162,22 @@ {% endif %} {% assign auth_request_authentik_upstream_port = auth_request_authentik_upstream_host | split: ":" | last %} {% assign auth_request_authentik_upstream_host_header = auth_request_authentik_upstream_sni_name %} - {% if auth_request_authentik_upstream_scheme == "http://" and auth_request_authentik_upstream_port != "80" %} + {% if auth_request_authentik_upstream_prefix == "cu_" %} + {% assign auth_request_authentik_upstream_sni_name = "$host" %} + {% assign auth_request_authentik_upstream_host_header = "$host$is_request_port$request_port" %} + {% elsif auth_request_authentik_upstream_scheme == "http://" and auth_request_authentik_upstream_port != "80" %} {% assign auth_request_authentik_upstream_host_header = auth_request_authentik_upstream_host %} {% elsif auth_request_authentik_upstream_scheme == "https://" and auth_request_authentik_upstream_port != "443" %} {% assign auth_request_authentik_upstream_host_header = auth_request_authentik_upstream_host %} {% endif %} {% if npmplus_auth_request == "authentik" or npmplus_auth_request == "authentik-send-basic-auth" or create_authentik_locations %} - upstream auth_request_authentik_{{ id }} { - zone auth_request_authentik_{{ id }} 128k; - server {{ auth_request_authentik_upstream_host }} resolve; - } + {% if auth_request_authentik_upstream_prefix != "cu_" %} + upstream auth_request_authentik_{{ id }} { + zone auth_request_authentik_{{ id }} 128k; + server {{ auth_request_authentik_upstream_host }} resolve; + } + {% endif %} {% endif %} server { @@ -330,7 +366,7 @@ proxy_ssl_name {{ auth_request_anubis_upstream_sni_name }}; proxy_set_header Host {{ auth_request_anubis_upstream_host_header }}; proxy_set_header Accept-Encoding ""; - proxy_pass {{ auth_request_anubis_upstream_scheme }}auth_request_anubis_{{ id }}; + proxy_pass {{ auth_request_anubis_upstream_scheme }}{% if auth_request_anubis_upstream_prefix == "cu_" %}{{ auth_request_anubis_upstream_host }}{% else %}auth_request_anubis_{{ id }}{% endif %}; proxy_method GET; proxy_pass_request_body off; proxy_set_header Content-Length ""; @@ -351,7 +387,7 @@ proxy_ssl_name {{ auth_request_tinyauth_upstream_sni_name }}; proxy_set_header Host {{ auth_request_tinyauth_upstream_host_header }}; proxy_set_header Accept-Encoding ""; - proxy_pass {{ auth_request_tinyauth_upstream_scheme }}auth_request_tinyauth_{{ id }}/api/auth/nginx; + proxy_pass {{ auth_request_tinyauth_upstream_scheme }}{% if auth_request_tinyauth_upstream_prefix == "cu_" %}{{ auth_request_tinyauth_upstream_host }}{% else %}auth_request_tinyauth_{{ id }}{% endif %}/api/auth/nginx; internal; proxy_method GET; proxy_pass_request_body off; @@ -366,7 +402,7 @@ proxy_ssl_name {{ auth_request_oauth2proxy_upstream_sni_name }}; proxy_set_header Host {{ auth_request_oauth2proxy_upstream_host_header }}; proxy_set_header Accept-Encoding ""; - proxy_pass {{ auth_request_oauth2proxy_upstream_scheme }}auth_request_oauth2proxy_{{ id }}; + proxy_pass {{ auth_request_oauth2proxy_upstream_scheme }}{% if auth_request_oauth2proxy_upstream_prefix == "cu_" %}{{ auth_request_oauth2proxy_upstream_host }}{% else %}auth_request_oauth2proxy_{{ id }}{% endif %}; proxy_set_header X-Auth-Request-Redirect $scheme://$host$is_request_port$request_port$request_uri; } location = /oauth2/auth { @@ -375,7 +411,7 @@ proxy_ssl_name {{ auth_request_oauth2proxy_upstream_sni_name }}; proxy_set_header Host {{ auth_request_oauth2proxy_upstream_host_header }}; proxy_set_header Accept-Encoding ""; - proxy_pass {{ auth_request_oauth2proxy_upstream_scheme }}auth_request_oauth2proxy_{{ id }}; + proxy_pass {{ auth_request_oauth2proxy_upstream_scheme }}{% if auth_request_oauth2proxy_upstream_prefix == "cu_" %}{{ auth_request_oauth2proxy_upstream_host }}{% else %}auth_request_oauth2proxy_{{ id }}{% endif %}; internal; proxy_method GET; proxy_pass_request_body off; @@ -395,7 +431,7 @@ proxy_ssl_name {{ auth_request_voidauth_upstream_sni_name }}; proxy_set_header Host {{ auth_request_voidauth_upstream_host_header }}; proxy_set_header Accept-Encoding ""; - proxy_pass {{ auth_request_voidauth_upstream_scheme }}auth_request_voidauth_{{ id }}/api/authz/auth-request; + proxy_pass {{ auth_request_voidauth_upstream_scheme }}{% if auth_request_voidauth_upstream_prefix == "cu_" %}{{ auth_request_voidauth_upstream_host }}{% else %}auth_request_voidauth_{{ id }}{% endif %}/api/authz/auth-request; internal; proxy_method GET; proxy_pass_request_body off; @@ -412,7 +448,7 @@ proxy_ssl_name {{ auth_request_authelia_upstream_sni_name }}; proxy_set_header Host {{ auth_request_authelia_upstream_host_header }}; proxy_set_header Accept-Encoding ""; - proxy_pass {{ auth_request_authelia_upstream_scheme }}auth_request_authelia_{{ id }}/api/authz/auth-request; + proxy_pass {{ auth_request_authelia_upstream_scheme }}{% if auth_request_authelia_upstream_prefix == "cu_" %}{{ auth_request_authelia_upstream_host }}{% else %}auth_request_authelia_{{ id }}{% endif %}/api/authz/auth-request; internal; proxy_method GET; proxy_pass_request_body off; @@ -429,7 +465,7 @@ proxy_ssl_name {{ auth_request_authentik_upstream_sni_name }}; proxy_set_header Host {{ auth_request_authentik_upstream_host_header }}; proxy_set_header Accept-Encoding ""; - proxy_pass {{ auth_request_authentik_upstream_scheme }}auth_request_authentik_{{ id }}; + proxy_pass {{ auth_request_authentik_upstream_scheme }}{% if auth_request_authentik_upstream_prefix == "cu_" %}{{ auth_request_authentik_upstream_host }}{% else %}auth_request_authentik_{{ id }}{% endif %}; auth_request_set $auth_cookie $upstream_http_set_cookie; add_header Set-Cookie $auth_cookie; proxy_method GET; diff --git a/frontend/src/components/Form/LocationsFields.jsx b/frontend/src/components/Form/LocationsFields.jsx index eb0886fbcf..6cc2c1950a 100644 --- a/frontend/src/components/Form/LocationsFields.jsx +++ b/frontend/src/components/Form/LocationsFields.jsx @@ -588,7 +588,7 @@ export function LocationsFields({ initialValues, name = "locations" }) { type="text" className={`form-control ${item.npmplusAuthRequestUpstream && !upstreamUrlPattern.test(item.npmplusAuthRequestUpstream) ? "is-invalid" : ""}`} placeholder="keep empty to reuse env value" - pattern="^https?://([^/:]+|\[[a-fA-F0-9:]+\]):[0-9]+$" + pattern="^https?://(([^\/:]+|\[[a-fA-F0-9:]+\]):[0-9]+|cu_[^\/:]+)$" value={item.npmplusAuthRequestUpstream} onChange={(e) => handleChange( diff --git a/frontend/src/modals/ProxyHostModal.jsx b/frontend/src/modals/ProxyHostModal.jsx index b989bee76d..253dc92202 100644 --- a/frontend/src/modals/ProxyHostModal.jsx +++ b/frontend/src/modals/ProxyHostModal.jsx @@ -612,7 +612,7 @@ const ProxyHostModal = EasyModal.create(({ id, isClone, visible, remove }) => { type="text" className={`form-control ${form.errors.npmplusAuthRequestUpstream && form.touched.npmplusAuthRequestUpstream ? "is-invalid" : ""}`} placeholder="keep empty to reuse env value" - pattern="^https?://([^/:]+|\[[a-fA-F0-9:]+\]):[0-9]+$" + pattern="^https?://(([^\/:]+|\[[a-fA-F0-9:]+\]):[0-9]+|cu_[^\/:]+)$" {...field} /> diff --git a/frontend/src/modules/Validations.jsx b/frontend/src/modules/Validations.jsx index c0c7f31db6..7ab4f23dba 100644 --- a/frontend/src/modules/Validations.jsx +++ b/frontend/src/modules/Validations.jsx @@ -3,7 +3,7 @@ import { intl } from "src/locale"; const emailPattern = /@/; const domainPattern = /^(?!.*:[0-9]+$).+$/; const ipv4Pattern = /^[0-9.]+$/; -const upstreamUrlPattern = /^https?:\/\/([^/:]+|\[[a-fA-F0-9:]+\]):[0-9]+$/; +const upstreamUrlPattern = /^https?:\/\/(([^/:]+|\[[a-fA-F0-9:]+\]):[0-9]+|cu_[^/:]+)$/; const validateString = (minLength = 0, maxLength = 0) => diff --git a/frontend/translations/ui/de.json b/frontend/translations/ui/de.json index eb9756d892..471d308323 100644 --- a/frontend/translations/ui/de.json +++ b/frontend/translations/ui/de.json @@ -136,7 +136,7 @@ "error.invalid-code": "Ungültiger Verifizierungscode", "error.invalid-domain": "Ungültige Domain: {domain}", "error.invalid-email": "Ungültige E-Mail-Adresse", - "error.invalid-upstream-url": "Muss eine gültige URL mit Schema, Host und Port sein (ohne Pfad)", + "error.invalid-upstream-url": "Muss eine gültige URL mit Schema, Host und Port oder mit Schema und cu_-Upstream-Name sein (ohne Pfad)", "error.max-character-length": "Die maximale Länge beträgt {max} Zeichen{max, plural, one {} other {s}}", "error.max-domains": "Zu viele Domains, maximal sind {max}", "error.maximum": "Maximum ist {max}", diff --git a/frontend/translations/ui/en.json b/frontend/translations/ui/en.json index 0baa92ac27..b057d21f74 100644 --- a/frontend/translations/ui/en.json +++ b/frontend/translations/ui/en.json @@ -136,7 +136,7 @@ "error.invalid-code": "Invalid verification code", "error.invalid-domain": "Invalid domain: {domain}", "error.invalid-email": "Invalid email address", - "error.invalid-upstream-url": "Must be a valid URL with scheme, host and port (no path)", + "error.invalid-upstream-url": "Must be a valid URL with scheme, host and port or with scheme and cu_ upstream name (no path)", "error.max-character-length": "Maximum length is {max} character{max, plural, one {} other {s}}", "error.max-domains": "Too many domains, max is {max}", "error.maximum": "Maximum is {max}", diff --git a/frontend/translations/ui/nl.json b/frontend/translations/ui/nl.json index 8bc0bc7dbc..bbeef73f95 100644 --- a/frontend/translations/ui/nl.json +++ b/frontend/translations/ui/nl.json @@ -136,7 +136,7 @@ "error.invalid-code": "Ongeldige verificatiecode", "error.invalid-domain": "Ongeldige domeinnaam: {domain}", "error.invalid-email": "Ongeldig e-mailadres", - "error.invalid-upstream-url": "Moet een geldige URL zijn met schema, host en poort (geen pad)", + "error.invalid-upstream-url": "Moet een geldige URL zijn met schema, host en poort of met schema en cu_-upstreamnaam (geen pad)", "error.max-character-length": "Maximale lengte is {max} teken{max, plural, one {} other {s}}", "error.max-domains": "Te veel domeinnamen, max is {max}", "error.maximum": "Maximum is {max}", diff --git a/rootfs/usr/local/bin/envs.sh b/rootfs/usr/local/bin/envs.sh index 4cfbc9e6a7..314437a12e 100755 --- a/rootfs/usr/local/bin/envs.sh +++ b/rootfs/usr/local/bin/envs.sh @@ -747,33 +747,33 @@ if ! echo "$AUTH_REQUEST_ANUBIS_USE_CUSTOM_IMAGES" | grep -q "^true$\|^false$"; sleep inf fi -if [ -n "$AUTH_REQUEST_ANUBIS_UPSTREAM" ] && ! echo "$AUTH_REQUEST_ANUBIS_UPSTREAM" | grep -q "^https\?://\([^/:]\+\|\[[a-fA-F0-9:]\+\]\):[0-9]\+$"; then - echo "AUTH_REQUEST_ANUBIS_UPSTREAM needs to contain the scheme, the target ip/domain/hostname and port but is not allowed to contain a path." +if [ -n "$AUTH_REQUEST_ANUBIS_UPSTREAM" ] && ! echo "$AUTH_REQUEST_ANUBIS_UPSTREAM" | grep -q "^https\?://\(\([^/:]\+\|\[[a-fA-F0-9:]\+\]\):[0-9]\+\|cu_[^/:]\+\)$"; then + echo "AUTH_REQUEST_ANUBIS_UPSTREAM needs to contain the scheme, the target ip/domain/hostname and port or the scheme and the name of a cu_ upstream block but is not allowed to contain a path." sleep inf fi -if [ -n "$AUTH_REQUEST_TINYAUTH_UPSTREAM" ] && ! echo "$AUTH_REQUEST_TINYAUTH_UPSTREAM" | grep -q "^https\?://\([^/:]\+\|\[[a-fA-F0-9:]\+\]\):[0-9]\+$"; then - echo "AUTH_REQUEST_TINYAUTH_UPSTREAM needs to contain the scheme, the target ip/domain/hostname and port but is not allowed to contain a path." +if [ -n "$AUTH_REQUEST_TINYAUTH_UPSTREAM" ] && ! echo "$AUTH_REQUEST_TINYAUTH_UPSTREAM" | grep -q "^https\?://\(\([^/:]\+\|\[[a-fA-F0-9:]\+\]\):[0-9]\+\|cu_[^/:]\+\)$"; then + echo "AUTH_REQUEST_TINYAUTH_UPSTREAM needs to contain the scheme, the target ip/domain/hostname and port or the scheme and the name of a cu_ upstream block but is not allowed to contain a path." sleep inf fi -if [ -n "$AUTH_REQUEST_OAUTH2PROXY_UPSTREAM" ] && ! echo "$AUTH_REQUEST_OAUTH2PROXY_UPSTREAM" | grep -q "^https\?://\([^/:]\+\|\[[a-fA-F0-9:]\+\]\):[0-9]\+$"; then - echo "AUTH_REQUEST_OAUTH2PROXY_UPSTREAM needs to contain the scheme, the target ip/domain/hostname and port but is not allowed to contain a path." +if [ -n "$AUTH_REQUEST_OAUTH2PROXY_UPSTREAM" ] && ! echo "$AUTH_REQUEST_OAUTH2PROXY_UPSTREAM" | grep -q "^https\?://\(\([^/:]\+\|\[[a-fA-F0-9:]\+\]\):[0-9]\+\|cu_[^/:]\+\)$"; then + echo "AUTH_REQUEST_OAUTH2PROXY_UPSTREAM needs to contain the scheme, the target ip/domain/hostname and port or the scheme and the name of a cu_ upstream block but is not allowed to contain a path." sleep inf fi -if [ -n "$AUTH_REQUEST_VOIDAUTH_UPSTREAM" ] && ! echo "$AUTH_REQUEST_VOIDAUTH_UPSTREAM" | grep -q "^https\?://\([^/:]\+\|\[[a-fA-F0-9:]\+\]\):[0-9]\+$"; then - echo "AUTH_REQUEST_VOIDAUTH_UPSTREAM needs to contain the scheme, the target ip/domain/hostname and port but is not allowed to contain a path." +if [ -n "$AUTH_REQUEST_VOIDAUTH_UPSTREAM" ] && ! echo "$AUTH_REQUEST_VOIDAUTH_UPSTREAM" | grep -q "^https\?://\(\([^/:]\+\|\[[a-fA-F0-9:]\+\]\):[0-9]\+\|cu_[^/:]\+\)$"; then + echo "AUTH_REQUEST_VOIDAUTH_UPSTREAM needs to contain the scheme, the target ip/domain/hostname and port or the scheme and the name of a cu_ upstream block but is not allowed to contain a path." sleep inf fi -if [ -n "$AUTH_REQUEST_AUTHELIA_UPSTREAM" ] && ! echo "$AUTH_REQUEST_AUTHELIA_UPSTREAM" | grep -q "^https\?://\([^/:]\+\|\[[a-fA-F0-9:]\+\]\):[0-9]\+$"; then - echo "AUTH_REQUEST_AUTHELIA_UPSTREAM needs to contain the scheme, the target ip/domain/hostname and port but is not allowed to contain a path." +if [ -n "$AUTH_REQUEST_AUTHELIA_UPSTREAM" ] && ! echo "$AUTH_REQUEST_AUTHELIA_UPSTREAM" | grep -q "^https\?://\(\([^/:]\+\|\[[a-fA-F0-9:]\+\]\):[0-9]\+\|cu_[^/:]\+\)$"; then + echo "AUTH_REQUEST_AUTHELIA_UPSTREAM needs to contain the scheme, the target ip/domain/hostname and port or the scheme and the name of a cu_ upstream block but is not allowed to contain a path." sleep inf fi -if [ -n "$AUTH_REQUEST_AUTHENTIK_UPSTREAM" ] && ! echo "$AUTH_REQUEST_AUTHENTIK_UPSTREAM" | grep -q "^https\?://\([^/:]\+\|\[[a-fA-F0-9:]\+\]\):[0-9]\+$"; then - echo "AUTH_REQUEST_AUTHENTIK_UPSTREAM needs to contain the scheme, the target ip/domain/hostname and port but is not allowed to contain a path." +if [ -n "$AUTH_REQUEST_AUTHENTIK_UPSTREAM" ] && ! echo "$AUTH_REQUEST_AUTHENTIK_UPSTREAM" | grep -q "^https\?://\(\([^/:]\+\|\[[a-fA-F0-9:]\+\]\):[0-9]\+\|cu_[^/:]\+\)$"; then + echo "AUTH_REQUEST_AUTHENTIK_UPSTREAM needs to contain the scheme, the target ip/domain/hostname and port or the scheme and the name of a cu_ upstream block but is not allowed to contain a path." sleep inf fi From a38ebcf984950e1eed34444b29c58cb3884a613d Mon Sep 17 00:00:00 2001 From: Zoey Date: Mon, 12 Oct 2026 15:37:29 +0200 Subject: [PATCH 37/62] skip unkown dns providers --- backend/setup.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/setup.js b/backend/setup.js index 60241c3b74..adf9a87137 100644 --- a/backend/setup.js +++ b/backend/setup.js @@ -1,4 +1,5 @@ import { writeFile } from "node:fs/promises"; +import dnsPlugins from "./certbot/dns-plugins.json" with { type: "json" }; import internalHost from "./internal/host.js"; import internalNginx from "./internal/nginx.js"; import internalProxyHost from "./internal/proxy-host.js"; @@ -92,7 +93,11 @@ const setupCertbotPlugins = async () => { for (const certificate of certificates) { if (certificate.npmplus_dns_challenge) { - if (plugins.indexOf(certificate.npmplus_dns_provider) === -1) { + if (dnsPlugins[certificate.npmplus_dns_provider]?.package_name === undefined) { + logger.warn( + `Unknown DNS provider '${certificate.npmplus_dns_provider}' for Cert #${certificate.id}, skipping Certbot plugin installation`, + ); + } else if (plugins.indexOf(certificate.npmplus_dns_provider) === -1) { plugins.push(certificate.npmplus_dns_provider); } From 8c5b4d8ffd44901096f592e125a58ab39c2c4b90 Mon Sep 17 00:00:00 2001 From: jeezat Date: Tue, 13 Oct 2026 07:57:28 +0200 Subject: [PATCH 38/62] fix(certbot): update Akamai Edge DNS plugin and credentials for v0.3.0 --- backend/certbot/dns-plugins.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/certbot/dns-plugins.json b/backend/certbot/dns-plugins.json index 4467a27afe..f22da5af7a 100644 --- a/backend/certbot/dns-plugins.json +++ b/backend/certbot/dns-plugins.json @@ -224,12 +224,12 @@ "version": "~=0.1.2" }, "edgedns": { - "credentials": "edgedns_client_secret = as3d1asd5d1a32sdfsdfs2d1asd5=\nedgedns_host = sdflskjdf-dfsdfsdf-sdfsdfsdf.luna.akamaiapis.net\nedgedns_access_token = kjdsi3-34rfsdfsdf-234234fsdfsdf\nedgedns_client_token = dkfjdf-342fsdfsd-23fsdfsdfsdf", - "dependencies": "", - "full_plugin_name": "edgedns", - "name": "Akamai Edge DNS", - "package_name": "certbot-plugin-edgedns", - "version": "~=0.1.0" + "credentials": "client_token = YOUR_CLIENT_TOKEN\naccess_token = YOUR_ACCESS_TOKEN\nclient_secret = YOUR_CLIENT_SECRET\nhost = YOUR_HOST.luna.akamaiapis.net\n# Optional: account_key = YOUR_ACCOUNT_KEY", + "dependencies": "", + "full_plugin_name": "edgedns", + "name": "Akamai Edge DNS", + "package_name": "certbot-plugin-edgedns", + "version": "==0.3.0" }, "edgeone": { "credentials": "dns_edgeone_secret_id = YOUR_TENCENTCLOUD_SECRET_ID\ndns_edgeone_secret_key = YOUR_TENCENTCLOUD_SECRET_KEY", From 82529d6402b63090592bf34ddc760e9d328c741c Mon Sep 17 00:00:00 2001 From: Alexis Loskoutoff Date: Tue, 13 Oct 2026 18:34:20 +0200 Subject: [PATCH 39/62] Require certificates:create permission to validate certificates --- backend/internal/certificate.js | 8 +++++--- backend/routes/nginx/certificates.js | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 9a14f58938..9f81b764dd 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -545,13 +545,15 @@ const internalCertificate = { /** * Validates that the certs provided are good. - * No access required here, nothing is changed or stored. + * Nothing is changed or stored, but the files go through openssl, so it needs the same access as creating one. * + * @param {Access} access * @param {Object} data * @param {Object} data.files * @returns {Promise} */ - validate: (data) => { + validate: async (access, data) => { + await access.can("certificates:create"); // Put file contents into an object const files = {}; _.map(data.files, (file, name) => { @@ -600,7 +602,7 @@ const internalCertificate = { throw new error.ValidationError("Cannot upload certificates for this type of provider"); } - const validations = await internalCertificate.validate(data); + const validations = await internalCertificate.validate(access, data); if (typeof validations.certificate === "undefined") { throw new error.ValidationError("Certificate file was not provided"); } diff --git a/backend/routes/nginx/certificates.js b/backend/routes/nginx/certificates.js index dea8094230..90f279c37f 100644 --- a/backend/routes/nginx/certificates.js +++ b/backend/routes/nginx/certificates.js @@ -161,7 +161,7 @@ router } try { - const result = await internalCertificate.validate({ + const result = await internalCertificate.validate(res.locals.access, { files: req.files, }); res.status(200).send(result); From 6acf34626214e59dfca8ac546d8c00af32111d81 Mon Sep 17 00:00:00 2001 From: Alexis Loskoutoff Date: Tue, 13 Oct 2026 18:35:50 +0200 Subject: [PATCH 40/62] Add test for unauthenticated certificate validation --- test/cypress/e2e/api/Certificates.cy.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/cypress/e2e/api/Certificates.cy.js b/test/cypress/e2e/api/Certificates.cy.js index 7d75d40cf8..80fc9f395a 100644 --- a/test/cypress/e2e/api/Certificates.cy.js +++ b/test/cypress/e2e/api/Certificates.cy.js @@ -35,6 +35,19 @@ describe('Certificates endpoints', () => { }); }); + it('Unauthenticated user cannot validate custom certificate', () => { + cy.task('backendApiPostFiles', { + path: '/api/nginx/certificates/validate', + files: { + certificate: certFile, + certificate_key: keyFile, + }, + returnOnError: true, + }).then((data) => { + expect(data.error.code).to.equal(403); + }); + }); + it('Custom certificate lifecycle', () => { // Create custom cert cy.task('backendApiPost', { From f1bd1574380f3e749dcfcd539c429bcbc6d57292 Mon Sep 17 00:00:00 2001 From: Alexis Loskoutoff Date: Tue, 13 Oct 2026 19:32:53 +0200 Subject: [PATCH 41/62] Add test for certificate validation with view-only permission --- test/cypress/e2e/api/Certificates.cy.js | 44 +++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/cypress/e2e/api/Certificates.cy.js b/test/cypress/e2e/api/Certificates.cy.js index 80fc9f395a..cc197bc365 100644 --- a/test/cypress/e2e/api/Certificates.cy.js +++ b/test/cypress/e2e/api/Certificates.cy.js @@ -48,6 +48,50 @@ describe('Certificates endpoints', () => { }); }); + it('User with certificates view permission cannot validate custom certificate', () => { + const viewer = { + email: 'viewer@example.com', + secret: 'changeme', + }; + + cy.task('backendApiPost', { + token: token, + path: '/api/users', + data: { + name: 'Certificate Viewer', + nickname: 'Viewer', + email: viewer.email, + roles: ['user'], + auth: { + type: 'password', + secret: viewer.secret, + }, + }, + }).then((user) => { + cy.task('backendApiPut', { + token: token, + path: `/api/users/${user.id}/permissions`, + data: { + certificates: 'view', + }, + }); + + cy.getToken(null, {identity: viewer.email, secret: viewer.secret}).then((viewerToken) => { + cy.task('backendApiPostFiles', { + token: viewerToken, + path: '/api/nginx/certificates/validate', + files: { + certificate: certFile, + certificate_key: keyFile, + }, + returnOnError: true, + }).then((data) => { + expect(data.error.code).to.equal(403); + }); + }); + }); + }); + it('Custom certificate lifecycle', () => { // Create custom cert cy.task('backendApiPost', { From 3211afee22e29c62484e0179d5cf1051fad909f0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 11 Oct 2026 01:01:48 +0000 Subject: [PATCH 42/62] dep updates --- backend/package.json | 6 +- backend/pnpm-lock.yaml | 97 ++++++++------- frontend/package.json | 4 +- frontend/pnpm-lock.yaml | 256 ++++++++++++++++++++-------------------- 4 files changed, 181 insertions(+), 182 deletions(-) diff --git a/backend/package.json b/backend/package.json index f8dda7f9d3..0b70350465 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,7 +7,7 @@ "main": "index.js", "type": "module", "dependencies": { - "@apidevtools/json-schema-ref-parser": "16.0.3", + "@apidevtools/json-schema-ref-parser": "16.0.5", "ajv": "8.20.0", "archiver": "8.0.0", "bcryptjs": "3.0.3", @@ -20,8 +20,8 @@ "knex": "3.3.0", "liquidjs": "10.30.0", "multer": "2.4.0", - "mysql2": "3.24.5", - "objection": "3.5.0", + "mysql2": "3.24.6", + "objection": "3.9.0", "openid-client": "6.8.8", "otplib": "13.5.0", "pg": "8.23.1", diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index f841547ddb..af51533bfd 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@apidevtools/json-schema-ref-parser': - specifier: 16.0.3 - version: 16.0.3(@types/json-schema@7.0.15) + specifier: 16.0.5 + version: 16.0.5(@types/json-schema@7.0.15) ajv: specifier: 8.20.0 version: 8.20.0 @@ -40,7 +40,7 @@ importers: version: 9.0.3 knex: specifier: 3.3.0 - version: 3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.5(@types/node@26.6.4))(pg@8.23.1) + version: 3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1) liquidjs: specifier: 10.30.0 version: 10.30.0 @@ -48,11 +48,11 @@ importers: specifier: 2.4.0 version: 2.4.0 mysql2: - specifier: 3.24.5 - version: 3.24.5(@types/node@26.6.4) + specifier: 3.24.6 + version: 3.24.6(@types/node@26.6.5) objection: - specifier: 3.5.0 - version: 3.5.0(knex@3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.5(@types/node@26.6.4))(pg@8.23.1)) + specifier: 3.9.0 + version: 3.9.0(knex@3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1)) openid-client: specifier: 6.8.8 version: 6.8.8 @@ -78,8 +78,8 @@ importers: packages: - '@apidevtools/json-schema-ref-parser@16.0.3': - resolution: {integrity: sha512-7lZp4XCTZhU/AgiGpIG+j4uJeWPdGbpQ+8wqXrzuHy+tboKsjSacjGH9/kNThRDIFhMWHF3ba+ejS8J3PeSI1Q==} + '@apidevtools/json-schema-ref-parser@16.0.5': + resolution: {integrity: sha512-z3LiEMqTP2FNu6u8bdUgBYErVuwMSZReOJy1MCEIB0BDHMoNgBtkciby32HOlcrfWwrOSzRU8YIsZ0CogYRuYQ==} engines: {node: '>=22.19.0'} peerDependencies: '@types/json-schema': ^7.0.15 @@ -185,8 +185,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@26.6.4': - resolution: {integrity: sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==} + '@types/node@26.6.5': + resolution: {integrity: sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==} abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} @@ -252,8 +252,8 @@ packages: bare-abort-controller: optional: true - bare-fs@4.8.2: - resolution: {integrity: sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==} + bare-fs@4.8.4: + resolution: {integrity: sha512-MPC2eyrpbk3A873sK4y6rk+Iv4o2enxTjHZ8zH3HO0mjrpm5XjnguY2XmfidIsDV6vnjpbMBKUAJpV0ISD2SsQ==} engines: {bare: '>=1.28.0'} peerDependencies: bare-buffer: '*' @@ -537,8 +537,8 @@ packages: resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} engines: {node: '>= 0.10'} - ip-address@10.7.3: - resolution: {integrity: sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==} + ip-address@10.7.4: + resolution: {integrity: sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -565,8 +565,8 @@ packages: jose@6.2.12: resolution: {integrity: sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==} - js-yaml@5.4.2: - resolution: {integrity: sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==} + js-yaml@5.4.3: + resolution: {integrity: sha512-yzzNtgczwgoVHNigCyKTXcQJU4Y8t/ikU4UxXW0BvrLfHTGWZhsNcOT2s/a7vn81/4KqVnNmHjaUEPlvWnNwVw==} hasBin: true json-schema-traverse@1.0.0: @@ -702,8 +702,8 @@ packages: resolution: {integrity: sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==} engines: {node: '>= 10.16.0'} - mysql2@3.24.5: - resolution: {integrity: sha512-X6Ujsr2QSkkLpkQGjxzpKRAPn9nu4axpR63ntBzquFVEvPOArgbUQ1sJjFKI7hnaYtiVZxa17Z7q18KSubW0IQ==} + mysql2@3.24.6: + resolution: {integrity: sha512-enu73KhbAua2L3dIxjc6qg6ZJ7H3pt1BImZASWNWY5xSsmJ1ZuUPULTDGXgmJbI61vb+nJAG19IZclIq19UidQ==} engines: {node: '>= 8.0'} peerDependencies: '@types/node': '>= 8' @@ -731,11 +731,11 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} - objection@3.5.0: - resolution: {integrity: sha512-uHKovRPeQX/h6u3yyBB+rWary9Q/Phr6epgiotioqTuive4PjWOyRvjrRT9GALetQCNMvcrymXy5s44AbsQLpQ==} + objection@3.9.0: + resolution: {integrity: sha512-D77MnQv43EZU0kz+AeV6GY9AXvpzwnQU9tAArv2IjVxeItS8xiOleUW8YUUbyjP8RR0sLt3XhdoK0F5QpzQLXQ==} engines: {node: '>=14.0.0'} peerDependencies: - knex: '>=2.3.0' + knex: '>=2.3.0 <4' on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} @@ -842,8 +842,8 @@ packages: readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - readable-stream@4.7.0: - resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + readable-stream@4.8.0: + resolution: {integrity: sha512-2jhTTU3Eilp/2DMT71gbcoqC35eQHasMJH0RHkYxwlxIv3+FR6Y5xAFZIwEGLxKkjZYEFlsiMNIdwUqjK2lLHQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} readdir-glob@3.0.0: @@ -862,8 +862,8 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - resolve@1.22.12: - resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + resolve@1.22.13: + resolution: {integrity: sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==} engines: {node: '>= 0.4'} hasBin: true @@ -1010,11 +1010,10 @@ packages: snapshots: - '@apidevtools/json-schema-ref-parser@16.0.3(@types/json-schema@7.0.15)': + '@apidevtools/json-schema-ref-parser@16.0.5(@types/json-schema@7.0.15)': dependencies: '@types/json-schema': 7.0.15 - js-yaml: 5.4.2 - undici: 8.11.2 + js-yaml: 5.4.3 '@apidevtools/openapi-schemas@2.1.0': {} @@ -1022,7 +1021,7 @@ snapshots: '@apidevtools/swagger-parser@13.1.0(openapi-types@12.1.3)': dependencies: - '@apidevtools/json-schema-ref-parser': 16.0.3(@types/json-schema@7.0.15) + '@apidevtools/json-schema-ref-parser': 16.0.5(@types/json-schema@7.0.15) '@apidevtools/openapi-schemas': 2.1.0 '@apidevtools/swagger-methods': 3.0.2 '@types/json-schema': 7.0.15 @@ -1101,7 +1100,7 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@26.6.4': + '@types/node@26.6.5': dependencies: undici-types: 8.9.0 @@ -1138,7 +1137,7 @@ snapshots: is-stream: 4.0.1 lazystream: 1.0.1 normalize-path: 3.0.0 - readable-stream: 4.7.0 + readable-stream: 4.8.0 readdir-glob: 3.0.0 tar-stream: 3.2.2 zip-stream: 7.0.5 @@ -1159,7 +1158,7 @@ snapshots: bare-events@2.9.2: {} - bare-fs@4.8.2: + bare-fs@4.8.4: dependencies: bare-events: 2.9.2 bare-path: 3.1.2 @@ -1249,7 +1248,7 @@ snapshots: crc32-stream: 7.0.1 is-stream: 4.0.1 normalize-path: 3.0.0 - readable-stream: 4.7.0 + readable-stream: 4.8.0 content-disposition@2.0.1: {} @@ -1273,7 +1272,7 @@ snapshots: crc32-stream@7.0.1: dependencies: crc-32: 1.2.2 - readable-stream: 4.7.0 + readable-stream: 4.8.0 dayjs@1.11.23: {} @@ -1333,7 +1332,7 @@ snapshots: dependencies: debug: 4.4.3 express: 5.3.0 - ip-address: 10.7.3 + ip-address: 10.7.4 transitivePeerDependencies: - supports-color @@ -1445,7 +1444,7 @@ snapshots: interpret@2.2.0: {} - ip-address@10.7.3: {} + ip-address@10.7.4: {} ipaddr.js@1.9.1: {} @@ -1463,7 +1462,7 @@ snapshots: jose@6.2.12: {} - js-yaml@5.4.2: + js-yaml@5.4.3: dependencies: argparse: 2.0.1 @@ -1493,7 +1492,7 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 - knex@3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.5(@types/node@26.6.4))(pg@8.23.1): + knex@3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1): dependencies: colorette: 2.0.19 commander: 10.0.1 @@ -1511,7 +1510,7 @@ snapshots: tildify: 2.0.0 optionalDependencies: better-sqlite3: 13.0.3 - mysql2: 3.24.5(@types/node@26.6.4) + mysql2: 3.24.6(@types/node@26.6.5) pg: 8.23.1 transitivePeerDependencies: - supports-color @@ -1578,9 +1577,9 @@ snapshots: busboy: 1.6.0 type-is: 1.6.18 - mysql2@3.24.5(@types/node@26.6.4): + mysql2@3.24.6(@types/node@26.6.5): dependencies: - '@types/node': 26.6.4 + '@types/node': 26.6.5 aws-ssl-profiles: 1.1.2 generate-function: 2.3.1 iconv-lite: 0.7.3 @@ -1605,12 +1604,12 @@ snapshots: object-inspect@1.13.4: {} - objection@3.5.0(knex@3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.5(@types/node@26.6.4))(pg@8.23.1)): + objection@3.9.0(knex@3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1)): dependencies: ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) db-errors: 0.2.3 - knex: 3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.5(@types/node@26.6.4))(pg@8.23.1) + knex: 3.3.0(better-sqlite3@13.0.3)(mysql2@3.24.6(@types/node@26.6.5))(pg@8.23.1) on-finished@2.4.1: dependencies: @@ -1722,7 +1721,7 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 - readable-stream@4.7.0: + readable-stream@4.8.0: dependencies: abort-controller: 3.0.0 buffer: 6.0.3 @@ -1736,13 +1735,13 @@ snapshots: rechoir@0.8.0: dependencies: - resolve: 1.22.12 + resolve: 1.22.13 require-from-string@2.0.2: {} resolve-from@5.0.0: {} - resolve@1.22.12: + resolve@1.22.13: dependencies: es-errors: 1.3.0 is-core-module: 2.17.0 @@ -1861,7 +1860,7 @@ snapshots: tar-stream@3.2.2: dependencies: b4a: 1.9.0 - bare-fs: 4.8.2 + bare-fs: 4.8.4 fast-fifo: 1.3.2 streamx: 2.28.1 transitivePeerDependencies: @@ -1917,4 +1916,4 @@ snapshots: dependencies: compress-commons: 7.0.1 normalize-path: 3.0.0 - readable-stream: 4.7.0 + readable-stream: 4.8.0 diff --git a/frontend/package.json b/frontend/package.json index bc2cd65a22..c9978c132f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,8 +9,8 @@ "@ebay/nice-modal-react": "1.2.13", "@tabler/core": "1.6.1", "@tabler/icons-react": "3.49.0", - "@tanstack/react-query": "5.104.1", - "@tanstack/react-table": "9.2.8", + "@tanstack/react-query": "5.104.2", + "@tanstack/react-table": "9.3.1", "clsx": "2.1.1", "country-flag-icons": "1.6.20", "date-fns": "4.4.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index d1396419b2..e1e310e3c8 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -18,11 +18,11 @@ importers: specifier: 3.49.0 version: 3.49.0(react@19.3.0) '@tanstack/react-query': - specifier: 5.104.1 - version: 5.104.1(react@19.3.0) + specifier: 5.104.2 + version: 5.104.2(react@19.3.0) '@tanstack/react-table': - specifier: 9.2.8 - version: 9.2.8(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + specifier: 9.3.1 + version: 9.3.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0) clsx: specifier: 2.1.1 version: 2.1.1 @@ -110,16 +110,16 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/runtime@7.29.7': - resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + '@babel/runtime@7.29.10': + resolution: {integrity: sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==} engines: {node: '>=6.9.0'} '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.8': - resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + '@babel/traverse@7.29.10': + resolution: {integrity: sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==} engines: {node: '>=6.9.0'} '@babel/types@7.29.8': @@ -254,8 +254,8 @@ packages: '@internationalized/date@3.12.4': resolution: {integrity: sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ==} - '@internationalized/number@3.6.8': - resolution: {integrity: sha512-8UmMFia46DUt+k97zKd9fKWXcWHR+k8ae3eYzILETuT2KbIvLyOfac7zesw+sJdRAAZ7Q9pM1Mk22aXp2LD0Ig==} + '@internationalized/number@3.6.9': + resolution: {integrity: sha512-GgWy2caO/+hV9sT/oCTWGHvbjmJMPT9kkXzoKRLnkEq7adghhR7UprhvY9sXrKfrp3NcDfhF6QQHXR1KfQbB8g==} '@internationalized/string@3.2.10': resolution: {integrity: sha512-PDx6//vHSpRnHfxqMqto11zQvhsaU74O3mKv2F/0eicGZcl9NLjQmGlbHz/LsJh5tLKp4A4L7ZVTzN1/MmMTvA==} @@ -277,8 +277,8 @@ packages: resolution: {integrity: sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==} engines: {node: '>=12'} - '@oxc-project/types@0.152.0': - resolution: {integrity: sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==} + '@oxc-project/types@0.153.0': + resolution: {integrity: sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==} '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -290,8 +290,8 @@ packages: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-types/shared@3.36.1': - resolution: {integrity: sha512-AzsuD9OfxTOZMMvTRhlN3oHBwOmFN7tDh27LzqmHt4+uOgPhJT7ZM7/kVs/8/o0WxayMUIk3hBmCFRHv1FUoag==} + '@react-types/shared@3.37.0': + resolution: {integrity: sha512-VNlZuifu4tHdxWPfHvoMfKsHA9kVTCGkduv5vKVTdinm8wHFJNhyVB5RzIMiRFj0q3RKoCbP68u2zaoBSiFueg==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 @@ -314,98 +314,98 @@ packages: react: '>=16.14.0' react-dom: '>=16.14.0' - '@rolldown/binding-android-arm-eabi@1.2.12': - resolution: {integrity: sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==} + '@rolldown/binding-android-arm-eabi@1.2.13': + resolution: {integrity: sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@rolldown/binding-android-arm64@1.2.12': - resolution: {integrity: sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==} + '@rolldown/binding-android-arm64@1.2.13': + resolution: {integrity: sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.2.12': - resolution: {integrity: sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==} + '@rolldown/binding-darwin-arm64@1.2.13': + resolution: {integrity: sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.12': - resolution: {integrity: sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==} + '@rolldown/binding-darwin-x64@1.2.13': + resolution: {integrity: sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.2.12': - resolution: {integrity: sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==} + '@rolldown/binding-freebsd-x64@1.2.13': + resolution: {integrity: sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.2.12': - resolution: {integrity: sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.13': + resolution: {integrity: sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.2.12': - resolution: {integrity: sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==} + '@rolldown/binding-linux-arm64-gnu@1.2.13': + resolution: {integrity: sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.2.12': - resolution: {integrity: sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==} + '@rolldown/binding-linux-arm64-musl@1.2.13': + resolution: {integrity: sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.2.12': - resolution: {integrity: sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==} + '@rolldown/binding-linux-ppc64-gnu@1.2.13': + resolution: {integrity: sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.12': - resolution: {integrity: sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==} + '@rolldown/binding-linux-s390x-gnu@1.2.13': + resolution: {integrity: sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.12': - resolution: {integrity: sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==} + '@rolldown/binding-linux-x64-gnu@1.2.13': + resolution: {integrity: sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.2.12': - resolution: {integrity: sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==} + '@rolldown/binding-linux-x64-musl@1.2.13': + resolution: {integrity: sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.2.12': - resolution: {integrity: sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==} + '@rolldown/binding-openharmony-arm64@1.2.13': + resolution: {integrity: sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-win32-arm64-msvc@1.2.12': - resolution: {integrity: sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==} + '@rolldown/binding-win32-arm64-msvc@1.2.13': + resolution: {integrity: sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.12': - resolution: {integrity: sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==} + '@rolldown/binding-win32-x64-msvc@1.2.13': + resolution: {integrity: sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -433,11 +433,11 @@ packages: '@tabler/icons@3.49.0': resolution: {integrity: sha512-l+GnSkFETx0U26icxv1VUFnxxiTqXfCcVRiRAnyMwibupM2S2L/hSaZ9gmSgz01rkrsH/93NvLsxykqdanc6lw==} - '@tanstack/query-core@5.104.1': - resolution: {integrity: sha512-TiRghcrGkUTE+M4JBxvpPSpBhxvIzQairw+cksrHYGr+2uwC77yW1SY9nYxnYRYOppvucf8rEg6b5byoz4rTpw==} + '@tanstack/query-core@5.104.2': + resolution: {integrity: sha512-/qbsxJK5c3Ve1aFIULWYNcT2jeRUAiD4HME6Eu2ZdpofHoVgAjpkQhLg5xHuOoBkTPQUJLfP7zOxy0qsBcqROA==} - '@tanstack/react-query@5.104.1': - resolution: {integrity: sha512-Zz70EgjahNI7aCz8BZxM3vUGU44ycxEAzMuCUjLeA7SVhhOkg3ZgPjPJPvHjs6Rmky/dtmnu1WzjGHNHmhCoZQ==} + '@tanstack/react-query@5.104.2': + resolution: {integrity: sha512-xKmzWGhadbE5wGS/ZmXShRITnI9oItox5GO14KvFZco3+vhTAyXS1erYzJWdiY280zapAGJXFUL9nHgwP6SV/g==} peerDependencies: react: ^18 || ^19 @@ -447,8 +447,8 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/react-table@9.2.8': - resolution: {integrity: sha512-cJxgratJm3uMm2sXnmY3PCTvcvIEFleCJ2uSOM2nxCmIY8RWA1KjXt5fsWGlYisL7DODr2w9YpLRQ3i/7MdIsQ==} + '@tanstack/react-table@9.3.1': + resolution: {integrity: sha512-E6Ya5TnY6EaGyZYIUqig/29Lr/gJSAubQkLJFqiWkUl5nRCr0Xr9hvJot+NiRTtG+TzGxHTiqUW3bnLtQxlHug==} engines: {node: '>=20'} peerDependencies: react: '>=18' @@ -456,8 +456,8 @@ packages: '@tanstack/store@0.11.2': resolution: {integrity: sha512-sJ4mjol8uQsHV0gOJzzjwXfh2Fwm+Sz0+8deqiTm4jGbMdjzNSW+xZCFm0kUa870uhd8yi+DpKnZb5Kc4apa0Q==} - '@tanstack/table-core@9.2.8': - resolution: {integrity: sha512-M4D6fY5jnc7z7TaoD3Pjvz5R+8MbhzCGYdkcFf2T1AgjigwLinvFWZh6mtqfN016A7yGIroOL0GiTlVGsTh6Vg==} + '@tanstack/table-core@9.3.1': + resolution: {integrity: sha512-zQvlxSNi2qGM3L4jh85Z6hqMfdIR5qOfD9KHOU32nuKFJGN0RlJ7UvV1lsPb1fPVhpFYI0VP3jJGxLdLDpTKjQ==} engines: {node: '>=20'} '@types/hoist-non-react-statics@3.3.7': @@ -759,8 +759,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.19: - resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -817,8 +817,8 @@ packages: raf@3.4.1: resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} - react-aria@3.52.1: - resolution: {integrity: sha512-fdZZruC9/x/joCg0mhKGs5aHpwrXLCSZ4GOJmhhYyiE0ffyEsk9MLFt9LCOCF9tn8ErTD+UDQP4oDUSlZkmpCg==} + react-aria@3.53.1: + resolution: {integrity: sha512-dikisyOjJd3D2fFrIDWmVWzqiLz5B86GzZv1zxs7xI4dVc0JIz/1Wa52I20gYvVL2WXeFXlygYsle/QcOMhtcg==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 @@ -869,8 +869,8 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-stately@3.50.0: - resolution: {integrity: sha512-TnckvpDQGU0672wEaLZqdzvhuSeXWjgW6vijMWxiKOxc8CosQUfPGISdcZyfHqjX3XMjEtRxj4w9HumvX4h4mw==} + react-stately@3.51.0: + resolution: {integrity: sha512-YJcmkAXu2XOQP97hO/qz/cC3qv9kni3oVhwBlxWr358BHePJUpY15XwL3YSRMKxYt4EkslPqAz11JRirMfANGw==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 @@ -894,13 +894,13 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - resolve@1.22.12: - resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + resolve@1.22.13: + resolution: {integrity: sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==} engines: {node: '>= 0.4'} hasBin: true - rolldown@1.2.12: - resolution: {integrity: sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==} + rolldown@1.2.13: + resolution: {integrity: sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -1037,7 +1037,7 @@ snapshots: '@babel/helper-module-imports@7.29.7': dependencies: - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.10 '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -1050,7 +1050,7 @@ snapshots: dependencies: '@babel/types': 7.29.8 - '@babel/runtime@7.29.7': {} + '@babel/runtime@7.29.10': {} '@babel/template@7.29.7': dependencies: @@ -1058,7 +1058,7 @@ snapshots: '@babel/parser': 7.29.9 '@babel/types': 7.29.8 - '@babel/traverse@7.29.8': + '@babel/traverse@7.29.10': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.8 @@ -1118,7 +1118,7 @@ snapshots: '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.29.7 - '@babel/runtime': 7.29.7 + '@babel/runtime': 7.29.10 '@emotion/hash': 0.9.2 '@emotion/memoize': 0.9.0 '@emotion/serialize': 1.3.3 @@ -1145,7 +1145,7 @@ snapshots: '@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)': dependencies: - '@babel/runtime': 7.29.7 + '@babel/runtime': 7.29.10 '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 @@ -1208,7 +1208,7 @@ snapshots: dependencies: '@swc/helpers': 0.5.23 - '@internationalized/number@3.6.8': + '@internationalized/number@3.6.9': dependencies: '@swc/helpers': 0.5.23 @@ -1235,7 +1235,7 @@ snapshots: jsbi: 4.3.2 optional: true - '@oxc-project/types@0.152.0': {} + '@oxc-project/types@0.153.0': {} '@popperjs/core@2.11.8': {} @@ -1243,10 +1243,10 @@ snapshots: dependencies: '@swc/helpers': 0.5.23 react: 19.3.0 - react-aria: 3.52.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + react-aria: 3.53.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0) react-dom: 19.3.0(react@19.3.0) - '@react-types/shared@3.36.1(react@19.3.0)': + '@react-types/shared@3.37.0(react@19.3.0)': dependencies: react: 19.3.0 @@ -1264,7 +1264,7 @@ snapshots: '@restart/ui@1.9.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': dependencies: - '@babel/runtime': 7.29.7 + '@babel/runtime': 7.29.10 '@popperjs/core': 2.11.8 '@react-aria/ssr': 3.10.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@restart/hooks': 0.5.1(react@19.3.0) @@ -1276,49 +1276,49 @@ snapshots: uncontrollable: 8.0.4(react@19.3.0) warning: 4.0.3 - '@rolldown/binding-android-arm-eabi@1.2.12': + '@rolldown/binding-android-arm-eabi@1.2.13': optional: true - '@rolldown/binding-android-arm64@1.2.12': + '@rolldown/binding-android-arm64@1.2.13': optional: true - '@rolldown/binding-darwin-arm64@1.2.12': + '@rolldown/binding-darwin-arm64@1.2.13': optional: true - '@rolldown/binding-darwin-x64@1.2.12': + '@rolldown/binding-darwin-x64@1.2.13': optional: true - '@rolldown/binding-freebsd-x64@1.2.12': + '@rolldown/binding-freebsd-x64@1.2.13': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.12': + '@rolldown/binding-linux-arm-gnueabihf@1.2.13': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.12': + '@rolldown/binding-linux-arm64-gnu@1.2.13': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.12': + '@rolldown/binding-linux-arm64-musl@1.2.13': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.12': + '@rolldown/binding-linux-ppc64-gnu@1.2.13': optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.12': + '@rolldown/binding-linux-s390x-gnu@1.2.13': optional: true - '@rolldown/binding-linux-x64-gnu@1.2.12': + '@rolldown/binding-linux-x64-gnu@1.2.13': optional: true - '@rolldown/binding-linux-x64-musl@1.2.12': + '@rolldown/binding-linux-x64-musl@1.2.13': optional: true - '@rolldown/binding-openharmony-arm64@1.2.12': + '@rolldown/binding-openharmony-arm64@1.2.13': optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.12': + '@rolldown/binding-win32-arm64-msvc@1.2.13': optional: true - '@rolldown/binding-win32-x64-msvc@1.2.12': + '@rolldown/binding-win32-x64-msvc@1.2.13': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -1338,11 +1338,11 @@ snapshots: '@tabler/icons@3.49.0': {} - '@tanstack/query-core@5.104.1': {} + '@tanstack/query-core@5.104.2': {} - '@tanstack/react-query@5.104.1(react@19.3.0)': + '@tanstack/react-query@5.104.2(react@19.3.0)': dependencies: - '@tanstack/query-core': 5.104.1 + '@tanstack/query-core': 5.104.2 react: 19.3.0 '@tanstack/react-store@0.11.2(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': @@ -1352,17 +1352,17 @@ snapshots: react-dom: 19.3.0(react@19.3.0) use-sync-external-store: 1.7.0(react@19.3.0) - '@tanstack/react-table@9.2.8(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + '@tanstack/react-table@9.3.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': dependencies: '@tanstack/react-store': 0.11.2(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - '@tanstack/table-core': 9.2.8 + '@tanstack/table-core': 9.3.1 react: 19.3.0 transitivePeerDependencies: - react-dom '@tanstack/store@0.11.2': {} - '@tanstack/table-core@9.2.8': + '@tanstack/table-core@9.3.1': dependencies: '@tanstack/store': 0.11.2 @@ -1396,9 +1396,9 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.29.7 + '@babel/runtime': 7.29.10 cosmiconfig: 7.1.0 - resolve: 1.22.12 + resolve: 1.22.13 callsites@3.1.0: {} @@ -1438,7 +1438,7 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.29.7 + '@babel/runtime': 7.29.10 csstype: 3.2.3 error-ex@1.3.4: @@ -1584,7 +1584,7 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.19: {} + nanoid@3.3.20: {} object-assign@4.1.1: {} @@ -1611,7 +1611,7 @@ snapshots: postcss@8.5.29: dependencies: - nanoid: 3.3.19 + nanoid: 3.3.20 picocolors: 1.1.1 source-map-js: 1.2.2 @@ -1641,23 +1641,23 @@ snapshots: dependencies: performance-now: 2.1.0 - react-aria@3.52.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + react-aria@3.53.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: '@internationalized/date': 3.12.4 - '@internationalized/number': 3.6.8 + '@internationalized/number': 3.6.9 '@internationalized/string': 3.2.10 - '@react-types/shared': 3.36.1(react@19.3.0) + '@react-types/shared': 3.37.0(react@19.3.0) '@swc/helpers': 0.5.23 aria-hidden: 1.2.6 clsx: 2.1.1 react: 19.3.0 react-dom: 19.3.0(react@19.3.0) - react-stately: 3.50.0(react@19.3.0) + react-stately: 3.51.0(react@19.3.0) use-sync-external-store: 1.7.0(react@19.3.0) react-bootstrap@2.10.10(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: - '@babel/runtime': 7.29.7 + '@babel/runtime': 7.29.10 '@restart/hooks': 0.4.16(react@19.3.0) '@restart/ui': 1.9.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@types/prop-types': 15.7.15 @@ -1704,7 +1704,7 @@ snapshots: react-select@5.10.2(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: - '@babel/runtime': 7.29.7 + '@babel/runtime': 7.29.10 '@emotion/cache': 11.14.0 '@emotion/react': 11.14.0(@types/react@19.3.0)(react@19.3.0) '@floating-ui/dom': 1.8.0 @@ -1719,12 +1719,12 @@ snapshots: - '@types/react' - supports-color - react-stately@3.50.0(react@19.3.0): + react-stately@3.51.0(react@19.3.0): dependencies: '@internationalized/date': 3.12.4 - '@internationalized/number': 3.6.8 + '@internationalized/number': 3.6.9 '@internationalized/string': 3.2.10 - '@react-types/shared': 3.36.1(react@19.3.0) + '@react-types/shared': 3.37.0(react@19.3.0) '@swc/helpers': 0.5.23 react: 19.3.0 use-sync-external-store: 1.7.0(react@19.3.0) @@ -1737,7 +1737,7 @@ snapshots: react-transition-group@4.4.5(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: - '@babel/runtime': 7.29.7 + '@babel/runtime': 7.29.10 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -1748,33 +1748,33 @@ snapshots: resolve-from@4.0.0: {} - resolve@1.22.12: + resolve@1.22.13: dependencies: es-errors: 1.3.0 is-core-module: 2.17.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rolldown@1.2.12: + rolldown@1.2.13: dependencies: - '@oxc-project/types': 0.152.0 + '@oxc-project/types': 0.153.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm-eabi': 1.2.12 - '@rolldown/binding-android-arm64': 1.2.12 - '@rolldown/binding-darwin-arm64': 1.2.12 - '@rolldown/binding-darwin-x64': 1.2.12 - '@rolldown/binding-freebsd-x64': 1.2.12 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.12 - '@rolldown/binding-linux-arm64-gnu': 1.2.12 - '@rolldown/binding-linux-arm64-musl': 1.2.12 - '@rolldown/binding-linux-ppc64-gnu': 1.2.12 - '@rolldown/binding-linux-s390x-gnu': 1.2.12 - '@rolldown/binding-linux-x64-gnu': 1.2.12 - '@rolldown/binding-linux-x64-musl': 1.2.12 - '@rolldown/binding-openharmony-arm64': 1.2.12 - '@rolldown/binding-win32-arm64-msvc': 1.2.12 - '@rolldown/binding-win32-x64-msvc': 1.2.12 + '@rolldown/binding-android-arm-eabi': 1.2.13 + '@rolldown/binding-android-arm64': 1.2.13 + '@rolldown/binding-darwin-arm64': 1.2.13 + '@rolldown/binding-darwin-x64': 1.2.13 + '@rolldown/binding-freebsd-x64': 1.2.13 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.13 + '@rolldown/binding-linux-arm64-gnu': 1.2.13 + '@rolldown/binding-linux-arm64-musl': 1.2.13 + '@rolldown/binding-linux-ppc64-gnu': 1.2.13 + '@rolldown/binding-linux-s390x-gnu': 1.2.13 + '@rolldown/binding-linux-x64-gnu': 1.2.13 + '@rolldown/binding-linux-x64-musl': 1.2.13 + '@rolldown/binding-openharmony-arm64': 1.2.13 + '@rolldown/binding-win32-arm64-msvc': 1.2.13 + '@rolldown/binding-win32-x64-msvc': 1.2.13 rooks@9.9.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: @@ -1810,7 +1810,7 @@ snapshots: uncontrollable@7.2.1(react@19.3.0): dependencies: - '@babel/runtime': 7.29.7 + '@babel/runtime': 7.29.10 '@types/react': 19.3.0 invariant: 2.2.4 react: 19.3.0 @@ -1835,7 +1835,7 @@ snapshots: lightningcss: 1.33.0 picomatch: 4.0.7 postcss: 8.5.29 - rolldown: 1.2.12 + rolldown: 1.2.13 tinyglobby: 0.2.17 optionalDependencies: fsevents: 2.3.3 From 7cb0e3cdc40685ba5d65d3d7a20a3e9837c9c75e Mon Sep 17 00:00:00 2001 From: Zoey Date: Mon, 12 Oct 2026 10:15:56 +0200 Subject: [PATCH 43/62] many small fixes --- backend/internal/access-list.js | 13 ++++++-- .../20261011120000_pass_auth_default.js | 33 +++++++++++++++++++ .../schema/components/proxy-host-object.json | 1 + frontend/src/components/Form/AccessFields.jsx | 29 +++++++++++----- .../src/components/Form/BasicAuthFields.jsx | 5 +-- .../src/components/Form/LocationsFields.jsx | 19 +++++------ frontend/src/hooks/useAccessList.js | 10 +++--- frontend/src/modals/AccessListModal.jsx | 8 ++--- frontend/src/modals/ProxyHostModal.jsx | 21 +++++++++--- frontend/src/modules/Forward.js | 9 +++++ frontend/src/pages/Access/TableWrapper.jsx | 2 +- frontend/src/pages/AuditLog/TableWrapper.jsx | 2 +- .../src/pages/Certificates/TableWrapper.jsx | 4 +-- .../pages/Nginx/DeadHosts/TableWrapper.jsx | 20 ++++++----- frontend/src/pages/Nginx/ProxyHosts/Table.jsx | 17 +++++----- .../pages/Nginx/ProxyHosts/TableWrapper.jsx | 27 ++++++++------- .../Nginx/RedirectionHosts/TableWrapper.jsx | 20 ++++++----- .../src/pages/Nginx/Streams/TableWrapper.jsx | 26 +++++++++------ frontend/src/pages/Users/TableWrapper.jsx | 20 ++++++----- 19 files changed, 190 insertions(+), 96 deletions(-) create mode 100644 backend/migrations/20261011120000_pass_auth_default.js create mode 100644 frontend/src/modules/Forward.js diff --git a/backend/internal/access-list.js b/backend/internal/access-list.js index cd22826936..423f1b84e9 100644 --- a/backend/internal/access-list.js +++ b/backend/internal/access-list.js @@ -10,6 +10,9 @@ import internalAuditLog from "./audit-log.js"; import internalNginx from "./nginx.js"; import internalProxyHostAccessList from "./proxy-host-access-list.js"; +const hasNoRules = (items = [], clients = []) => + items.length === 0 && clients.every(({ address = "" }) => ["", "all"].includes(address.trim().toLowerCase())); + const internalAccessList = { /** * @param {Access} access @@ -21,6 +24,9 @@ const internalAccessList = { if (data.items?.some((item) => !item.password)) { throw new errs.ValidationError("New access list users need a password"); } + if (hasNoRules(data.items, data.clients)) { + throw new errs.ValidationError("Access list needs at least one user or one rule that is not all"); + } const row = await accessListModel.transaction(async (trx) => { const insertedRow = await accessListModel.query(trx).insertAndFetch({ @@ -88,7 +94,7 @@ const internalAccessList = { */ update: async (access, data) => { access.can("access_lists:manage"); - const row = await internalAccessList.get(access, { id: data.id, expand: ["items"] }); + const row = await internalAccessList.get(access, { id: data.id, expand: ["items", "clients"] }); if (row.id !== data.id) { // Sanity check that something crazy hasn't happened throw new errs.InternalValidationError( @@ -100,6 +106,9 @@ const internalAccessList = { ) { throw new errs.ValidationError("New access list users need a password"); } + if (hasNoRules(data.items ?? row.items, data.clients ?? row.clients)) { + throw new errs.ValidationError("Access list needs at least one user or one rule that is not all"); + } await accessListModel.transaction(async (trx) => { // patch name if specified @@ -169,7 +178,7 @@ const internalAccessList = { try { await internalAccessList.build(freshRow); - if (Number.parseInt(freshRow.proxy_host_count, 10)) { + if (freshRow.proxy_host_count) { // locations don't have accessList objects, only IDs, so populate it with the object itself freshRow.proxy_hosts = await Promise.all( (freshRow.proxy_hosts || []).map((host) => { diff --git a/backend/migrations/20261011120000_pass_auth_default.js b/backend/migrations/20261011120000_pass_auth_default.js new file mode 100644 index 0000000000..194220445f --- /dev/null +++ b/backend/migrations/20261011120000_pass_auth_default.js @@ -0,0 +1,33 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "pass_auth_default"; + +/** + * Migrate + * + * @see https://knexjs.org/guide/migrations.html#migration-api + * + * @param {Object} knex + * @returns {Promise} + */ +const up = async (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + await knex.schema.table("access_list", (access_list) => { + access_list.integer("pass_auth").notNull().defaultTo(0).alter(); + }); + + logger.info(`[${migrateName}] access_list Table altered`); +}; + +/** + * Undo Migrate + * + * @param {Object} _knex + * @returns {Promise} + */ +const down = (_knex) => { + throw new Error(`[${migrateName}] You can't migrate down this one.`); +}; + +export { down, up }; diff --git a/backend/schema/components/proxy-host-object.json b/backend/schema/components/proxy-host-object.json index e9ad74898a..076cf54772 100644 --- a/backend/schema/components/proxy-host-object.json +++ b/backend/schema/components/proxy-host-object.json @@ -200,6 +200,7 @@ "path": { "type": "string", "minLength": 1, + "maxLength": 255, "pattern": "^\\S+$" }, "location_type": { diff --git a/frontend/src/components/Form/AccessFields.jsx b/frontend/src/components/Form/AccessFields.jsx index 042d47916e..1226773d02 100644 --- a/frontend/src/components/Form/AccessFields.jsx +++ b/frontend/src/components/Form/AccessFields.jsx @@ -105,8 +105,11 @@ export function AccessFields({ const applyUpdatedValues = (newValues) => { setValues(newValues); - void setFieldValue(name, newValues); - onChange?.({ npmplusAccessListIds: newValues }); + if (onChange) { + onChange({ npmplusAccessListIds: newValues }); + } else { + void setFieldValue(name, newValues); + } }; const onAccessListChange = (acl, idx) => { @@ -159,7 +162,11 @@ export function AccessFields({ return (
{isLoading ?
: null} - {isError ?
{`${error}`}
: null} + {isError ? ( +
+ +
+ ) : null}
@@ -181,8 +188,11 @@ export function AccessFields({ onChange={(e) => { const value = e.type; setAclValue(value); - void setFieldValue(typeFieldName, value); - onChange?.({ npmplusAccessListType: value }); + if (onChange) { + onChange({ npmplusAccessListType: value }); + } else { + void setFieldValue(typeFieldName, value); + } }} />
@@ -196,7 +206,9 @@ export function AccessFields({ className="col-md-8 mb-1" unstyled classNames={selectClassNames} - value={defaultOptions.find((o) => o.value === item) ?? null} + value={ + defaultOptions.find((o) => o.value === item) ?? { value: item, label: `#${item}` } + } options={options} components={{ Option }} styles={{ @@ -246,7 +258,7 @@ export function AccessFields({
))} - {values.length < defaultOptions.length && ( // only show add button if there are more acls that can be added + {options.length > 0 && ( // only show add button if there are more acls that can be added
diff --git a/frontend/src/components/Form/LocationsFields.jsx b/frontend/src/components/Form/LocationsFields.jsx index 6cc2c1950a..aab66cb7ed 100644 --- a/frontend/src/components/Form/LocationsFields.jsx +++ b/frontend/src/components/Form/LocationsFields.jsx @@ -14,6 +14,7 @@ import { useRef, useState } from "react"; import { flushSync } from "react-dom"; import { AccessFields } from "src/components"; import { intl, T } from "src/locale"; +import { forwardSummary } from "src/modules/Forward"; import { upstreamUrlPattern } from "src/modules/Validations"; export function LocationsFields({ initialValues, name = "locations" }) { @@ -78,6 +79,9 @@ export function LocationsFields({ initialValues, name = "locations" }) { if (field === "npmplusProxyRequestBuffering" && fieldValue === true) { updatedLocation.npmplusCrowdsecAppsec = true; } + if (field === "npmplusAuthRequest" && fieldValue === "none") { + updatedLocation.npmplusAuthRequestUpstream = ""; + } if (field === "forwardScheme" && fieldValue !== "empty") { if (!["http", "https"].includes(fieldValue)) { updatedLocation.npmplusProxyRequestBuffering = false; @@ -115,14 +119,6 @@ export function LocationsFields({ initialValues, name = "locations" }) { const locationLabel = (item) => `${item.locationType}${item.path}`; - const forwardSummary = ({ forwardScheme, forwardHost, forwardPort }) => { - if (!forwardHost || forwardScheme === "empty") return ""; - if (forwardScheme !== "path") { - return `${forwardScheme}://${forwardHost}${forwardPort ? `:${forwardPort}` : ""}`; - } - return forwardHost; - }; - const matchesFilter = (item) => `${locationLabel(item)} ${forwardSummary(item)}`.toLowerCase().includes(filter.trim().toLowerCase()); @@ -273,6 +269,8 @@ export function LocationsFields({ initialValues, name = "locations" }) { className="form-control" placeholder="/path" autoComplete="off" + required + pattern="\S{1,255}" value={item.path} onChange={(e) => handleChange(idx, "path", e.target.value)} /> @@ -327,6 +325,7 @@ export function LocationsFields({ initialValues, name = "locations" }) { type="text" className="form-control" required={item.forwardScheme !== "empty"} + pattern="\S{0,255}" placeholder="eg: 10.0.0.1/path/" value={item.forwardHost} onChange={(e) => handleChange(idx, "forwardHost", e.target.value)} @@ -345,7 +344,7 @@ export function LocationsFields({ initialValues, name = "locations" }) { max={65535} className="form-control" placeholder="eg: 8081" - value={item.forwardPort} + value={item.forwardPort ?? ""} onChange={(e) => handleChange(idx, "forwardPort", e.target.value)} />
@@ -619,8 +618,6 @@ export function LocationsFields({ initialValues, name = "locations" }) { initialAccessListType={item?.npmplusAccessListType} location={item.path} initialAccessListIds={item?.npmplusAccessListIds} - name={`locations[${idx}].npmplusAccessListIds`} - typeFieldName={`locations[${idx}].npmplusAccessListType`} onChange={(changes) => handleAccessFieldsChange(idx, changes)} authRequest={item.npmplusAuthRequest} /> diff --git a/frontend/src/hooks/useAccessList.js b/frontend/src/hooks/useAccessList.js index b808de8102..cc73d709c6 100644 --- a/frontend/src/hooks/useAccessList.js +++ b/frontend/src/hooks/useAccessList.js @@ -1,7 +1,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { createAccessList, getAccessList, updateAccessList } from "src/api/backend"; -const fetchAccessList = (id, expand) => { +const fetchAccessList = (id) => { if (id === "new") { return Promise.resolve({ id: 0, @@ -15,13 +15,13 @@ const fetchAccessList = (id, expand) => { clients: [], }); } - return getAccessList(id, expand); + return getAccessList(id, ["items", "clients"]); }; -const useAccessList = (id, expand) => { +const useAccessList = (id) => { return useQuery({ - queryKey: ["access-list", id, expand], - queryFn: () => fetchAccessList(id, expand), + queryKey: ["access-list", id], + queryFn: () => fetchAccessList(id), staleTime: 60 * 1000, // 1 minute }); }; diff --git a/frontend/src/modals/AccessListModal.jsx b/frontend/src/modals/AccessListModal.jsx index 391f51f9b3..45252a8ba2 100644 --- a/frontend/src/modals/AccessListModal.jsx +++ b/frontend/src/modals/AccessListModal.jsx @@ -15,14 +15,14 @@ const showAccessListModal = (id) => { }; const AccessListModal = EasyModal.create(({ id, visible, remove }) => { - const { data, isLoading, error } = useAccessList(id, ["items", "clients"]); + const { data, isPending, error } = useAccessList(id); const { mutate: setAccessList } = useSetAccessList(); const [errorMsg, setErrorMsg] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const validate = (values) => { // either Auths or Clients must be defined - if (values.items.length === 0 && values.clients.length === 0) { + if (values.items.length === 0 && values.clients.every((c) => c.address.trim().toLowerCase() === "all")) { return intl.formatMessage({ id: "error.access.at-least-one" }); } @@ -88,7 +88,7 @@ const AccessListModal = EasyModal.create(({ id, visible, remove }) => { {error?.message} )} - {isLoading && } + {isPending && } {data && ( { type="text" required autoComplete="off" - className="form-control" + className={`form-control ${form.errors.name && form.touched.name ? "is-invalid" : ""}`} {...field} /> diff --git a/frontend/src/modals/ProxyHostModal.jsx b/frontend/src/modals/ProxyHostModal.jsx index 253dc92202..465d9d4b92 100644 --- a/frontend/src/modals/ProxyHostModal.jsx +++ b/frontend/src/modals/ProxyHostModal.jsx @@ -24,7 +24,7 @@ import { showTabOfInvalid, validateNumber, validateUpstreamUrl } from "src/modul import { showObjectSuccess } from "src/notifications"; const ProxyHostModal = EasyModal.create(({ id, isClone, visible, remove }) => { - const { data, isLoading, error } = useProxyHost(id); + const { data, isPending, error } = useProxyHost(id); const { data: allProxyHosts } = useProxyHosts(); const suggestions = useDirectorySuggestions(allProxyHosts); const { mutate: setProxyHost } = useSetProxyHost(); @@ -83,7 +83,7 @@ const ProxyHostModal = EasyModal.create(({ id, isClone, visible, remove }) => { {error?.message} )} - {isLoading && } + {isPending && } {data && ( { domainNames: data?.domainNames, forwardScheme: data?.forwardScheme, forwardHost: data?.forwardHost, - forwardPort: data?.forwardPort || undefined, + forwardPort: data?.forwardPort || "", npmplusAccessListIds: data?.npmplusAccessListIds, npmplusAccessListType: data?.npmplusAccessListType, // Locations tab @@ -250,7 +250,7 @@ const ProxyHostModal = EasyModal.create(({ id, isClone, visible, remove }) => {
- {({ field }) => ( + {({ field, form }) => (
@@ -558,12 +560,21 @@ const ProxyHostModal = EasyModal.create(({ id, isClone, visible, remove }) => { - {({ field }) => ( + {({ field, form }) => (