diff --git a/.version b/.version index 3b1fc7950f..7524906967 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.15.1 +2.16.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index fa9d17e4be..d317d14307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to the NPMplus Security Fork are documented here. The fork u ## Unreleased +- Reconciled upstream develop through `a4a29e09`, closing the 71-commit gap since `03688155` (regular merge commit, ancestry preserved). Upstream moved the nginx online/err, directory, mTLS, and DNS-challenge state out of row `meta` into `npmplus_*` columns (`meta_to_columns`, which also wipes `meta`), and its new global `jsonReplacer` — wired through `app.set("json replacer", …)` — blanks `password` and drops `certificate`, `certificate_key`, and the dns-provider credentials from every API response, superseding the fork's per-site meta masking for those fields. The fork's forward-destination reachability probe survives as the only remaining meta writer (`reach_ok`/`reach_err`, patched after `configure` persists its own online/err columns), and `proxy_host`'s `$parseDatabaseJson` keeps `meta` readable so the UI reach badge still works; `Table.jsx` sorts enabled → offline → unreachable → online and reads `npmplus_nginx_online` with the meta reach state. The access-list row masking (`maskItems`/`maskAccessListItems`, lodash `_.omit` plus blanked item passwords) is kept — upstream's recursive replacer may now cover part of it, and per FORK.md's replacement rule that redundancy sweep happens in a normal reviewable commit, not in the merge. The merged permission model takes upstream's sync `canAdmin()`/`canUser(id)` (throwing) and async `can()` with the crowdsec routes gating through a local `requireAdmin`; host CRUD takes upstream's `savedRow`/`try`/`finally` shape with the fork's `assertPrivilegedNginxFields`, `validateIncomingPort`, mTLS, and SSL/HSTS cleanup intact; `user.js` takes upstream's email/avatar handling with the fork's bounded gravatar fetch. `lodash` returns to the backend manifest for the masking functions. Dependency aging held: `@tabler/core` 1.6.0 and `vite` 8.3.1 failed the `minimumReleaseAge` policy and stay at 1.5.1/8.3.0, with both lockfiles regenerated under the enforced window. Adopted upstream's ECH key rotation (`/opt/npmplus/tls/ech/cron.sh`, `ECH_ROTATION_INTERVAL`, the cloudflare example script); the README section keeps to the release-discipline word budget and the full guide moved to `docs/ech.md`. Kept the fork's caddy source build with its CVE pin matrix, and the goaccess CSP/no-cache headers in `npmplus.conf`; upstream's goaccess dinit flag reorder landed. Test fixtures and the live smokes track the new columns (`certificate-dns.test.js`, the `sqlite-upgrade.test.js` raw-knex legacy seed, `security-regressions.mjs` and `rc5-features.mjs` reading `npmplus_nginx_online` plus `meta.reach_ok`). Verified by 163 backend and 15 frontend tests, `validate-schema`, biome on both LF-normalized trees, the vite build, `tests/security-invariants.mjs`, frozen lockfiles, the 45-check in-process backend smoke, and the full disposable-container smoke (socket-injection regressions, MFA replay, browser-driven first-admin setup and UI flows); the Linux-only python contracts reproduce identically on a pristine fork-HEAD worktree and stay green on CI. + - Third same-day upstream push (`03688155`–`a2b765f8`, the locale simplification pass): adopted the reload-based `changeLocale`, module-level `document.dir`/`lang` application, and `formatDateTime` reading the active locale internally — the fork's CrowdSec views dropped the now-deleted `LocaleContext`/`useLocaleState` plumbing along with the locale argument, while keeping their `crowdsec-decision` audit rendering (gavel icon, `Decision #`, i18n'd unknown-type fallback) that upstream cannot know about. The tabler stylesheet selection moved to a top-level `await` keyed on `document.dir` (set synchronously at `src/locale` module init, so it is readable before the awaits), with `installDeploymentRecovery()` called before them so a broken deploy 404ing the chunks cannot take the boot guard down too. `vite.config.js` keeps `tabler.rtl.min.css` out of the `ui-vendor` code-splitting group — required for any dynamic-tabler shape, since the group otherwise merges both stylesheets into one always-loaded asset and applies RTL rules to LTR pages (the +12px dialog overflow that broke the responsive smoke checks in the second pass). `api/backend/base.js` keeps the fork's 401 handling — the 401 branch runs before `response.json()` so a proxy's HTML error page cannot crash the parse, logout still calls `deleteToken()`, `error.status` survives, and null `params`/bare `AbortSignal` are tolerated — capabilities upstream's simplification removed from its own copy. Verified by the full container smoke (171 checks, including the five responsive-layout checks that exposed the chunking bug), 163 backend and 15 frontend tests, and biome clean on the LF-normalized tree. - Second reconciliation of upstream develop after another force-push (merge-base regressed to `20b56ee8` again; patch-id comparison shows everything already carried was re-offered rebased, and eight commits are genuinely new). Kept the fork's DB-level TOTP/challenge claims over upstream's new claim-before-verify reorder, which exists to serialize their in-memory Maps; the SQL step claim already makes that race impossible atomically and preserves the friendlier mistyped-code retry. Adopted upstream's removal of the `validate: { trustProxy: false }` limiter suppressions — dead code since `trust proxy` became `1`, and dropping it restores fail-loud behavior if the setting ever returns to `true`. Dockerfile reconciled as fork-plus-deltas: the fork's pip/certbot/luarocks pins and the crowdsec telemetry instrumentation survive, while upstream's `RCP_VER`→`ORP_VER` rename, the nginx patch-2 swap to PR 1756, and openresty patches 8–10 (resolver hosts, upstream pipelining, reuseport fd cleanup) land; nginx.conf auto-merged to `resolver local=on hosts=on ipv6=on` and the reworked quoted `alog` format, with goaccess taking upstream's matching log-format fields plus the fork's `--external-assets` for the strict CSP. Dependencies split by maturity: took `multer 2.4.0` and `react-router 8.4.0` (now past the 7-day window), held back `@biomejs/biome 2.5.14`, `undici 8.11.0`, `json-schema-ref-parser 16.0.3`, `swagger-parser 13.1.0`, `@tabler/icons-react 3.48.0`, `react-query 5.103.2`, `markdown-to-jsx 9.10.3`, and `react-intl 12.1.2` (a two-major jump; kept at 10.1.26) for the renovate cron, with lockfiles regenerated under the enforced `minimumReleaseAge`. Adopted upstream's RTL support on top of the fork's `installDeploymentRecovery()` in `main.jsx` (dynamic RTL tabler stylesheet, document `dir`/`lang` handling, Persian and Turkish translations); the RTL code uses only APIs present in react-intl 10. Certificates keep the fork's stricter multer limits (`fields: 0, parts: 2, fieldNameSize: 64` were already there), and the fail-closed CrowdSec `APPSEC_URL` default stays. diff --git a/Dockerfile b/Dockerfile index 8ee6fb2e76..d1065b5754 100644 --- a/Dockerfile +++ b/Dockerfile @@ -84,6 +84,7 @@ RUN git-clone-commit.sh https://github.com/nginx/nginx "$NGINX_VER" /src/nginx & 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 && \ @@ -251,6 +252,7 @@ RUN apk upgrade --no-cache -a && \ luarocks-5.1 install lua-resty-http 0.18.0-0 && \ luarocks-5.1 install lua-resty-string 0.09-0 && \ luarocks-5.1 install lua-resty-openssl 1.9.0-1 && \ + sed -i 's|^local legacy_nids = {}$|C.ERR_clear_error()\n&|' /usr/local/share/lua/5.1/resty/openssl/pkey.lua && \ \ git config --global advice.detachedHead false && \ git config --global init.defaultBranch main && \ diff --git a/FORK.md b/FORK.md index d89759d386..5965283245 100644 --- a/FORK.md +++ b/FORK.md @@ -528,3 +528,68 @@ moved to a top-level `await` keyed on `document.dir` (set synchronously at `main.jsx` adopts the top-level `await` shape with `installDeploymentRecovery()` called before the stylesheet awaits, so the boot guard installs even when a broken deploy 404s the chunks it awaits. + +## Upstream merge resolution notes (September 26) + +The September 26 reconciliation on `fix/upstream-sync-20260926` merges +upstream `a4a29e09` (merge-base `03688155`, the 71-commit gap) into the +fork's develop state after PRs #35–#39 landed. The dominant upstream change +is the move from row `meta` to dedicated `npmplus_*` columns +(`meta_to_columns`: nginx online/err, directory, mTLS verify, and the +certificate dns-provider fields), together with a global `jsonReplacer` in +`helpers.js` — wired with `app.set("json replacer", …)` — that blanks +`password` and drops `certificate`, `certificate_key`, and the dns-provider +credentials from every response. That replacer supersedes the fork's old +per-site meta masking for those fields, so the merge adopts upstream's +shapes. Two fork behaviors ride on the new model: + +- The forward-destination reachability probe (`configureWithReachability`) + survives as the only remaining meta writer: it runs `internalNginx.configure` + — which now persists `npmplus_nginx_online`/`npmplus_nginx_err` itself — + then patches only `reach_ok`/`reach_err` into meta. `proxy_host`'s + `$parseDatabaseJson` no longer destructures `meta` out (upstream's version + stripped it), so the probe state reaches the API; the frontend sorts + enabled → offline → unreachable → online and passes + `npmplus_nginx_online`/`meta.reach_ok` separately. +- The access-list row masking (`maskItems`/`maskAccessListItems`, lodash + `_.omit` plus blanked item passwords) is kept, and `lodash` returns to the + backend manifest for it. Upstream's recursive replacer may now blank the + nested item passwords too; per this file's replacement rule, any redundancy + sweep happens later in a normal reviewable commit — never inside the merge. + +The permission model is a true MERGE-BOTH: upstream's sync `canAdmin()` and +`canUser(id)` (both throwing, ids must be positive) and async `can()` are +adopted, while the fork-only crowdsec routes gate through a local +`requireAdmin(res)` helper because their handlers are not per-user scoped. +Host CRUD takes upstream's `savedRow`/`try`/`finally` shape with the fork's +`assertPrivilegedNginxFields`, `validateIncomingPort`, mTLS, and SSL/HSTS +cleanup intact; `user.js` takes upstream's email/avatar handling and keeps +the fork's bounded gravatar fetch. `token.js` keeps the fork's session-token +architecture. Dependency aging held: `@tabler/core` 1.6.0 and `vite` 8.3.1 +failed the `minimumReleaseAge` window and stay at 1.5.1/8.3.0 (both +lockfiles regenerated under the policy). Upstream's ECH key rotation is +adopted (rootfs hooks, `ECH_ROTATION_INTERVAL`, the cloudflare example); +the README section keeps the 1300-word release-discipline budget and the +full guide lives in `docs/ech.md`. The caddy build keeps the fork's source +build and CVE pin matrix, `npmplus.conf` keeps the goaccess CSP/no-cache +headers, and upstream's goaccess dinit flag reorder landed. + +Test fixtures and smokes track the new contract per this file's rules: +`certificate-dns.test.js` moved its fixture to the `npmplus_*` columns, +`sqlite-upgrade.test.js` seeds the legacy proxy row through raw knex with +the columns the pre-replay schema actually had and asserts survival at the +field level, and the live smokes (`security-regressions.mjs`, +`rc5-features.mjs`) read `npmplus_nginx_online` plus `meta.reach_ok`. The +security-regressions assertions were updated because the response shape +changed upstream, not to loosen a check: the container smoke caught the +first miss against the real image. + +Local validation: 163 backend and 15 frontend tests, `validate-schema`, +biome on both LF-normalized trees, the vite build, +`tests/security-invariants.mjs`, frozen lockfiles, the 45-check +in-process backend smoke, and the full disposable-container smoke +(security-regressions, modal-ui, ui-driver, and browser-driven +security-ui against the image built from this branch). The Linux-only +python contracts (installer-recovery, heal-migration, upstream-sync) and +`sort-locale.sh` (needs jq) reproduce identically on a pristine fork-HEAD +worktree on this rig and stay green on CI. diff --git a/README.md b/README.md index b4ed0f40a4..0fffc3f9a6 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,10 @@ Every service should say `Up`, and `npmplus` should become `healthy` after start Moving to another machine, reading logs, recovering a failed update, and the advanced opt-ins an ordinary update deliberately preserves (AppSec, protected startup, the Cloudflare origin lock) are all covered in [host setup and operations](docs/setup-npmplus.md) - migration is three commands, and [Diagnostics](docs/setup-npmplus.md#diagnostics) covers logs, CrowdSec checks, and backup restoration. +## Encrypted Client Hello (ECH) + +NPMplus can generate and automatically rotate ECH keys: fill `/opt/npmplus/tls/ech/cron.sh` with a script that calls the built-in `ech.sh` and pushes the resulting config to your DNS provider's HTTPS records. The container runs the script hourly (`ECH_ROTATION_INTERVAL` in `compose.yaml`), enables ECH in nginx, and reloads. Clearing the file disables ECH again. Full instructions and a Cloudflare example script ([`ech-cron-cloudflare-example.sh`](ech-cron-cloudflare-example.sh)) are in [docs/ech.md](docs/ech.md). + ## What this fork adds - One interactive installer for installation, updates, diagnostics, restore, and uninstall, with loopback-only dashboard access by default. diff --git a/backend/.smoke/rc5-features.mjs b/backend/.smoke/rc5-features.mjs index 38381c9cc2..d1f4554f3e 100644 --- a/backend/.smoke/rc5-features.mjs +++ b/backend/.smoke/rc5-features.mjs @@ -88,13 +88,12 @@ const created = await request("POST", "/api/nginx/proxy-hosts", createBody); check("proxy host is created", created.status === 200 || created.status === 201, JSON.stringify(created)); const hostId = created.json?.id; const listAfterCreate = await request("GET", "/api/nginx/proxy-hosts"); -const createdMeta = listAfterCreate.json?.find((item) => item.id === hostId)?.meta; +const createdRow = listAfterCreate.json?.find((item) => item.id === hostId); +const createdMeta = createdRow?.meta; check( "created host appears in the list with online meta and a reachability probe", - listAfterCreate.json?.some((item) => item.id === hostId && item.meta) && - createdMeta?.nginx_online === true && - createdMeta?.reach_ok === false, - JSON.stringify(createdMeta), + createdRow?.npmplus_nginx_online === true && createdMeta?.reach_ok === false, + JSON.stringify(createdRow), ); const updated = await request("PUT", `/api/nginx/proxy-hosts/${hostId}`, { diff --git a/backend/.smoke/security-regressions.mjs b/backend/.smoke/security-regressions.mjs index 19fd6dc255..549a143b5d 100644 --- a/backend/.smoke/security-regressions.mjs +++ b/backend/.smoke/security-regressions.mjs @@ -102,7 +102,7 @@ for (const destination of [ } const created = await delegated.request("POST", "/nginx/proxy-hosts", host); check("normal delegated proxy created", created.status, 201); -check("real nginx accepts normal proxy", created.data.meta.nginx_online, true); +check("real nginx accepts normal proxy", created.data.npmplus_nginx_online, true); const hostUrl = `/nginx/proxy-hosts/${created.data.id}`; check( "socket update rejected", @@ -151,7 +151,7 @@ const app = await admin.request("POST", "/nginx/proxy-hosts", { forward_port: null, }); check("admin application socket remains supported", app.status, 201); -check("nginx accepts application socket", app.data.meta.nginx_online, true); +check("nginx accepts application socket", app.data.npmplus_nginx_online, true); await admin.request("DELETE", `/nginx/proxy-hosts/${app.data.id}`); for (const username of ["bad:name", "bad\nname", "bad\rname"]) { check( diff --git a/backend/.smoke/smoke-backend.mjs b/backend/.smoke/smoke-backend.mjs index a52b93a1f6..be4c7b9dcd 100644 --- a/backend/.smoke/smoke-backend.mjs +++ b/backend/.smoke/smoke-backend.mjs @@ -70,6 +70,7 @@ app.use( Object.defineProperty(res.locals, "access", { get: () => ({ can: async () => true, + canAdmin: () => true, token: { getUserId: () => 1 }, }), set: () => {}, @@ -207,21 +208,22 @@ const server = app.listen(13000, "127.0.0.1", async () => { // 7. alerts context for an ip: attacker geo + sanitized event meta const alerts = await fetch(`${base}/alerts?scope=Ip&value=198.51.100.7`, { headers: admin }); const alertsBody = await alerts.json(); + const alertsItems = alertsBody.items ?? []; check( "GET alerts -> 200 with the alert", - alerts.status === 200 && alertsBody.length === 1 && alertsBody[0].scenario === "crowdsecurity/http-probing", + alerts.status === 200 && alertsItems.length === 1 && alertsItems[0].scenario === "crowdsecurity/http-probing", `got ${alerts.status} ${JSON.stringify(alertsBody).slice(0, 80)}`, ); check( "alert carries attacker geo details", - alertsBody[0]?.source?.country === "DE" && alertsBody[0]?.source?.as_name === "Example ASN", - JSON.stringify(alertsBody[0]?.source), + alertsItems[0]?.source?.country === "DE" && alertsItems[0]?.source?.as_name === "Example ASN", + JSON.stringify(alertsItems[0]?.source), ); check( "alert events keep attack meta but strip unknown keys", - alertsBody[0]?.events?.[0]?.meta?.some((m) => m.key === "target_uri" && m.value === "/.env") && - !alertsBody[0]?.events?.[0]?.meta?.some((m) => m.key === "raw_request"), - JSON.stringify(alertsBody[0]?.events?.[0]?.meta), + alertsItems[0]?.events?.[0]?.meta?.some((m) => m.key === "target_uri" && m.value === "/.env") && + !alertsItems[0]?.events?.[0]?.meta?.some((m) => m.key === "raw_request"), + JSON.stringify(alertsItems[0]?.events?.[0]?.meta), ); // 8. no machine key file -> 503 not-wired-machine diff --git a/backend/app.js b/backend/app.js index 2212a13b69..73d81be55d 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1,9 +1,13 @@ import crypto from "node:crypto"; import cookieParser from "cookie-parser"; import express from "express"; +import multer from "multer"; +import { jsonReplacer } from "./lib/helpers.js"; import { debug, express as logger } from "./logger.js"; import mainRoutes from "./routes/main.js"; +Object.assign(multer.MulterError.prototype, { public: true, status: 400 }); + /** * App */ @@ -12,6 +16,7 @@ const app = express(); app.set("trust proxy", 1); app.disable("x-powered-by"); app.set("json spaces", 2); +app.set("json replacer", jsonReplacer); app.use(cookieParser(process.env.COOKIE_SECRET || crypto.randomBytes(16).toString("hex"))); app.use(express.json({ limit: "1mb" })); @@ -50,11 +55,13 @@ app.use("/", mainRoutes); // production error handler // no stacktraces leaked to user app.use((err, req, res, _) => { + const status = err.status || 500; + const exposed = err.public || err.expose; const requestId = crypto.randomUUID(); const payload = { error: { - code: err.status || 500, - message: err.public ? err.message : "Internal Error", + code: status, + message: exposed ? err.message : "Internal Error", request_id: requestId, }, }; @@ -71,7 +78,7 @@ app.use((err, req, res, _) => { // Not every error is worth logging - but this is good for now until it gets annoying. if (typeof err.stack !== "undefined" && err.stack) { debug(logger, `[${requestId}] ${err.stack}`); - if (typeof err.public === "undefined" || !err.public) { + if (!exposed) { logger.warn(`[${requestId}] ${req.method.toUpperCase()} ${req.originalUrl}: ${err}`); } } diff --git a/backend/certbot/dns-plugins.json b/backend/certbot/dns-plugins.json index b0c2f24887..f0bf60715d 100644 --- a/backend/certbot/dns-plugins.json +++ b/backend/certbot/dns-plugins.json @@ -24,10 +24,10 @@ "package_name": "certbot-dns-arvan" }, "azure": { - "credentials": "# This plugin supported API authentication using either Service Principals or utilizing a Managed Identity assigned to the virtual machine.\n# Regardless which authentication method used, the identity will need the “DNS Zone Contributor” role assigned to it.\n# As multiple Azure DNS Zones in multiple resource groups can exist, the config file needs a mapping of zone to resource group ID. Multiple zones -> ID mappings can be listed by using the key dns_azure_zoneX where X is a unique number. At least 1 zone mapping is required.\n\n# Using a service principal (option 1)\ndns_azure_sp_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\ndns_azure_sp_client_secret = E-xqXU83Y-jzTI6xe9fs2YC~mck3ZzUih9\ndns_azure_tenant_id = ed1090f3-ab18-4b12-816c-599af8a88cf7\n\n# Using used assigned MSI (option 2)\n# dns_azure_msi_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\n\n# Using system assigned MSI (option 3)\n# dns_azure_msi_system_assigned = true\n\n# Zones (at least one always required)\ndns_azure_zone1 = example.com:/subscriptions/c135abce-d87d-48df-936c-15596c6968a5/resourceGroups/dns1\ndns_azure_zone2 = example.org:/subscriptions/99800903-fb14-4992-9aff-12eaf2744622/resourceGroups/dns2", + "credentials": "# Docs: https://cloudchristoph.github.io/certbot-dns-azure-modern/\n# Service principal with the \"DNS Zone Contributor\" role on the DNS zone\ndns_azure_sp_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\ndns_azure_sp_client_secret = example-client-secret-not-real\ndns_azure_tenant_id = ed1090f3-ab18-4b12-816c-599af8a88cf7\n# One line per DNS zone, format ZONE_NAME:RESOURCE_GROUP_ID\ndns_azure_zone1 = example.com:/subscriptions/c135abce-d87d-48df-936c-15596c6968a5/resourceGroups/rg-dns", "full_plugin_name": "dns-azure", "name": "Azure", - "package_name": "certbot-dns-azure" + "package_name": "certbot-dns-azure-modern" }, "baidu": { "credentials": "dns_baidu_access_key = 12345678\ndns_baidu_secret_key = 1234567890abcdef1234567890abcdef", @@ -173,6 +173,12 @@ "name": "Akamai Edge DNS", "package_name": "certbot-plugin-edgedns" }, + "edgeone": { + "credentials": "dns_edgeone_secret_id = YOUR_TENCENTCLOUD_SECRET_ID\ndns_edgeone_secret_key = YOUR_TENCENTCLOUD_SECRET_KEY", + "full_plugin_name": "dns-edgeone", + "name": "Tencent Cloud EdgeOne", + "package_name": "certbot-dns-edgeone" + }, "eurodns": { "credentials": "dns_eurodns_applicationId = myuser\ndns_eurodns_apiKey = mysecretpassword\ndns_eurodns_endpoint = https://rest-api.eurodns.com/dns-zones/", "full_plugin_name": "dns-eurodns", diff --git a/backend/internal/access-list.js b/backend/internal/access-list.js index ac33c3ea1e..545e419152 100644 --- a/backend/internal/access-list.js +++ b/backend/internal/access-list.js @@ -2,7 +2,6 @@ import { appendFile, rm, writeFile } from "node:fs/promises"; import bcrypt from "bcryptjs"; import _ from "lodash"; import errs from "../lib/error.js"; -import utils from "../lib/utils.js"; import { access as logger } from "../logger.js"; import accessListModel from "../models/access_list.js"; import accessListAuthModel from "../models/access_list_auth.js"; @@ -12,10 +11,11 @@ import internalAuditLog from "./audit-log.js"; import internalNginx from "./nginx.js"; import internalProxyHostAccessList from "./proxy-host-access-list.js"; -const omissions = () => ["is_deleted", "owner.is_deleted"]; // biome-ignore lint/suspicious/noControlCharactersInRegex: reject htpasswd record delimiters and control bytes const invalidUsername = /[:\u0000-\u001f\u007f]/; +const omissions = () => ["is_deleted", "owner.is_deleted"]; + const internalAccessList = { /** * @param {Access} access @@ -24,14 +24,12 @@ const internalAccessList = { */ create: async (access, data) => { access.can("access_lists:manage"); - const row = utils.omitRow(omissions())( - 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.query().insertAndFetch({ + name: data.name, + satisfy_any: data.satisfy_any, + pass_auth: data.pass_auth, + owner_user_id: access.token.getUserId(1), + }); data.id = row.id; @@ -58,40 +56,24 @@ const internalAccessList = { ); // re-fetch with expansions - const freshRow = await internalAccessList.get( - access, - { - id: data.id, - expand: ["owner", "items", "clients", "proxy_hosts.[access_lists.[clients,items]]"], - }, - true, // skip masking - ); + const freshRow = await internalAccessList.get(access, { + id: data.id, + expand: ["items", "clients"], + }); - // Audit log - data.meta = { ...data.meta, ...freshRow.meta }; - await internalAccessList.build(freshRow); - if (Number.parseInt(freshRow.proxy_host_count, 10)) { - // 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) => { - const cleanedHost = internalProxyHostAccessList.cleanAccessListTypes(host); - return internalProxyHostAccessList.populateLocationAccessLists(cleanedHost); - }), - ); - await internalNginx.bulkGenerateConfigs(proxyHostModel, "proxy_host", freshRow.proxy_hosts); + try { + await internalAccessList.build(freshRow); + } finally { + // Add to audit log + await internalAuditLog.add(access, { + action: "created", + object_type: "access-list", + object_id: freshRow.id, + meta: freshRow, + }); } - // Add to audit log - await internalAuditLog.add(access, { - action: "created", - object_type: "access-list", - object_id: freshRow.id, - meta: internalAccessList.maskItems(data), - }); - - if (Array.isArray(freshRow.proxy_hosts)) - freshRow.proxy_hosts = freshRow.proxy_hosts.map(internalProxyHostAccessList.maskAccessListItems); - return internalAccessList.maskItems(freshRow); + return freshRow; }, /** @@ -164,39 +146,38 @@ const internalAccessList = { ); } - // Add to audit log - await internalAuditLog.add(access, { - action: "updated", - object_type: "access-list", - object_id: data.id, - meta: internalAccessList.maskItems(data), + // re-fetch with expansions + const freshRow = await internalAccessList.get(access, { + id: data.id, + expand: ["items", "clients", "proxy_hosts.[certificate,access_lists.[clients,items]]"], }); - // re-fetch with expansions - const freshRow = await internalAccessList.get( - access, - { - id: data.id, - expand: ["owner", "items", "clients", "proxy_hosts.[certificate,access_lists.[clients,items]]"], - }, - true, // skip masking - ); + const savedRow = { ...freshRow, proxy_hosts: undefined }; - await internalAccessList.build(freshRow); - if (Number.parseInt(freshRow.proxy_host_count, 10)) { - // 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) => { - const cleanedHost = internalProxyHostAccessList.cleanAccessListTypes(host); - return internalProxyHostAccessList.populateLocationAccessLists(cleanedHost); - }), - ); - await internalNginx.bulkGenerateConfigs(proxyHostModel, "proxy_host", freshRow.proxy_hosts); + try { + await internalAccessList.build(freshRow); + if (Number.parseInt(freshRow.proxy_host_count, 10)) { + // 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) => { + const cleanedHost = internalProxyHostAccessList.cleanAccessListTypes(host); + return internalProxyHostAccessList.populateLocationAccessLists(cleanedHost); + }), + ); + await internalNginx.bulkGenerateConfigs(proxyHostModel, "proxy_host", freshRow.proxy_hosts); + } + await internalNginx.reload(); + } finally { + // Add to audit log + await internalAuditLog.add(access, { + action: "updated", + object_type: "access-list", + object_id: data.id, + meta: savedRow, + }); } - await internalNginx.reload(); - if (Array.isArray(freshRow.proxy_hosts)) - freshRow.proxy_hosts = freshRow.proxy_hosts.map(internalProxyHostAccessList.maskAccessListItems); - return internalAccessList.maskItems(freshRow); + + return savedRow; }, /** @@ -204,11 +185,9 @@ const internalAccessList = { * @param {Object} data * @param {Integer} data.id * @param {Array} [data.expand] - * @param {Array} [data.omit] - * @param {Boolean} [skipMasking] * @return {Promise} */ - get: async (access, data, skipMasking) => { + get: async (access, data) => { const thisData = data || {}; access.can("access_lists:view"); @@ -230,7 +209,7 @@ const internalAccessList = { .where("access_list.is_deleted", 0) .andWhere("access_list.id", thisData.id) .groupBy("access_list.id") - .allowGraph("[owner,items,clients,proxy_hosts.[certificate,access_lists.[clients,items]]]") + .allowGraph("[items,clients,proxy_hosts.[certificate,access_lists.[clients,items]]]") .first(); if (access.visibility !== "all") { @@ -241,20 +220,11 @@ const internalAccessList = { query.withGraphFetched(`[${thisData.expand.join(", ")}]`); } - let row = utils.omitRow(omissions())(await query); + const row = await query; if (!row?.id) { throw new errs.ItemNotFoundError(thisData.id); } - if (!skipMasking && Array.isArray(row.proxy_hosts)) - row.proxy_hosts = row.proxy_hosts.map(internalProxyHostAccessList.maskAccessListItems); - if (!skipMasking) { - row = internalAccessList.maskItems(row); - } - // Custom omissions - if (typeof data.omit !== "undefined" && data.omit !== null) { - row = _.omit(row, data.omit); - } return row; }, @@ -325,46 +295,50 @@ const internalAccessList = { }); return updatedHost; }); - // 3. Write the changes to the database and 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); + const deletedRow = { ...row, proxy_hosts: undefined }; + + try { + // 3. Write the changes to the database and 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 + // 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) => { + const cleanedHost = internalProxyHostAccessList.cleanAccessListTypes(host); + return internalProxyHostAccessList.populateLocationAccessLists(cleanedHost); }), ); + await internalNginx.bulkGenerateConfigs(proxyHostModel, "proxy_host", row.proxy_hosts); + } + + await internalNginx.reload(); + + // delete the htpasswd file + await rm(internalAccessList.getFilename(row), { force: true }); + } finally { + // 4. audit log + await internalAuditLog.add(access, { + action: "deleted", + object_type: "access-list", + object_id: row.id, + meta: deletedRow, }); - row.proxy_hosts = affectedHosts; - // step 4. 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) => { - const cleanedHost = internalProxyHostAccessList.cleanAccessListTypes(host); - return internalProxyHostAccessList.populateLocationAccessLists(cleanedHost); - }), - ); - await internalNginx.bulkGenerateConfigs(proxyHostModel, "proxy_host", row.proxy_hosts); } - - await internalNginx.reload(); - - // delete the htpasswd file - await rm(internalAccessList.getFilename(row), { force: true }); - - // 4. audit log - await internalAuditLog.add(access, { - action: "deleted", - object_type: "access-list", - object_id: row.id, - meta: _.omit(row, ["is_deleted", "proxy_hosts"]), - }); - return true; + return deletedRow; }, /** @@ -413,9 +387,7 @@ const internalAccessList = { query.withGraphFetched(`[${expand.join(", ")}]`); } - return utils - .omitRows(omissions())(await query) - .map((row) => internalAccessList.maskItems(row)); + return await query; }, /** @@ -436,10 +408,6 @@ const internalAccessList = { return Number.parseInt(row.count, 10); }, - /** - * @param {Object} list - * @returns {Object} - */ maskItems: (list) => { if (!list) { return list; diff --git a/backend/internal/audit-log.js b/backend/internal/audit-log.js index b894fc578c..aa15d4286d 100644 --- a/backend/internal/audit-log.js +++ b/backend/internal/audit-log.js @@ -75,9 +75,7 @@ const internalAuditLog = { * @returns {Promise} */ add: (access, data) => { - if (typeof data.user_id === "undefined" || !data.user_id) { - data.user_id = access.token.getUserId(1); - } + data.user_id ||= access.token.getUserId(1); if (typeof data.action === "undefined" || !data.action) { throw new errs.InternalValidationError("Audit log entry must contain an Action"); diff --git a/backend/internal/backup-codes.js b/backend/internal/backup-codes.js index 60e43cb91e..448fd52111 100644 --- a/backend/internal/backup-codes.js +++ b/backend/internal/backup-codes.js @@ -68,12 +68,12 @@ const internalBackupCodes = { * @returns {Promise} */ verify: async (userId, code) => { - const codeTrim = code.trim().toUpperCase().replace(/O/g, "0").replace(/[IL]/g, "1"); + const normalizedCode = code.toUpperCase().replace(/O/g, "0").replace(/[IL]/g, "1"); for (const row of await codesOf(userId)) { const match = row.secret.startsWith("$2") - ? await bcrypt.compare(codeTrim, row.secret) - : await verify(codeTrim, row.secret); + ? await bcrypt.compare(normalizedCode, row.secret) + : await verify(normalizedCode, row.secret); // Remove used backup code, only the request that removes it counts as used if (match) return (await authModel.query().findById(row.id).delete()) === 1; } diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 9bd0f77a01..fb614ca42b 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -6,11 +6,11 @@ import path from "node:path"; import { domainToASCII } from "node:url"; import { ZipArchive } from "archiver"; import dayjs from "dayjs"; -import _ from "lodash"; import dnsPlugins from "../certbot/dns-plugins.json" with { type: "json" }; import { fetchWithTimeout, readBoundedJson } from "../lib/bounded-fetch.js"; import { installPlugin } from "../lib/certbot.js"; import error from "../lib/error.js"; +import { pickCertificateFields } from "../lib/helpers.js"; import utils from "../lib/utils.js"; import { debug, ssl as logger } from "../logger.js"; import certificateModel from "../models/certificate.js"; @@ -24,8 +24,6 @@ import internalNginx from "./nginx.js"; const cnPattern = /\bCN=([^\n]+)/i; -const omissions = () => ["is_deleted", "owner.is_deleted", "meta.dns_provider_credentials"]; - const internalCertificate = { allowedSslFiles: ["certificate", "certificate_key"], intervalTimeout: 1000 * 60 * 60 * (Number.parseInt(process.env.CERTBOT_RUN_INTERVAL, 10) || 3), @@ -129,7 +127,7 @@ const internalCertificate = { try { if (certificate.provider === "letsencrypt") { // Request a new Cert with Certbot. Let the fun begin. - if (certificate.meta?.dns_challenge) { + if (certificate.npmplus_dns_challenge) { await internalCertificate.requestCertbotWithDnsChallenge(certificate); } else { await internalCertificate.requestCertbot(certificate); @@ -141,14 +139,9 @@ const internalCertificate = { const certInfo = await internalCertificate.getCertificateInfoFromFile( `${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`, ); - const savedRow = utils.omitRow(omissions())( - await certificateModel.query().patchAndFetchById(certificate.id, { - expires_on: dayjs.unix(certInfo.dates.to).format("YYYY-MM-DD HH:mm:ss"), - }), - ); - - // Add cert data for audit log - savedRow.meta = { ...savedRow.meta, letsencrypt_certificate: certInfo }; + const savedRow = await certificateModel.query().patchAndFetchById(certificate.id, { + expires_on: dayjs.unix(certInfo.dates.to).format("YYYY-MM-DD HH:mm:ss"), + }); await internalCertificate.addCreatedAuditLog(access, certificate.id, savedRow); @@ -165,12 +158,12 @@ const internalCertificate = { throw err; } - data.meta = { ...data.meta, ...certificate.meta }; + const savedRow = await internalCertificate.get(access, { id: certificate.id }); // Add to audit log - await internalCertificate.addCreatedAuditLog(access, certificate.id, utils.omitRow(omissions())(data)); + await internalCertificate.addCreatedAuditLog(access, certificate.id, savedRow); - return utils.omitRow(omissions())(certificate); + return savedRow; }, addCreatedAuditLog: async (access, certificate_id, meta) => { @@ -201,22 +194,14 @@ const internalCertificate = { ); } - const savedRow = utils.omitRow(omissions())(await certificateModel.query().patchAndFetchById(row.id, data)); - - savedRow.meta = internalCertificate.cleanMeta(savedRow.meta); - data.meta = internalCertificate.cleanMeta(data.meta); - - // Add row.nice_name for custom certs - if (savedRow.provider === "other") { - data.nice_name = savedRow.nice_name; - } + const savedRow = await certificateModel.query().patchAndFetchById(row.id, data); // Add to audit log await internalAuditLog.add(access, { action: "updated", object_type: "certificate", object_id: row.id, - meta: _.omit(data, ["expires_on"]), // this prevents json circular reference because expires_on might be raw + meta: savedRow, }); return savedRow; @@ -226,52 +211,21 @@ const internalCertificate = { * @param {Access} access * @param {Object} data * @param {Number} data.id - * @param {Array} [data.expand] - * @param {Array} [data.omit] * @return {Promise} */ get: async (access, data) => { access.can("certificates:view"); - const query = certificateModel - .query() - .where("is_deleted", 0) - .andWhere("id", data.id) - .allowGraph("[owner,proxy_hosts,redirection_hosts,dead_hosts,streams]") - .first(); + const query = certificateModel.query().where("is_deleted", 0).andWhere("id", data.id).first(); if (access.visibility !== "all") { query.andWhere("owner_user_id", access.token.getUserId(1)); } - if (typeof data.expand !== "undefined" && data.expand !== null) { - query.withGraphFetched(`[${data.expand.join(", ")}]`); - } - - const row = utils.omitRow(omissions())(await query); + const row = await query; if (!row?.id) { throw new error.ItemNotFoundError(data.id); } - // Custom omissions - if (typeof data.omit !== "undefined" && data.omit !== null) { - return _.omit(row, [...data.omit]); - } - - return internalCertificate.cleanExpansions(row); - }, - cleanExpansions: (row) => { - if (typeof row.proxy_hosts !== "undefined") { - row.proxy_hosts = utils.omitRows(["is_deleted"])(row.proxy_hosts); - } - if (typeof row.redirection_hosts !== "undefined") { - row.redirection_hosts = utils.omitRows(["is_deleted"])(row.redirection_hosts); - } - if (typeof row.dead_hosts !== "undefined") { - row.dead_hosts = utils.omitRows(["is_deleted"])(row.dead_hosts); - } - if (typeof row.streams !== "undefined") { - row.streams = utils.omitRows(["is_deleted"])(row.streams); - } return row; }, @@ -282,7 +236,7 @@ const internalCertificate = { * @returns {Promise} */ download: async (access, data) => { - access.can("certificates:view"); + access.can("certificates:manage"); const certificate = await internalCertificate.get(access, data); if (certificate.provider === "letsencrypt") { const zipDirectory = internalCertificate.getLiveCertPath(data.id); @@ -356,13 +310,12 @@ const internalCertificate = { } for (const hostModel of [proxyHostModel, redirectionHostModel, deadHostModel, streamModel]) { - const hosts = await hostModel.query().where("is_deleted", 0).select("id", "certificate_id", "meta"); if ( - hosts.some( - (host) => - Number(host.certificate_id) === row.id || - Number(host.meta?.npmplus_mtls_certificate_id) === row.id, - ) + (await hostModel + .query() + .where("is_deleted", 0) + .andWhere((qb) => qb.where("certificate_id", row.id).orWhere("npmplus_mtls_certificate_id", row.id)) + .resultSize()) > 0 ) { throw new error.ValidationError("Certificate is still in use"); } @@ -373,13 +326,11 @@ const internalCertificate = { }); // Add to audit log - row.meta = internalCertificate.cleanMeta(row.meta); - await internalAuditLog.add(access, { action: "deleted", object_type: "certificate", object_id: row.id, - meta: _.omit(row, omissions()), + meta: row, }); if (row.provider === "letsencrypt") { @@ -391,7 +342,7 @@ const internalCertificate = { await rm(`/data/tls/custom/npm-${row.id}`, { force: true, recursive: true }); await rm(`/data/tls/custom/npm-${row.id}.der`, { force: true }); } - return true; + return row; }, /** @@ -409,7 +360,9 @@ const internalCertificate = { .query() .where("is_deleted", 0) .groupBy("id") - .allowGraph("[owner,proxy_hosts,redirection_hosts,dead_hosts,streams]") + .allowGraph( + "[owner,proxy_hosts,redirection_hosts,dead_hosts,streams,mtls_proxy_hosts,mtls_redirection_hosts,mtls_dead_hosts,mtls_streams]", + ) .orderBy("nice_name", "ASC"); if (access.visibility !== "all") { @@ -427,9 +380,7 @@ const internalCertificate = { query.withGraphFetched(`[${expand.join(", ")}]`); } - return utils - .omitRows(omissions())(await query) - .map((row) => internalCertificate.cleanExpansions(row)); + return await query; }, /** @@ -462,15 +413,15 @@ const internalCertificate = { logger.info("Writing Custom Certificate:", certificate.id); if (certificate.provider === "mtls") { - await writeFile(`/data/tls/mtls/npm-${certificate.id}.pem`, certificate.meta.certificate); + await writeFile(`/data/tls/mtls/npm-${certificate.id}.pem`, certificate.certificate); return; } const dir = `/data/tls/custom/npm-${certificate.id}`; await mkdir(dir, { recursive: true }); - await writeFile(`${dir}/fullchain.pem`, certificate.meta.certificate); - await writeFile(`${dir}/privkey.pem`, certificate.meta.certificate_key); + await writeFile(`${dir}/fullchain.pem`, certificate.certificate); + await writeFile(`${dir}/privkey.pem`, certificate.certificate_key); }, /** @@ -483,7 +434,7 @@ const internalCertificate = { internalCertificate.create(access, { provider: "letsencrypt", domain_names: data.domain_names, - meta: data.meta, + ...pickCertificateFields(data), }), /** @@ -547,13 +498,11 @@ const internalCertificate = { id: data.id, expires_on: dayjs.unix(validations.certificate.dates.to).format("YYYY-MM-DD HH:mm:ss"), domain_names: validations.certificate.cn, - meta: { ...row.meta }, // Prevent the update method from changing this value that we'll use later }); - certificate.meta = { ...row.meta, ...certs }; - await internalCertificate.writeCustomCert(certificate); + await internalCertificate.writeCustomCert({ ...certificate, ...certs }); await internalNginx.reload(); - return _.omit(certificate.meta, internalCertificate.allowedSslFiles); + return certificate; }, /** @@ -637,27 +586,6 @@ const internalCertificate = { return internalCertificate.getCertificateInfo(certContent, throwExpired); }, - /** - * Cleans the tls keys from the meta object and sets them - * @param {String} email the email address to use for registration to "true" - * - * @param {Object} meta - * @param {Boolean} [remove] - * @returns {Object} - */ - cleanMeta: (meta, remove) => { - for (const key of internalCertificate.allowedSslFiles) { - if (meta[key]) { - if (remove) { - delete meta[key]; - } else { - meta[key] = true; - } - } - } - return meta; - }, - /** * Request a certificate using the http challenge * @param {Object} certificate the certificate row @@ -683,7 +611,7 @@ const internalCertificate = { `npm-${certificate.id}`, ...(domains.length > 0 ? ["--domains", domains.map(domainToASCII).join(",")] : []), ...ips.flatMap((ip) => ["--ip-address", ip]), - ...(certificate.meta.reuse_key ? ["--reuse-key"] : ["--no-reuse-key"]), + ...(certificate.npmplus_reuse_key ? ["--reuse-key"] : ["--no-reuse-key"]), "--authenticator", "webroot", ]); @@ -696,18 +624,18 @@ const internalCertificate = { * @returns {Promise} */ requestCertbotWithDnsChallenge: async (certificate) => { - const dnsPlugin = dnsPlugins[certificate.meta.dns_provider]; + const dnsPlugin = dnsPlugins[certificate.npmplus_dns_provider]; if (!dnsPlugin) { - throw new Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`); + throw new Error(`Unknown DNS provider '${certificate.npmplus_dns_provider}'`); } - await installPlugin(certificate.meta.dns_provider); + await installPlugin(certificate.npmplus_dns_provider); logger.info( `Requesting Certbot certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(", ")}`, ); const credentialsLocation = `/tmp/certbot-credentials/credentials-${certificate.id}`; - await writeFile(credentialsLocation, certificate.meta.dns_provider_credentials, { mode: 0o600 }); + await writeFile(credentialsLocation, certificate.npmplus_dns_provider_credentials, { mode: 0o600 }); try { const result = await utils.execFile("certbot", [ @@ -720,15 +648,15 @@ const internalCertificate = { `npm-${certificate.id}`, "--domains", certificate.domain_names.map(domainToASCII).join(","), - ...(certificate.meta.reuse_key ? ["--reuse-key"] : ["--no-reuse-key"]), + ...(certificate.npmplus_reuse_key ? ["--reuse-key"] : ["--no-reuse-key"]), "--authenticator", dnsPlugin.full_plugin_name, `--${dnsPlugin.full_plugin_name}-credentials`, credentialsLocation, - ...(certificate.meta.propagation_seconds !== undefined + ...(certificate.npmplus_propagation_seconds > 0 ? [`--${dnsPlugin.full_plugin_name}-propagation-seconds`] : []), - ...(certificate.meta.propagation_seconds !== undefined ? [certificate.meta.propagation_seconds] : []), + ...(certificate.npmplus_propagation_seconds > 0 ? [certificate.npmplus_propagation_seconds] : []), ]); logger.info(result); return result; @@ -749,12 +677,11 @@ const internalCertificate = { const certificate = await internalCertificate.get(access, data); if (certificate.provider === "letsencrypt") { - const renewMethod = certificate.meta.dns_challenge + const renewMethod = certificate.npmplus_dns_challenge ? internalCertificate.renewCertbotWithDnsChallenge : internalCertificate.renewCertbot; await renewMethod(certificate); - await internalNginx.reload(); const certInfo = await internalCertificate.getCertificateInfoFromFile( `${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`, ); @@ -763,13 +690,17 @@ const internalCertificate = { expires_on: dayjs.unix(certInfo.dates.to).format("YYYY-MM-DD HH:mm:ss"), }); - // Add to audit log - await internalAuditLog.add(access, { - action: "renewed", - object_type: "certificate", - object_id: updatedCertificate.id, - meta: updatedCertificate, - }); + try { + await internalNginx.reload(); + } finally { + // Add to audit log + await internalAuditLog.add(access, { + action: "updated", + object_type: "certificate", + object_id: updatedCertificate.id, + meta: updatedCertificate, + }); + } return updatedCertificate; } @@ -823,9 +754,9 @@ const internalCertificate = { * @returns {Promise} */ renewCertbotWithDnsChallenge: async (certificate) => { - const dnsPlugin = dnsPlugins[certificate.meta.dns_provider]; + const dnsPlugin = dnsPlugins[certificate.npmplus_dns_provider]; if (!dnsPlugin) { - throw new Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`); + throw new Error(`Unknown DNS provider '${certificate.npmplus_dns_provider}'`); } logger.info( diff --git a/backend/internal/dead-host.js b/backend/internal/dead-host.js index 12183ffe92..aa448c69e1 100644 --- a/backend/internal/dead-host.js +++ b/backend/internal/dead-host.js @@ -1,16 +1,12 @@ -import _ from "lodash"; import errs from "../lib/error.js"; import { castJsonIfNeed } from "../lib/helpers.js"; import { assertPrivilegedNginxFields } from "../lib/nginx-privilege.js"; -import utils from "../lib/utils.js"; import deadHostModel from "../models/dead_host.js"; import internalAuditLog from "./audit-log.js"; import internalCertificate from "./certificate.js"; import internalHost from "./host.js"; import internalNginx from "./nginx.js"; -const omissions = () => ["is_deleted", "owner.is_deleted", "certificate.is_deleted"]; - const internalDeadHost = { /** * @param {Access} access @@ -26,6 +22,9 @@ const internalDeadHost = { } else if (Number(thisData.certificate_id) > 0) { await internalCertificate.get(access, { id: thisData.certificate_id }); } + if (Number(thisData.npmplus_mtls_certificate_id) > 0) { + await internalCertificate.get(access, { id: thisData.npmplus_mtls_certificate_id }); + } access.can("dead_hosts:manage"); await assertPrivilegedNginxFields(access, thisData); @@ -43,36 +42,38 @@ const internalDeadHost = { thisData.owner_user_id = access.token.getUserId(1); thisData = internalHost.cleanSslHstsData(createCertificate, thisData); - const createdRow = utils.omitRow(omissions())(await deadHostModel.query().insertAndFetch(thisData)); + const createdRow = await deadHostModel.query().insertAndFetch(thisData); - if (createCertificate) { - const cert = await internalCertificate.createQuickCertificate(access, thisData); + let savedRow; + try { + if (createCertificate) { + // update host with cert id + await deadHostModel + .query() + .where("id", createdRow.id) + .patch({ certificate_id: (await internalCertificate.createQuickCertificate(access, thisData)).id }); + } - // update host with cert id - await internalDeadHost.update(access, { + const row = await internalDeadHost.get(access, { id: createdRow.id, - certificate_id: cert.id, + expand: ["certificate"], }); - } - const row = await internalDeadHost.get(access, { - id: createdRow.id, - expand: ["certificate", "owner"], - }); - - // Configure nginx - await internalNginx.configure(deadHostModel, "dead_host", row); - - // Add to audit log - thisData.meta = { ...thisData.meta, ...row.meta }; - await internalAuditLog.add(access, { - action: "created", - object_type: "dead-host", - object_id: row.id, - meta: thisData, - }); + // Configure nginx + await internalNginx.configure(deadHostModel, "dead_host", row); + } finally { + savedRow = await internalDeadHost.get(access, { id: createdRow.id }); + + // Add to audit log + await internalAuditLog.add(access, { + action: "created", + object_type: "dead-host", + object_id: savedRow.id, + meta: savedRow, + }); + } - return row; + return savedRow; }, /** @@ -90,6 +91,9 @@ const internalDeadHost = { } else if (Number(thisData.certificate_id) > 0) { await internalCertificate.get(access, { id: thisData.certificate_id }); } + if (Number(thisData.npmplus_mtls_certificate_id) > 0) { + await internalCertificate.get(access, { id: thisData.npmplus_mtls_certificate_id }); + } access.can("dead_hosts:manage"); @@ -118,42 +122,43 @@ const internalDeadHost = { if (createCertificate) { const cert = await internalCertificate.createQuickCertificate(access, { + ...thisData, domain_names: thisData.domain_names || existingRow.domain_names, - meta: { ...existingRow.meta, ...thisData.meta }, }); // update host with cert id thisData.certificate_id = cert.id; } - // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. - thisData = { domain_names: existingRow.domain_names, ...thisData }; thisData = internalHost.cleanSslHstsData(createCertificate, thisData, existingRow); await deadHostModel.query().where({ id: thisData.id }).patch(thisData); - // Add to audit log - await internalAuditLog.add(access, { - action: "updated", - object_type: "dead-host", - object_id: existingRow.id, - meta: thisData, - }); - - const row = await internalDeadHost.get(access, { - id: thisData.id, - expand: ["certificate", "owner"], - }); + let savedRow; + try { + const row = await internalDeadHost.get(access, { + id: thisData.id, + expand: ["certificate"], + }); - if (!row.enabled) { // No need to add nginx config if host is disabled - return _.omit(internalHost.cleanRowCertificateMeta(row), omissions()); + if (row.enabled) { + // Configure nginx + await internalNginx.configure(deadHostModel, "dead_host", row); + } + } finally { + savedRow = await internalDeadHost.get(access, { id: thisData.id }); + + // Add to audit log + await internalAuditLog.add(access, { + action: "updated", + object_type: "dead-host", + object_id: savedRow.id, + meta: savedRow, + }); } - // Configure nginx - row.meta = await internalNginx.configure(deadHostModel, "dead_host", row); - - return _.omit(internalHost.cleanRowCertificateMeta(row), omissions()); + return savedRow; }, /** @@ -161,7 +166,6 @@ const internalDeadHost = { * @param {Object} data * @param {Number} data.id * @param {Array} [data.expand] - * @param {Array} [data.omit] * @return {Promise} */ get: async (access, data) => { @@ -173,7 +177,7 @@ const internalDeadHost = { .query() .where("is_deleted", 0) .andWhere("id", thisData.id) - .allowGraph(deadHostModel.defaultAllowGraph) + .allowGraph("[certificate]") .first(); if (access.visibility !== "all") { @@ -184,19 +188,12 @@ const internalDeadHost = { query.withGraphFetched(`[${thisData.expand.join(", ")}]`); } - const row = utils.omitRow(omissions())(await query); + const row = await query; if (!row?.id) { throw new errs.ItemNotFoundError(thisData.id); } - const thisRow = internalHost.cleanRowCertificateMeta(row); - - // Custom omissions - if (typeof thisData.omit !== "undefined" && thisData.omit !== null) { - return _.omit(thisRow, thisData.omit); - } - - return thisRow; + return row; }, /** @@ -218,19 +215,21 @@ const internalDeadHost = { is_deleted: 1, }); - // Delete Nginx Config - await internalNginx.deleteConfig("dead_host", row); - await internalNginx.reload(); - - // Add to audit log - await internalAuditLog.add(access, { - action: "deleted", - object_type: "dead-host", - object_id: row.id, - meta: _.omit(row, omissions()), - }); + try { + // Delete Nginx Config + await internalNginx.deleteConfig("dead_host", row); + await internalNginx.reload(); + } finally { + // Add to audit log + await internalAuditLog.add(access, { + action: "deleted", + object_type: "dead-host", + object_id: row.id, + meta: row, + }); + } - return true; + return row; }, /** @@ -245,7 +244,7 @@ const internalDeadHost = { const row = await internalDeadHost.get(access, { id: data.id, - expand: ["certificate", "owner"], + expand: ["certificate"], }); if (!row?.id) { throw new errs.ItemNotFoundError(data.id); @@ -268,18 +267,23 @@ const internalDeadHost = { enabled: 1, }); - // Configure nginx - await internalNginx.configure(deadHostModel, "dead_host", row); - - // Add to audit log - await internalAuditLog.add(access, { - action: "enabled", - object_type: "dead-host", - object_id: row.id, - meta: _.omit(row, omissions()), - }); + let savedRow; + try { + // Configure nginx + await internalNginx.configure(deadHostModel, "dead_host", row); + } finally { + savedRow = await internalDeadHost.get(access, { id: row.id }); + + // Add to audit log + await internalAuditLog.add(access, { + action: "enabled", + object_type: "dead-host", + object_id: row.id, + meta: savedRow, + }); + } - return true; + return savedRow; }, /** @@ -306,19 +310,24 @@ const internalDeadHost = { enabled: 0, }); - // Delete Nginx Config - await internalNginx.deleteConfig("dead_host", row); - await internalNginx.reload(); - - // Add to audit log - await internalAuditLog.add(access, { - action: "disabled", - object_type: "dead-host", - object_id: row.id, - meta: _.omit(row, omissions()), - }); + let savedRow; + try { + // Delete Nginx Config + await internalNginx.deleteConfig("dead_host", row); + await internalNginx.reload(); + } finally { + savedRow = await internalDeadHost.get(access, { id: row.id }); + + // Add to audit log + await internalAuditLog.add(access, { + action: "disabled", + object_type: "dead-host", + object_id: row.id, + meta: savedRow, + }); + } - return true; + return savedRow; }, /** @@ -336,7 +345,7 @@ const internalDeadHost = { .query() .where("is_deleted", 0) .groupBy("id") - .allowGraph(deadHostModel.defaultAllowGraph) + .allowGraph("[owner,certificate]") .orderBy(castJsonIfNeed("domain_names"), "ASC"); if (access.visibility !== "all") { @@ -354,12 +363,7 @@ const internalDeadHost = { query.withGraphFetched(`[${expand.join(", ")}]`); } - const rows = utils.omitRows(omissions())(await query); - if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) { - return internalHost.cleanAllRowsCertificateMeta(rows); - } - - return rows; + return await query; }, /** diff --git a/backend/internal/host.js b/backend/internal/host.js index f7c7264196..7ed2331660 100644 --- a/backend/internal/host.js +++ b/backend/internal/host.js @@ -48,36 +48,6 @@ const internalHost = { return combinedData; }, - /** - * used by the getAll functions of hosts, this removes the certificate meta if present - * - * @param {Array} rows - * @returns {Array} - */ - cleanAllRowsCertificateMeta: (rows) => { - for (const row of rows) { - if (row.certificate) { - row.certificate.meta = {}; - } - } - - return rows; - }, - - /** - * used by the get/update functions of hosts, this removes the certificate meta if present - * - * @param {Object} row - * @returns {Object} - */ - cleanRowCertificateMeta: (row) => { - if (typeof row.certificate !== "undefined" && row.certificate) { - row.certificate.meta = {}; - } - - return row; - }, - /** * Internal use only, checks to see if the domain is already taken by any other record * diff --git a/backend/internal/mfa.js b/backend/internal/mfa.js index d475389095..a9d561bab5 100644 --- a/backend/internal/mfa.js +++ b/backend/internal/mfa.js @@ -93,15 +93,13 @@ const internalMfa = { * @returns {Promise} */ verifyForLogin: async (userId, token) => { - const tokenTrim = token.trim(); - // TOTP codes are 6 chars, backup codes are 8 chars - if (tokenTrim.length === 6) { - return await totp.verifyCode(userId, tokenTrim); + if (token.length === 6) { + return await totp.verifyCode(userId, token); } - if (tokenTrim.length === 8) { - return await backupCodes.verify(userId, tokenTrim); + if (token.length === 8) { + return await backupCodes.verify(userId, token); } return false; @@ -158,12 +156,10 @@ const internalMfa = { throw new errs.ValidationError("MFA is not enabled"); } - const tokenTrim = token.trim(); - - if (tokenTrim.length !== 6) { + if (token.length !== 6) { throw new errs.ValidationError("Invalid verification code"); } - if (!(await totp.verifyCode(userId, tokenTrim))) { + if (!(await totp.verifyCode(userId, token))) { throw new errs.ValidationError("Invalid verification code"); } diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js index e9b28ed5a9..628b26cf6f 100644 --- a/backend/internal/nginx.js +++ b/backend/internal/nginx.js @@ -18,8 +18,8 @@ const internalNginx = { * - test the nginx config first to make sure it's OK * - create / recreate the config for the host * - test again - * - IF OK: update the meta with online status - * - IF BAD: update the meta with offline status and rename the config + * - IF OK: save the online status + * - IF BAD: save the offline status and rename the config * - then reload nginx * * @param {Object|String} model @@ -28,10 +28,10 @@ const internalNginx = { * @returns {Promise} */ configure: async (model, host_type, host, { skipReload = false } = {}) => { - let combined_meta = {}; + let status = {}; // skip disabled hosts - if (!host.enabled) return host.meta; + if (!host.enabled) return status; await internalProxyHostAccessList.build(host_type, host); await internalNginx.deleteConfig(host_type, host); @@ -39,26 +39,22 @@ const internalNginx = { try { await (skipReload ? internalNginx.test() : internalNginx.reload()); - combined_meta = { ...host.meta, nginx_online: true, nginx_err: null }; + status = { npmplus_nginx_online: true, npmplus_nginx_err: "" }; - await model.query().where("id", host.id).patch({ - meta: combined_meta, - }); + await model.query().where("id", host.id).patch(status); } catch (err) { logger.error(err.message); - // config is bad, update meta and rename config - combined_meta = { ...host.meta, nginx_online: false, nginx_err: err.message }; + // config is bad, update status and rename config + status = { npmplus_nginx_online: false, npmplus_nginx_err: err.message }; - await model.query().where("id", host.id).patch({ - meta: combined_meta, - }); + await model.query().where("id", host.id).patch(status); await internalNginx.renameConfigAsError(host_type, host); if (!skipReload) await internalNginx.reload(); } - return combined_meta; + return status; }, /** @@ -122,13 +118,7 @@ const internalNginx = { * @returns {Promise} */ renderLocations: async (host) => { - let template; - - try { - template = await utils.getParsedTemplate(`${__dirname}/../templates/_proxy_host_custom_location.conf`); - } catch (err) { - throw new errs.ConfigurationError(err.message); - } + const template = await utils.getParsedTemplate(`${__dirname}/../templates/_proxy_host_custom_location.conf`); const renderEngine = utils.getRenderEngine(); let renderedLocations = ""; @@ -167,13 +157,7 @@ const internalNginx = { * @returns {Promise} */ renderUpstreams: async (host) => { - let template; - - try { - template = await utils.getParsedTemplate(`${__dirname}/../templates/_upstream.conf`); - } catch (err) { - throw new errs.ConfigurationError(err.message); - } + const template = await utils.getParsedTemplate(`${__dirname}/../templates/_upstream.conf`); const renderEngine = utils.getRenderEngine(); let renderedUpstreams = ""; @@ -241,14 +225,9 @@ const internalNginx = { const renderEngine = utils.getRenderEngine(); - let template = null; const filename = internalNginx.getConfigName(nice_host_type, host.id); - try { - template = await utils.getParsedTemplate(`${__dirname}/../templates/${nice_host_type}.conf`); - } catch (err) { - throw new errs.ConfigurationError(err.message); - } + const template = await utils.getParsedTemplate(`${__dirname}/../templates/${nice_host_type}.conf`); host.env = process.env; @@ -378,21 +357,16 @@ const internalNginx = { } } - try { - const config_text = await renderEngine.render(template, host); - - await writeFile(filename, config_text, { encoding: "utf8" }); - debug(logger, "Wrote config:", filename); + const config_text = await renderEngine.render(template, host); - if (process.env.DISABLE_NGINX_BEAUTIFIER === "false") { - await utils.execFile("nginxbeautifier", ["-s", "2", filename]).catch(() => {}); - } + await writeFile(filename, config_text, { encoding: "utf8" }); + debug(logger, "Wrote config:", filename); - return true; - } catch (err) { - debug(logger, `Could not write ${filename}:`, err.message); - throw new errs.ConfigurationError(err.message); + if (process.env.DISABLE_NGINX_BEAUTIFIER === "false") { + await utils.execFile("nginxbeautifier", ["-s", "2", filename]).catch(() => {}); } + + return true; }, /** diff --git a/backend/internal/proxy-host-access-list.js b/backend/internal/proxy-host-access-list.js index 164ea9268f..be90a2acf3 100644 --- a/backend/internal/proxy-host-access-list.js +++ b/backend/internal/proxy-host-access-list.js @@ -454,17 +454,6 @@ const internalProxyHostAccessList = { proxyHost.npmplus_access_list_ids = []; } - // fallback from old column (only if needed) - if ( - typeof proxyHost.npmplus_access_list_type === "undefined" && - proxyHost.npmplus_access_list_ids.length === 0 && - proxyHost.access_list_id && - proxyHost.access_list_id !== 0 - ) { - proxyHost.npmplus_access_list_ids = [proxyHost.access_list_id]; - proxyHost.npmplus_access_list_type = "custom"; - } - // ensure type exists if (!proxyHost.npmplus_access_list_type) { proxyHost.npmplus_access_list_type = "public"; diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js index 4af47b29ff..975745b640 100644 --- a/backend/internal/proxy-host.js +++ b/backend/internal/proxy-host.js @@ -1,9 +1,7 @@ import net from "node:net"; -import _ from "lodash"; import errs from "../lib/error.js"; import { castJsonIfNeed } from "../lib/helpers.js"; import { assertPrivilegedNginxFields } from "../lib/nginx-privilege.js"; -import utils from "../lib/utils.js"; import proxyHostModel from "../models/proxy_host.js"; import internalAuditLog from "./audit-log.js"; import internalCertificate from "./certificate.js"; @@ -11,8 +9,6 @@ import internalHost from "./host.js"; import internalNginx from "./nginx.js"; import internalProxyHostAccessList from "./proxy-host-access-list.js"; -const omissions = () => ["is_deleted", "owner.is_deleted", "certificate.is_deleted"]; - // tcp probe of the forward destination; nginx -t never checks it, but a // destination nothing listens on means the host serves errors, so the // dashboard dot should say so. Returns null when there is no tcp @@ -40,14 +36,15 @@ const probeForwardDestination = (scheme, host, port) => { // configure nginx, then probe and persist reachability into the host meta so // the create/update/enable responses and the list all carry the same state const configureWithReachability = async (row) => { - const meta = await internalNginx.configure(proxyHostModel, "proxy_host", row); + const status = await internalNginx.configure(proxyHostModel, "proxy_host", row); const reach = await probeForwardDestination(row.forward_scheme, row.forward_host, row.forward_port); if (reach) { - meta.reach_ok = reach.ok; - meta.reach_err = reach.err; - await proxyHostModel.query().where("id", row.id).patch({ meta }); + await proxyHostModel + .query() + .where("id", row.id) + .patch({ meta: { reach_ok: reach.ok, reach_err: reach.err } }); } - return meta; + return status; }; const internalProxyHost = { @@ -66,6 +63,9 @@ const internalProxyHost = { // a delegated manager may only attach a certificate they can see await internalCertificate.get(access, { id: thisData.certificate_id }); } + if (Number(thisData.npmplus_mtls_certificate_id) > 0) { + await internalCertificate.get(access, { id: thisData.npmplus_mtls_certificate_id }); + } access.can("proxy_hosts:manage"); await assertPrivilegedNginxFields(access, thisData); @@ -85,51 +85,51 @@ const internalProxyHost = { thisData = internalProxyHostAccessList.cleanAccessListTypes(thisData); await internalProxyHostAccessList.validateAccessLists(access, thisData); - const createdRow = utils.omitRow(omissions())( - await proxyHostModel.transaction(async (trx) => { - const insertedRow = await proxyHostModel.query(trx).insertAndFetch(thisData); + const createdRow = await proxyHostModel.transaction(async (trx) => { + const insertedRow = await proxyHostModel.query(trx).insertAndFetch(thisData); - const relationRows = internalProxyHostAccessList.getAccessListRelationRows(insertedRow.id, thisData); - if (relationRows.length > 0) { - await trx("npmplus_proxy_host_access_list").insert(relationRows); - } + const relationRows = internalProxyHostAccessList.getAccessListRelationRows(insertedRow.id, thisData); + if (relationRows.length > 0) { + await trx("npmplus_proxy_host_access_list").insert(relationRows); + } - return insertedRow; - }), - ); + return insertedRow; + }); - if (createCertificate) { - const cert = await internalCertificate.createQuickCertificate(access, thisData); + let savedRow; + try { + if (createCertificate) { + // update host with cert id + await proxyHostModel + .query() + .where("id", createdRow.id) + .patch({ certificate_id: (await internalCertificate.createQuickCertificate(access, thisData)).id }); + } - // update host with cert id - await internalProxyHost.update(access, { + const fetchedRow = await internalProxyHost.get(access, { id: createdRow.id, - certificate_id: cert.id, + expand: ["certificate", "access_lists.[clients,items]"], }); - } - const fetchedRow = await internalProxyHost.get(access, { - id: createdRow.id, - expand: ["certificate", "owner", "access_lists.[clients,items]"], - }); - - const row = await internalProxyHostAccessList.populateLocationAccessLists( - internalProxyHostAccessList.cleanAccessListTypes(fetchedRow), - ); - - // Configure nginx - row.meta = await configureWithReachability(row); + const row = await internalProxyHostAccessList.populateLocationAccessLists( + internalProxyHostAccessList.cleanAccessListTypes(fetchedRow), + ); - // Add to audit log - thisData.meta = { ...thisData.meta, ...row.meta }; - await internalAuditLog.add(access, { - action: "created", - object_type: "proxy-host", - object_id: row.id, - meta: thisData, - }); + // Configure nginx + await configureWithReachability(row); + } finally { + savedRow = await internalProxyHost.get(access, { id: createdRow.id }); + + // Add to audit log + await internalAuditLog.add(access, { + action: "created", + object_type: "proxy-host", + object_id: savedRow.id, + meta: savedRow, + }); + } - return internalProxyHostAccessList.maskAccessListItems(row); + return savedRow; }, /** @@ -148,6 +148,9 @@ const internalProxyHost = { // a delegated manager may only attach a certificate they can see await internalCertificate.get(access, { id: thisData.certificate_id }); } + if (Number(thisData.npmplus_mtls_certificate_id) > 0) { + await internalCertificate.get(access, { id: thisData.npmplus_mtls_certificate_id }); + } access.can("proxy_hosts:manage"); @@ -176,16 +179,14 @@ const internalProxyHost = { if (createCertificate) { const cert = await internalCertificate.createQuickCertificate(access, { + ...thisData, domain_names: thisData.domain_names || existingRow.domain_names, - meta: { ...existingRow.meta, ...thisData.meta }, }); // update host with cert id thisData.certificate_id = cert.id; } - // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. - thisData = { domain_names: existingRow.domain_names, ...thisData }; thisData = internalHost.cleanSslHstsData(createCertificate, thisData, existingRow); thisData = internalProxyHostAccessList.cleanAccessListTypes(thisData); await internalProxyHostAccessList.validateAccessLists(access, thisData); @@ -198,36 +199,35 @@ const internalProxyHost = { return patchResult; }); - // Add to audit log - await internalAuditLog.add(access, { - action: "updated", - object_type: "proxy-host", - object_id: existingRow.id, - meta: thisData, - }); - - const fetchedRow = await internalProxyHost.get(access, { - id: thisData.id, - expand: ["certificate", "owner", "access_lists.[clients,items]"], - }); + let savedRow; + try { + const fetchedRow = await internalProxyHost.get(access, { + id: thisData.id, + expand: ["certificate", "access_lists.[clients,items]"], + }); - const row = await internalProxyHostAccessList.populateLocationAccessLists( - internalProxyHostAccessList.cleanAccessListTypes(fetchedRow), - ); + const row = await internalProxyHostAccessList.populateLocationAccessLists( + internalProxyHostAccessList.cleanAccessListTypes(fetchedRow), + ); - if (!row.enabled) { // No need to add nginx config if host is disabled - return internalProxyHostAccessList.maskAccessListItems( - _.omit(internalHost.cleanRowCertificateMeta(row), omissions()), - ); + if (row.enabled) { + // Configure nginx + await configureWithReachability(row); + } + } finally { + savedRow = await internalProxyHost.get(access, { id: thisData.id }); + + // Add to audit log + await internalAuditLog.add(access, { + action: "updated", + object_type: "proxy-host", + object_id: savedRow.id, + meta: savedRow, + }); } - // Configure nginx - row.meta = await configureWithReachability(row); - - return internalProxyHostAccessList.maskAccessListItems( - _.omit(internalHost.cleanRowCertificateMeta(row), omissions()), - ); + return savedRow; }, /** @@ -235,7 +235,6 @@ const internalProxyHost = { * @param {Object} data * @param {Number} data.id * @param {Array} [data.expand] - * @param {Array} [data.omit] * @return {Promise} */ get: async (access, data) => { @@ -247,7 +246,7 @@ const internalProxyHost = { .query() .where("is_deleted", 0) .andWhere("id", thisData.id) - .allowGraph(proxyHostModel.defaultAllowGraph) + .allowGraph("[access_lists.[clients,items],certificate]") .first(); if (access.visibility !== "all") { @@ -258,19 +257,12 @@ const internalProxyHost = { query.withGraphFetched(`[${thisData.expand.join(", ")}]`); } - const row = utils.omitRow(omissions())(await query); + const row = await query; if (!row?.id) { throw new errs.ItemNotFoundError(thisData.id); } - const thisRow = internalHost.cleanRowCertificateMeta(internalProxyHostAccessList.cleanAccessListTypes(row)); - - // Custom omissions - if (typeof thisData.omit !== "undefined" && thisData.omit !== null) { - return _.omit(thisRow, thisData.omit); - } - - return thisRow; + return internalProxyHostAccessList.cleanAccessListTypes(row); }, /** @@ -294,21 +286,23 @@ const internalProxyHost = { }), ); - // Delete Nginx Config - await internalNginx.deleteConfig("proxy_host", row); - - await internalProxyHostAccessList.delete(row); - await internalNginx.reload(); - - // Add to audit log - await internalAuditLog.add(access, { - action: "deleted", - object_type: "proxy-host", - object_id: row.id, - meta: _.omit(row, omissions()), - }); + try { + // Delete Nginx Config + await internalNginx.deleteConfig("proxy_host", row); + + await internalProxyHostAccessList.delete(row); + await internalNginx.reload(); + } finally { + // Add to audit log + await internalAuditLog.add(access, { + action: "deleted", + object_type: "proxy-host", + object_id: row.id, + meta: row, + }); + } - return true; + return row; }, /** @@ -323,7 +317,7 @@ const internalProxyHost = { const row = await internalProxyHost.get(access, { id: data.id, - expand: ["certificate", "owner", "access_lists.[clients,items]"], + expand: ["certificate", "access_lists.[clients,items]"], }); if (!row?.id) { throw new errs.ItemNotFoundError(data.id); @@ -346,22 +340,27 @@ const internalProxyHost = { enabled: 1, }); - // Configure nginx - row.meta = await configureWithReachability( - await internalProxyHostAccessList.populateLocationAccessLists( - internalProxyHostAccessList.cleanAccessListTypes(row), - ), - ); - - // Add to audit log - await internalAuditLog.add(access, { - action: "enabled", - object_type: "proxy-host", - object_id: row.id, - meta: _.omit(internalProxyHostAccessList.maskAccessListItems(row), omissions()), - }); + let savedRow; + try { + // Configure nginx + await configureWithReachability( + await internalProxyHostAccessList.populateLocationAccessLists( + internalProxyHostAccessList.cleanAccessListTypes(row), + ), + ); + } finally { + savedRow = await internalProxyHost.get(access, { id: row.id }); + + // Add to audit log + await internalAuditLog.add(access, { + action: "enabled", + object_type: "proxy-host", + object_id: row.id, + meta: savedRow, + }); + } - return true; + return savedRow; }, /** @@ -388,19 +387,24 @@ const internalProxyHost = { enabled: 0, }); - // Delete Nginx Config - await internalNginx.deleteConfig("proxy_host", row); - await internalNginx.reload(); - - // Add to audit log - await internalAuditLog.add(access, { - action: "disabled", - object_type: "proxy-host", - object_id: row.id, - meta: _.omit(row, omissions()), - }); + let savedRow; + try { + // Delete Nginx Config + await internalNginx.deleteConfig("proxy_host", row); + await internalNginx.reload(); + } finally { + savedRow = await internalProxyHost.get(access, { id: row.id }); + + // Add to audit log + await internalAuditLog.add(access, { + action: "disabled", + object_type: "proxy-host", + object_id: row.id, + meta: savedRow, + }); + } - return true; + return savedRow; }, /** @@ -418,7 +422,7 @@ const internalProxyHost = { .query() .where("is_deleted", 0) .groupBy("id") - .allowGraph(proxyHostModel.defaultAllowGraph) + .allowGraph("[owner,access_lists,certificate]") .orderBy(castJsonIfNeed("domain_names"), "ASC"); if (access.visibility !== "all") { @@ -436,14 +440,7 @@ const internalProxyHost = { query.withGraphFetched(`[${expand.join(", ")}]`); } - const rows = utils - .omitRows(omissions())(await query) - .map((row) => internalProxyHostAccessList.cleanAccessListTypes(row)); - if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) { - return internalHost.cleanAllRowsCertificateMeta(rows); - } - - return rows; + return (await query).map((row) => internalProxyHostAccessList.cleanAccessListTypes(row)); }, /** diff --git a/backend/internal/redirection-host.js b/backend/internal/redirection-host.js index 97dedeb88f..35cfb0cf86 100644 --- a/backend/internal/redirection-host.js +++ b/backend/internal/redirection-host.js @@ -1,16 +1,12 @@ -import _ from "lodash"; import errs from "../lib/error.js"; import { castJsonIfNeed } from "../lib/helpers.js"; import { assertPrivilegedNginxFields } from "../lib/nginx-privilege.js"; -import utils from "../lib/utils.js"; import redirectionHostModel from "../models/redirection_host.js"; import internalAuditLog from "./audit-log.js"; import internalCertificate from "./certificate.js"; import internalHost from "./host.js"; import internalNginx from "./nginx.js"; -const omissions = () => ["is_deleted", "owner.is_deleted", "certificate.is_deleted"]; - const internalRedirectionHost = { /** * @param {Access} access @@ -26,6 +22,9 @@ const internalRedirectionHost = { } else if (Number(thisData.certificate_id) > 0) { await internalCertificate.get(access, { id: thisData.certificate_id }); } + if (Number(thisData.npmplus_mtls_certificate_id) > 0) { + await internalCertificate.get(access, { id: thisData.npmplus_mtls_certificate_id }); + } access.can("redirection_hosts:manage"); await assertPrivilegedNginxFields(access, thisData); @@ -43,36 +42,38 @@ const internalRedirectionHost = { thisData.owner_user_id = access.token.getUserId(1); thisData = internalHost.cleanSslHstsData(createCertificate, thisData); - const createdRow = utils.omitRow(omissions())(await redirectionHostModel.query().insertAndFetch(thisData)); + const createdRow = await redirectionHostModel.query().insertAndFetch(thisData); - if (createCertificate) { - const cert = await internalCertificate.createQuickCertificate(access, thisData); + let savedRow; + try { + if (createCertificate) { + // update host with cert id + await redirectionHostModel + .query() + .where("id", createdRow.id) + .patch({ certificate_id: (await internalCertificate.createQuickCertificate(access, thisData)).id }); + } - // update host with cert id - await internalRedirectionHost.update(access, { + const row = await internalRedirectionHost.get(access, { id: createdRow.id, - certificate_id: cert.id, + expand: ["certificate"], }); - } - const row = await internalRedirectionHost.get(access, { - id: createdRow.id, - expand: ["certificate", "owner"], - }); - - // Configure nginx - await internalNginx.configure(redirectionHostModel, "redirection_host", row); - - // Add to audit log - thisData.meta = { ...thisData.meta, ...row.meta }; - await internalAuditLog.add(access, { - action: "created", - object_type: "redirection-host", - object_id: row.id, - meta: thisData, - }); + // Configure nginx + await internalNginx.configure(redirectionHostModel, "redirection_host", row); + } finally { + savedRow = await internalRedirectionHost.get(access, { id: createdRow.id }); + + // Add to audit log + await internalAuditLog.add(access, { + action: "created", + object_type: "redirection-host", + object_id: savedRow.id, + meta: savedRow, + }); + } - return row; + return savedRow; }, /** @@ -90,6 +91,9 @@ const internalRedirectionHost = { } else if (Number(thisData.certificate_id) > 0) { await internalCertificate.get(access, { id: thisData.certificate_id }); } + if (Number(thisData.npmplus_mtls_certificate_id) > 0) { + await internalCertificate.get(access, { id: thisData.npmplus_mtls_certificate_id }); + } access.can("redirection_hosts:manage"); @@ -118,42 +122,43 @@ const internalRedirectionHost = { if (createCertificate) { const cert = await internalCertificate.createQuickCertificate(access, { + ...thisData, domain_names: thisData.domain_names || existingRow.domain_names, - meta: { ...existingRow.meta, ...thisData.meta }, }); // update host with cert id thisData.certificate_id = cert.id; } - // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. - thisData = { domain_names: existingRow.domain_names, ...thisData }; thisData = internalHost.cleanSslHstsData(createCertificate, thisData, existingRow); await redirectionHostModel.query().where({ id: thisData.id }).patch(thisData); - // Add to audit log - await internalAuditLog.add(access, { - action: "updated", - object_type: "redirection-host", - object_id: existingRow.id, - meta: thisData, - }); - - const row = await internalRedirectionHost.get(access, { - id: thisData.id, - expand: ["certificate", "owner"], - }); + let savedRow; + try { + const row = await internalRedirectionHost.get(access, { + id: thisData.id, + expand: ["certificate"], + }); - if (!row.enabled) { // No need to add nginx config if host is disabled - return _.omit(internalHost.cleanRowCertificateMeta(row), omissions()); + if (row.enabled) { + // Configure nginx + await internalNginx.configure(redirectionHostModel, "redirection_host", row); + } + } finally { + savedRow = await internalRedirectionHost.get(access, { id: thisData.id }); + + // Add to audit log + await internalAuditLog.add(access, { + action: "updated", + object_type: "redirection-host", + object_id: savedRow.id, + meta: savedRow, + }); } - // Configure nginx - row.meta = await internalNginx.configure(redirectionHostModel, "redirection_host", row); - - return _.omit(internalHost.cleanRowCertificateMeta(row), omissions()); + return savedRow; }, /** @@ -161,7 +166,6 @@ const internalRedirectionHost = { * @param {Object} data * @param {Number} data.id * @param {Array} [data.expand] - * @param {Array} [data.omit] * @return {Promise} */ get: async (access, data) => { @@ -173,7 +177,7 @@ const internalRedirectionHost = { .query() .where("is_deleted", 0) .andWhere("id", thisData.id) - .allowGraph(redirectionHostModel.defaultAllowGraph) + .allowGraph("[certificate]") .first(); if (access.visibility !== "all") { @@ -184,19 +188,12 @@ const internalRedirectionHost = { query.withGraphFetched(`[${thisData.expand.join(", ")}]`); } - const row = utils.omitRow(omissions())(await query); + const row = await query; if (!row?.id) { throw new errs.ItemNotFoundError(thisData.id); } - const thisRow = internalHost.cleanRowCertificateMeta(row); - - // Custom omissions - if (typeof thisData.omit !== "undefined" && thisData.omit !== null) { - return _.omit(thisRow, thisData.omit); - } - - return thisRow; + return row; }, /** @@ -218,19 +215,21 @@ const internalRedirectionHost = { is_deleted: 1, }); - // Delete Nginx Config - await internalNginx.deleteConfig("redirection_host", row); - await internalNginx.reload(); - - // Add to audit log - await internalAuditLog.add(access, { - action: "deleted", - object_type: "redirection-host", - object_id: row.id, - meta: _.omit(row, omissions()), - }); + try { + // Delete Nginx Config + await internalNginx.deleteConfig("redirection_host", row); + await internalNginx.reload(); + } finally { + // Add to audit log + await internalAuditLog.add(access, { + action: "deleted", + object_type: "redirection-host", + object_id: row.id, + meta: row, + }); + } - return true; + return row; }, /** @@ -245,7 +244,7 @@ const internalRedirectionHost = { const row = await internalRedirectionHost.get(access, { id: data.id, - expand: ["certificate", "owner"], + expand: ["certificate"], }); if (!row?.id) { throw new errs.ItemNotFoundError(data.id); @@ -268,18 +267,23 @@ const internalRedirectionHost = { enabled: 1, }); - // Configure nginx - await internalNginx.configure(redirectionHostModel, "redirection_host", row); - - // Add to audit log - await internalAuditLog.add(access, { - action: "enabled", - object_type: "redirection-host", - object_id: row.id, - meta: _.omit(row, omissions()), - }); + let savedRow; + try { + // Configure nginx + await internalNginx.configure(redirectionHostModel, "redirection_host", row); + } finally { + savedRow = await internalRedirectionHost.get(access, { id: row.id }); + + // Add to audit log + await internalAuditLog.add(access, { + action: "enabled", + object_type: "redirection-host", + object_id: row.id, + meta: savedRow, + }); + } - return true; + return savedRow; }, /** @@ -306,19 +310,24 @@ const internalRedirectionHost = { enabled: 0, }); - // Delete Nginx Config - await internalNginx.deleteConfig("redirection_host", row); - await internalNginx.reload(); - - // Add to audit log - await internalAuditLog.add(access, { - action: "disabled", - object_type: "redirection-host", - object_id: row.id, - meta: _.omit(row, omissions()), - }); + let savedRow; + try { + // Delete Nginx Config + await internalNginx.deleteConfig("redirection_host", row); + await internalNginx.reload(); + } finally { + savedRow = await internalRedirectionHost.get(access, { id: row.id }); + + // Add to audit log + await internalAuditLog.add(access, { + action: "disabled", + object_type: "redirection-host", + object_id: row.id, + meta: savedRow, + }); + } - return true; + return savedRow; }, /** @@ -336,7 +345,7 @@ const internalRedirectionHost = { .query() .where("is_deleted", 0) .groupBy("id") - .allowGraph(redirectionHostModel.defaultAllowGraph) + .allowGraph("[owner,certificate]") .orderBy(castJsonIfNeed("domain_names"), "ASC"); if (access.visibility !== "all") { @@ -354,12 +363,7 @@ const internalRedirectionHost = { query.withGraphFetched(`[${expand.join(", ")}]`); } - const rows = utils.omitRows(omissions())(await query); - if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) { - return internalHost.cleanAllRowsCertificateMeta(rows); - } - - return rows; + return await query; }, /** diff --git a/backend/internal/report.js b/backend/internal/report.js index 2e2bbc7030..5b26bbb558 100644 --- a/backend/internal/report.js +++ b/backend/internal/report.js @@ -1,3 +1,5 @@ +import internalAccessList from "./access-list.js"; +import internalCertificate from "./certificate.js"; import internalDeadHost from "./dead-host.js"; import internalProxyHost from "./proxy-host.js"; import internalRedirectionHost from "./redirection-host.js"; @@ -11,14 +13,16 @@ const internalReport = { getHostsReport: async (access) => { const userId = access.token.getUserId(1); - const [proxy, redirection, stream, dead] = await Promise.all([ + const [proxy, redirection, stream, dead, access_list, certificate] = await Promise.all([ internalProxyHost.getCount(userId, access.visibility), internalRedirectionHost.getCount(userId, access.visibility), internalStream.getCount(userId, access.visibility), internalDeadHost.getCount(userId, access.visibility), + internalAccessList.getCount(userId, access.visibility), + internalCertificate.getCount(userId, access.visibility), ]); - return { proxy, redirection, stream, dead }; + return { proxy, redirection, stream, dead, access_list, certificate }; }, }; diff --git a/backend/internal/setting.js b/backend/internal/setting.js index cf51ed676e..e983df0323 100644 --- a/backend/internal/setting.js +++ b/backend/internal/setting.js @@ -31,10 +31,7 @@ const internalSetting = { await internalAuditLog.add(access, { action: "updated", object_type: "setting", - meta: { - id: row.id, - value: row.value, - }, + meta: row, }); if (row.id === "default-site") { // write the html if we need to diff --git a/backend/internal/stream.js b/backend/internal/stream.js index f8436a1e5e..0bf34fc8b3 100644 --- a/backend/internal/stream.js +++ b/backend/internal/stream.js @@ -1,16 +1,11 @@ -import _ from "lodash"; import errs from "../lib/error.js"; import { castJsonIfNeed } from "../lib/helpers.js"; import { assertPrivilegedNginxFields } from "../lib/nginx-privilege.js"; -import utils from "../lib/utils.js"; import streamModel from "../models/stream.js"; import internalAuditLog from "./audit-log.js"; import internalCertificate from "./certificate.js"; -import internalHost from "./host.js"; import internalNginx from "./nginx.js"; -const omissions = () => ["is_deleted", "owner.is_deleted", "certificate.is_deleted"]; - /** * A stream port must be a single valid port, must not collide with the ports * NPMplus listens on itself and must not be taken by another stream @@ -51,6 +46,9 @@ const internalStream = { } else if (Number(thisData.certificate_id) > 0) { await internalCertificate.get(access, { id: thisData.certificate_id }); } + if (Number(thisData.npmplus_mtls_certificate_id) > 0) { + await internalCertificate.get(access, { id: thisData.npmplus_mtls_certificate_id }); + } access.can("streams:manage"); await assertPrivilegedNginxFields(access, thisData); @@ -59,36 +57,38 @@ const internalStream = { thisData.owner_user_id = access.token.getUserId(1); - const createdRow = utils.omitRow(omissions())(await streamModel.query().insertAndFetch(thisData)); + const createdRow = await streamModel.query().insertAndFetch(thisData); - if (createCertificate) { - const cert = await internalCertificate.createQuickCertificate(access, thisData); + let savedRow; + try { + if (createCertificate) { + // update host with cert id + await streamModel + .query() + .where("id", createdRow.id) + .patch({ certificate_id: (await internalCertificate.createQuickCertificate(access, thisData)).id }); + } - // update host with cert id - await internalStream.update(access, { + const row = await internalStream.get(access, { id: createdRow.id, - certificate_id: cert.id, + expand: ["certificate"], }); - } - const row = await internalStream.get(access, { - id: createdRow.id, - expand: ["certificate", "owner"], - }); - - // Configure nginx - await internalNginx.configure(streamModel, "stream", row); - - // Add to audit log - thisData.meta = { ...thisData.meta, ...row.meta }; - await internalAuditLog.add(access, { - action: "created", - object_type: "stream", - object_id: row.id, - meta: thisData, - }); + // Configure nginx + await internalNginx.configure(streamModel, "stream", row); + } finally { + savedRow = await internalStream.get(access, { id: createdRow.id }); + + // Add to audit log + await internalAuditLog.add(access, { + action: "created", + object_type: "stream", + object_id: savedRow.id, + meta: savedRow, + }); + } - return row; + return savedRow; }, /** @@ -106,6 +106,9 @@ const internalStream = { } else if (Number(thisData.certificate_id) > 0) { await internalCertificate.get(access, { id: thisData.certificate_id }); } + if (Number(thisData.npmplus_mtls_certificate_id) > 0) { + await internalCertificate.get(access, { id: thisData.npmplus_mtls_certificate_id }); + } access.can("streams:manage"); @@ -124,8 +127,8 @@ const internalStream = { if (createCertificate) { const cert = await internalCertificate.createQuickCertificate(access, { + ...thisData, domain_names: thisData.domain_names || existingRow.domain_names, - meta: { ...existingRow.meta, ...thisData.meta }, }); // update host with cert id @@ -134,28 +137,31 @@ const internalStream = { await streamModel.query().where({ id: thisData.id }).patch(thisData); - // Add to audit log - await internalAuditLog.add(access, { - action: "updated", - object_type: "stream", - object_id: existingRow.id, - meta: thisData, - }); - - const row = await internalStream.get(access, { - id: thisData.id, - expand: ["certificate", "owner"], - }); + let savedRow; + try { + const row = await internalStream.get(access, { + id: thisData.id, + expand: ["certificate"], + }); - if (!row.enabled) { // No need to add nginx config if host is disabled - return _.omit(internalHost.cleanRowCertificateMeta(row), omissions()); + if (row.enabled) { + // Configure nginx + await internalNginx.configure(streamModel, "stream", row); + } + } finally { + savedRow = await internalStream.get(access, { id: thisData.id }); + + // Add to audit log + await internalAuditLog.add(access, { + action: "updated", + object_type: "stream", + object_id: savedRow.id, + meta: savedRow, + }); } - // Configure nginx - row.meta = await internalNginx.configure(streamModel, "stream", row); - - return _.omit(internalHost.cleanRowCertificateMeta(row), omissions()); + return savedRow; }, /** @@ -163,7 +169,6 @@ const internalStream = { * @param {Object} data * @param {Number} data.id * @param {Array} [data.expand] - * @param {Array} [data.omit] * @return {Promise} */ get: async (access, data) => { @@ -175,7 +180,7 @@ const internalStream = { .query() .where("is_deleted", 0) .andWhere("id", thisData.id) - .allowGraph(streamModel.defaultAllowGraph) + .allowGraph("[certificate]") .first(); if (access.visibility !== "all") { @@ -186,19 +191,12 @@ const internalStream = { query.withGraphFetched(`[${thisData.expand.join(", ")}]`); } - const row = utils.omitRow(omissions())(await query); + const row = await query; if (!row?.id) { throw new errs.ItemNotFoundError(thisData.id); } - const thisRow = internalHost.cleanRowCertificateMeta(row); - - // Custom omissions - if (typeof thisData.omit !== "undefined" && thisData.omit !== null) { - return _.omit(thisRow, thisData.omit); - } - - return thisRow; + return row; }, /** @@ -220,19 +218,21 @@ const internalStream = { is_deleted: 1, }); - // Delete Nginx Config - await internalNginx.deleteConfig("stream", row); - await internalNginx.reload(); - - // Add to audit log - await internalAuditLog.add(access, { - action: "deleted", - object_type: "stream", - object_id: row.id, - meta: _.omit(row, omissions()), - }); + try { + // Delete Nginx Config + await internalNginx.deleteConfig("stream", row); + await internalNginx.reload(); + } finally { + // Add to audit log + await internalAuditLog.add(access, { + action: "deleted", + object_type: "stream", + object_id: row.id, + meta: row, + }); + } - return true; + return row; }, /** @@ -247,7 +247,7 @@ const internalStream = { const row = await internalStream.get(access, { id: data.id, - expand: ["certificate", "owner"], + expand: ["certificate"], }); if (!row?.id) { throw new errs.ItemNotFoundError(data.id); @@ -262,18 +262,23 @@ const internalStream = { enabled: 1, }); - // Configure nginx - await internalNginx.configure(streamModel, "stream", row); - - // Add to audit log - await internalAuditLog.add(access, { - action: "enabled", - object_type: "stream", - object_id: row.id, - meta: _.omit(row, omissions()), - }); + let savedRow; + try { + // Configure nginx + await internalNginx.configure(streamModel, "stream", row); + } finally { + savedRow = await internalStream.get(access, { id: row.id }); + + // Add to audit log + await internalAuditLog.add(access, { + action: "enabled", + object_type: "stream", + object_id: row.id, + meta: savedRow, + }); + } - return true; + return savedRow; }, /** @@ -300,19 +305,24 @@ const internalStream = { enabled: 0, }); - // Delete Nginx Config - await internalNginx.deleteConfig("stream", row); - await internalNginx.reload(); - - // Add to audit log - await internalAuditLog.add(access, { - action: "disabled", - object_type: "stream", - object_id: row.id, - meta: _.omit(row, omissions()), - }); + let savedRow; + try { + // Delete Nginx Config + await internalNginx.deleteConfig("stream", row); + await internalNginx.reload(); + } finally { + savedRow = await internalStream.get(access, { id: row.id }); + + // Add to audit log + await internalAuditLog.add(access, { + action: "disabled", + object_type: "stream", + object_id: row.id, + meta: savedRow, + }); + } - return true; + return savedRow; }, /** @@ -330,7 +340,7 @@ const internalStream = { .query() .where("is_deleted", 0) .groupBy("id") - .allowGraph(streamModel.defaultAllowGraph) + .allowGraph("[owner,certificate]") .orderBy("incoming_port", "ASC"); if (access.visibility !== "all") { @@ -351,12 +361,7 @@ const internalStream = { query.withGraphFetched(`[${expand.join(", ")}]`); } - const rows = utils.omitRows(omissions())(await query); - if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) { - return internalHost.cleanAllRowsCertificateMeta(rows); - } - - return rows; + return await query; }, /** diff --git a/backend/internal/token.js b/backend/internal/token.js index bed6a87db9..df4e6a7206 100644 --- a/backend/internal/token.js +++ b/backend/internal/token.js @@ -59,7 +59,7 @@ export default { const user = await userModel .query() - .where("email", data.identity.toLowerCase().trim()) + .where("email", data.identity.toLowerCase()) .andWhere("is_deleted", 0) .andWhere("is_disabled", 0) .first(); diff --git a/backend/internal/totp.js b/backend/internal/totp.js index 43bd693cb1..e36429201b 100644 --- a/backend/internal/totp.js +++ b/backend/internal/totp.js @@ -76,9 +76,7 @@ const internalTotp = { throw new errs.ValidationError("TOTP setup has expired"); } - const codeTrim = code.trim(); - - const result = await verify({ token: codeTrim, secret: pending.secret }); + const result = await verify({ token: code, secret: pending.secret }); if (!result.valid) { throw new errs.ValidationError("Invalid verification code"); } diff --git a/backend/internal/user.js b/backend/internal/user.js index 48169e8a47..ff16c59ebf 100644 --- a/backend/internal/user.js +++ b/backend/internal/user.js @@ -1,10 +1,8 @@ import crypto from "node:crypto"; import { rm, writeFile } from "node:fs/promises"; import process from "node:process"; -import _ from "lodash"; import { fetchWithTimeout, readBoundedBuffer } from "../lib/bounded-fetch.js"; import errs from "../lib/error.js"; -import utils from "../lib/utils.js"; import { gravatar as logger } from "../logger.js"; import authModel from "../models/auth.js"; import userModel from "../models/user.js"; @@ -13,16 +11,6 @@ import pjson from "../package.json" with { type: "json" }; import internalAuditLog from "./audit-log.js"; import internalToken from "./token.js"; -const omissions = () => [ - "is_deleted", - "nickname", - "npmplus_token_valid_after", - "permissions.id", - "permissions.user_id", - "permissions.created_on", - "permissions.modified_on", -]; - const avatarExts = ["png", "jpg", "gif", "webp"]; const avatarExt = (b) => { @@ -30,8 +18,9 @@ const avatarExt = (b) => { if (b.subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex"))) return "png"; if (b.subarray(0, 3).equals(Buffer.from("ffd8ff", "hex"))) return "jpg"; if (b.subarray(0, 4).toString("latin1") === "GIF8") return "gif"; - if (b.subarray(0, 4).toString("latin1") === "RIFF" && b.subarray(8, 12).toString("latin1") === "WEBP") + if (b.subarray(0, 4).toString("latin1") === "RIFF" && b.subarray(8, 12).toString("latin1") === "WEBP") { return "webp"; + } return null; }; @@ -115,7 +104,7 @@ const internalUser = { data.avatar = data.avatar || ""; data.roles = data.roles || []; - data.email = data.email.toLowerCase().trim(); + data.email = data.email.toLowerCase(); if (typeof data.is_disabled !== "undefined") { data.is_disabled = data.is_disabled ? 1 : 0; @@ -127,7 +116,7 @@ const internalUser = { throw new errs.ValidationError(`Email address already in use - ${data.email}`); } - let user = utils.omitRow(omissions())(await userModel.query().insertAndFetch(data)); + let user = await userModel.query().insertAndFetch(data); if (auth) { await authModel.query().insert({ user_id: user.id, @@ -155,7 +144,7 @@ const internalUser = { .query() .patchAndFetchById(user.id, { avatar: await internalUser.fetchGravatar(user.id, user.email, user.name) }); - user = await internalUser.get(access, { id: user.id, expand: ["permissions"] }); + user = await internalUser.get(access, { id: user.id }); await internalAuditLog.add(access, { action: "created", @@ -178,7 +167,16 @@ const internalUser = { await rmAvatars("avatar", user.id); await writeFile(`/data/npmplus/avatar/${user.id}.${ext}`, file.buffer); await userModel.query().patchAndFetchById(user.id, { avatar: `/images/avatar/${user.id}.${ext}` }); - return internalUser.update(access, { id: user.id }); + const savedUser = await internalUser.get(access, { id: user.id }); + + await internalAuditLog.add(access, { + action: "updated", + object_type: "user", + object_id: savedUser.id, + meta: { ...savedUser, avatar_changed: true }, + }); + + return savedUser; }, deleteAvatar: async (access, id) => { @@ -211,7 +209,7 @@ const internalUser = { const existingUser = await internalUser.get(access, { id: data.id }); // 2. if email is to be changed, find other users with that email if (typeof data.email !== "undefined") { - data.email = data.email.toLowerCase().trim(); + data.email = data.email.toLowerCase(); if (existingUser.email !== data.email && !(await internalUser.isEmailAvailable(data.email, data.id))) { throw new errs.ValidationError(`Email address already in use - ${data.email}`); @@ -242,7 +240,7 @@ const internalUser = { action: "updated", object_type: "user", object_id: user.id, - meta: { ...data, id: user.id, name: user.name }, + meta: user, }); return user; @@ -258,9 +256,7 @@ const internalUser = { get: async (access, data) => { const thisData = data || {}; - if (typeof thisData.id === "undefined" || !thisData.id) { - thisData.id = access.token.getUserId(0); - } + thisData.id ||= access.token.getUserId(0); access.canUser(thisData.id); @@ -275,7 +271,7 @@ const internalUser = { query.withGraphFetched(`[${thisData.expand.join(", ")}]`); } - const row = utils.omitRow(omissions())(await query); + const row = await query; if (!row?.id) { throw new errs.ItemNotFoundError(thisData.id); } @@ -300,7 +296,7 @@ const internalUser = { * @param user_id */ isEmailAvailable: async (email, user_id) => { - const query = userModel.query().where("email", "=", email.toLowerCase().trim()).where("is_deleted", 0).first(); + const query = userModel.query().where("email", "=", email.toLowerCase()).where("is_deleted", 0).first(); if (typeof user_id !== "undefined") { query.where("id", "!=", user_id); @@ -339,10 +335,10 @@ const internalUser = { action: "deleted", object_type: "user", object_id: user.id, - meta: _.omit(user, omissions()), + meta: user, }); - return true; + return user; }, /** @@ -372,18 +368,12 @@ const internalUser = { * All users * * @param {Access} access - * @param {Array} [expand] * @param {String} [search_query] * @returns {Promise} */ - getAll: async (access, expand, search_query) => { + getAll: async (access, search_query) => { access.canAdmin(); - const query = userModel - .query() - .where("is_deleted", 0) - .groupBy("id") - .allowGraph("[permissions]") - .orderBy("name", "ASC"); + const query = userModel.query().where("is_deleted", 0).groupBy("id").orderBy("name", "ASC"); // Query is used for searching if (typeof search_query === "string") { @@ -392,12 +382,7 @@ const internalUser = { }); } - if (typeof expand !== "undefined" && expand !== null) { - query.withGraphFetched(`[${expand.join(", ")}]`); - } - - const res = await query; - return utils.omitRows(omissions())(res); + return await query; }, /** @@ -426,7 +411,7 @@ const internalUser = { } await internalToken.getTokenFromEmail({ - identity: user.email.toLowerCase().trim(), + identity: user.email.toLowerCase(), secret: data.current, }); } diff --git a/backend/lib/certbot.js b/backend/lib/certbot.js index fc5f2ae830..4eb35940a0 100644 --- a/backend/lib/certbot.js +++ b/backend/lib/certbot.js @@ -11,7 +11,7 @@ import utils from "./utils.js"; * @returns {Object} */ const installPlugin = async (pluginKey) => { - if (typeof dnsPlugins[pluginKey] === "undefined") { + if (dnsPlugins[pluginKey]?.package_name === undefined) { // throw Error(`Certbot plugin ${pluginKey} not found`); throw new errs.ItemNotFoundError(pluginKey); } diff --git a/backend/lib/helpers.js b/backend/lib/helpers.js index 73737fe87d..bd1ef31f58 100644 --- a/backend/lib/helpers.js +++ b/backend/lib/helpers.js @@ -49,6 +49,30 @@ const convertBoolFieldsToInt = (obj, fields) => { return obj; }; +const certificateFields = [ + "npmplus_reuse_key", + "npmplus_dns_challenge", + "npmplus_dns_provider", + "npmplus_dns_provider_credentials", + "npmplus_propagation_seconds", +]; + +const pickCertificateFields = (obj) => + Object.fromEntries(Object.entries(obj).filter(([key]) => certificateFields.includes(key))); + +const removeCertificateFields = (obj) => + Object.fromEntries(Object.entries(obj).filter(([key]) => !certificateFields.includes(key))); + +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, + ) + ? undefined + : value; +}; + /** * Casts a column to json if using postgres * @@ -57,4 +81,12 @@ const convertBoolFieldsToInt = (obj, fields) => { */ const castJsonIfNeed = (colName) => (isPostgres() ? ref(colName).castText() : colName); -export { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool, parseDatePeriod }; +export { + castJsonIfNeed, + convertBoolFieldsToInt, + convertIntFieldsToBool, + jsonReplacer, + parseDatePeriod, + pickCertificateFields, + removeCertificateFields, +}; diff --git a/backend/lib/utils.js b/backend/lib/utils.js index e8e9d7db93..5597961091 100644 --- a/backend/lib/utils.js +++ b/backend/lib/utils.js @@ -5,7 +5,6 @@ import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { Liquid } from "liquidjs"; -import _ from "lodash"; import { debug, global as logger } from "../logger.js"; import errs from "./error.js"; @@ -73,34 +72,6 @@ const execFile = async (cmd, args) => { } }; -/** - * Used in objection query builder - * - * @param {Array} omissions - * @returns {Function} - */ -const omitRow = (omissions) => { - /** - * @param {Object} row - * @returns {Object} - */ - return (row) => _.omit(row, omissions); -}; - -/** - * Used in objection query builder - * - * @param {Array} omissions - * @returns {Function} - */ -const omitRows = (omissions) => { - /** - * @param {Array} rows - * @returns {Object} - */ - return (rows) => rows.map((row) => _.omit(row, omissions)); -}; - /** * @returns {Object} Liquid render engine */ @@ -158,8 +129,6 @@ const getParsedTemplate = async (templatePath) => { export default { writeHash, execFile, - omitRow, - omitRows, getRenderEngine, getParsedTemplate, isValidAuthRequestUpstream, diff --git a/backend/lib/validator/api.js b/backend/lib/validator/api.js index 0ed1f29a13..2e27952938 100644 --- a/backend/lib/validator/api.js +++ b/backend/lib/validator/api.js @@ -3,12 +3,36 @@ import errs from "../error.js"; const ajv = new Ajv({ verbose: true, - allErrors: true, allowUnionTypes: true, strict: false, coerceTypes: true, }); +const untrimmedPaths = new Set([ + "/secret", + "/current", + "/auth/secret", + "/items/username", + "/items/password", + "/npmplus_dns_provider_credentials", + "/advanced_config", + "/npmplus_advanced_config", + "/npmplus_location_config", + "/locations/advanced_config", + "/locations/location_type", + "/meta/html", +]); + +const trimStrings = (value, path = "", depth = 0) => { + if (depth > 8) throw new errs.ValidationError("Payload is nested too deeply"); + if (typeof value === "string") return untrimmedPaths.has(path) ? value : value.trim(); + if (Array.isArray(value)) return value.map((item) => trimStrings(item, path, depth + 1)); + if (value === null || typeof value !== "object") return value; + return Object.fromEntries( + Object.entries(value).map(([key, item]) => [key, trimStrings(item, `${path}/${key}`, depth + 1)]), + ); +}; + /** * @param {Object} schema * @param {Object} payload @@ -26,10 +50,11 @@ const apiValidator = (schema, payload /*, description*/) => { const validate = ajv.compile(schema); - const valid = validate(payload); + const data = trimStrings(payload); + const valid = validate(data); if (valid && !validate.errors) { - return payload; + return data; } const message = ajv.errorsText(validate.errors); diff --git a/backend/lib/validator/index.js b/backend/lib/validator/index.js index ebc02d0133..78307924d1 100644 --- a/backend/lib/validator/index.js +++ b/backend/lib/validator/index.js @@ -32,7 +32,7 @@ const validator = (schema, payload) => resolve(structuredClone(payload)); } else { const message = ajv.errorsText(validate.errors); - reject(new errs.InternalValidationError(message)); + reject(new errs.ValidationError(message)); } } catch (err) { reject(err); diff --git a/backend/logger.js b/backend/logger.js index 70fd55039d..15901a670e 100644 --- a/backend/logger.js +++ b/backend/logger.js @@ -20,7 +20,7 @@ const gravatar = createLogger("Gravatar "); const oidc = createLogger("OIDC "); const debug = (logger, ...args) => { - if (logger !== express) logger.debug(...args); + logger.debug(...args); }; // Everything below runs on a timer: the telemetry collectors every 60s, the diff --git a/backend/migrations/20260725143120_not_null_optional_columns.js b/backend/migrations/20260725143120_not_null_optional_columns.js index dd218143af..ce5026ecca 100644 --- a/backend/migrations/20260725143120_not_null_optional_columns.js +++ b/backend/migrations/20260725143120_not_null_optional_columns.js @@ -19,8 +19,8 @@ const up = async (knex) => { await knex("stream").whereNull("npmplus_description").update({ npmplus_description: "" }); await knex.schema.alterTable("proxy_host", (table) => { - table.json("locations").notNull().defaultTo("[]").alter(); - table.json("npmplus_access_list_ids").notNull().defaultTo("[]").alter(); + table.json("locations").notNull().defaultTo([]).alter(); + table.json("npmplus_access_list_ids").notNull().defaultTo([]).alter(); }); logger.info(`[${migrateName}] proxy_host Table altered`); diff --git a/backend/migrations/20260913183944_nickname_default.js b/backend/migrations/20260913183944_nickname_default.js index 157402af61..9c97221b22 100644 --- a/backend/migrations/20260913183944_nickname_default.js +++ b/backend/migrations/20260913183944_nickname_default.js @@ -2,6 +2,14 @@ import { migrate as logger } from "../logger.js"; const migrateName = "nickname_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...`); @@ -12,6 +20,12 @@ const up = async (knex) => { logger.info(`[${migrateName}] user Table altered`); }; +/** + * Undo Migrate + * + * @param {Object} _knex + * @returns {Promise} + */ const down = (_knex) => { throw new Error(`[${migrateName}] You can't migrate down this one.`); }; diff --git a/backend/migrations/20260924163000_upstream_location_access_list.js b/backend/migrations/20260924163000_upstream_location_access_list.js new file mode 100644 index 0000000000..92ef3401d3 --- /dev/null +++ b/backend/migrations/20260924163000_upstream_location_access_list.js @@ -0,0 +1,53 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "upstream_location_access_list"; + +/** + * Migrate + * + * @see https://knexjs.org/guide/migrations.html#migration-api + * + * @param {Object} knex + * @returns {Promise} + */ +const up = async (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + const validIds = new Set(await knex("access_list").where("is_deleted", 0).pluck("id")); + for (const row of await knex("proxy_host").where("is_deleted", 0).select("id", "locations")) { + const locations = Array.isArray(row.locations) ? row.locations : JSON.parse(row.locations || "[]"); + if (!locations.some((location) => location.access_list_id !== undefined)) continue; + for (const { access_list_id: id } of locations) + if (validIds.has(id)) { + await knex("npmplus_proxy_host_access_list") + .insert({ proxy_host_id: row.id, access_list_id: id }) + .onConflict() + .ignore(); + } + await knex("proxy_host") + .where("id", row.id) + .update({ + locations: JSON.stringify( + locations.map(({ access_list_id: id, ...location }) => + validIds.has(id) + ? { ...location, npmplus_access_list_type: "custom", npmplus_access_list_ids: [id] } + : location, + ), + ), + }); + } + + 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/migrations/20260925221019_remove_certificate_meta_files.js b/backend/migrations/20260925221019_remove_certificate_meta_files.js new file mode 100644 index 0000000000..e27c8e6313 --- /dev/null +++ b/backend/migrations/20260925221019_remove_certificate_meta_files.js @@ -0,0 +1,37 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "remove_certificate_meta_files"; + +/** + * Migrate + * + * @see https://knexjs.org/guide/migrations.html#migration-api + * + * @param {Object} knex + * @returns {Promise} + */ +const up = async (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + for (const row of await knex("certificate").select("id", "meta")) { + const { certificate, certificate_key, intermediate_certificate, ...meta } = + typeof row.meta === "string" ? JSON.parse(row.meta) : row.meta; + await knex("certificate") + .where("id", row.id) + .update({ meta: JSON.stringify(meta) }); + } + + logger.info(`[${migrateName}] certificate 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/migrations/20260925224722_remove_location_button_values.js b/backend/migrations/20260925224722_remove_location_button_values.js new file mode 100644 index 0000000000..4a32baa00a --- /dev/null +++ b/backend/migrations/20260925224722_remove_location_button_values.js @@ -0,0 +1,46 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "remove_location_button_values"; + +/** + * Migrate + * + * @see https://knexjs.org/guide/migrations.html#migration-api + * + * @param {Object} knex + * @returns {Promise} + */ +const up = async (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + const fields = [ + "caching_enabled", + "block_exploits", + "allow_websocket_upgrade", + "npmplus_fancyindex_upstream_compression", + "npmplus_disable_uri_sanitisation", + "npmplus_spoof_host_header", + ]; + for (const row of await knex("proxy_host").where("is_deleted", 0).select("id", "locations")) { + const locations = Array.isArray(row.locations) ? row.locations : JSON.parse(row.locations || "[]"); + if (!locations.some((location) => fields.some((field) => location[field] !== undefined))) continue; + for (const location of locations) for (const field of fields) delete location[field]; + await knex("proxy_host") + .where("id", row.id) + .update({ locations: JSON.stringify(locations) }); + } + + 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/migrations/20260926003840_clean_audit_log_meta.js b/backend/migrations/20260926003840_clean_audit_log_meta.js new file mode 100644 index 0000000000..74d842af35 --- /dev/null +++ b/backend/migrations/20260926003840_clean_audit_log_meta.js @@ -0,0 +1,47 @@ +import { jsonReplacer } from "../lib/helpers.js"; +import { migrate as logger } from "../logger.js"; + +const migrateName = "clean_audit_log_meta"; + +/** + * Migrate + * + * @see https://knexjs.org/guide/migrations.html#migration-api + * + * @param {Object} knex + * @returns {Promise} + */ +const up = async (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + let rows = []; + do { + rows = await knex("audit_log") + .select("id", "meta") + .where("id", ">", rows.at(-1)?.id ?? 0) + .orderBy("id") + .limit(100); + + for (const row of rows) { + const meta = typeof row.meta === "string" ? JSON.parse(row.meta) : row.meta; + const cleanedMeta = JSON.stringify(meta, jsonReplacer); + if (cleanedMeta !== JSON.stringify(meta)) { + await knex("audit_log").where("id", row.id).update({ meta: cleanedMeta }); + } + } + } while (rows.length > 0); + + logger.info(`[${migrateName}] audit_log 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/migrations/20260926160524_meta_to_columns.js b/backend/migrations/20260926160524_meta_to_columns.js new file mode 100644 index 0000000000..80d3570322 --- /dev/null +++ b/backend/migrations/20260926160524_meta_to_columns.js @@ -0,0 +1,83 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "meta_to_columns"; + +/** + * Migrate + * + * @see https://knexjs.org/guide/migrations.html#migration-api + * + * @param {Object} knex + * @returns {Promise} + */ +const up = async (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + for (const tableName of ["proxy_host", "redirection_host", "dead_host", "stream"]) { + await knex.schema.alterTable(tableName, (table) => { + table.integer("npmplus_nginx_online").notNull().unsigned().defaultTo(0); + table.text("npmplus_nginx_err").notNull().defaultTo(""); + table.string("npmplus_directory", 255).notNull().defaultTo(""); + table.integer("npmplus_mtls_certificate_id").notNull().unsigned().defaultTo(0); + table.integer("npmplus_mtls_verify_client_optional").notNull().unsigned().defaultTo(0); + }); + + for (const row of await knex(tableName).select("id", "meta")) { + const meta = (typeof row.meta === "string" ? JSON.parse(row.meta) : row.meta) ?? {}; + await knex(tableName) + .where("id", row.id) + .update({ + npmplus_nginx_online: meta.nginx_online ? 1 : 0, + npmplus_nginx_err: typeof meta.nginx_err === "string" ? meta.nginx_err : "", + npmplus_directory: typeof meta.directory === "string" ? meta.directory.trim().slice(0, 255) : "", + npmplus_mtls_certificate_id: Number.parseInt(meta.npmplus_mtls_certificate_id, 10) || 0, + npmplus_mtls_verify_client_optional: meta.npmplus_mtls_verify_client_optional === true ? 1 : 0, + meta: "{}", + }); + } + + logger.info(`[${migrateName}] ${tableName} Table altered`); + } + + await knex.schema.alterTable("certificate", (table) => { + table.integer("npmplus_reuse_key").notNull().unsigned().defaultTo(0); + table.integer("npmplus_dns_challenge").notNull().unsigned().defaultTo(0); + table.string("npmplus_dns_provider", 255).notNull().defaultTo(""); + table.text("npmplus_dns_provider_credentials").notNull().defaultTo(""); + table.integer("npmplus_propagation_seconds").notNull().unsigned().defaultTo(0); + }); + + for (const row of await knex("certificate").select("id", "meta")) { + const meta = (typeof row.meta === "string" ? JSON.parse(row.meta) : row.meta) ?? {}; + await knex("certificate") + .where("id", row.id) + .update({ + npmplus_reuse_key: meta.reuse_key ? 1 : 0, + npmplus_dns_challenge: meta.dns_challenge ? 1 : 0, + npmplus_dns_provider: typeof meta.dns_provider === "string" ? meta.dns_provider.slice(0, 255) : "", + npmplus_dns_provider_credentials: + typeof meta.dns_provider_credentials === "string" ? meta.dns_provider_credentials : "", + npmplus_propagation_seconds: Number.parseInt(meta.propagation_seconds, 10) || 0, + meta: "{}", + }); + } + + logger.info(`[${migrateName}] certificate Table altered`); + + for (const tableName of ["access_list", "access_list_auth", "access_list_client", "auth"]) { + await knex(tableName).update({ meta: "{}" }); + logger.info(`[${migrateName}] ${tableName} 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/access_list.js b/backend/models/access_list.js index 86fb3d161a..87fb271927 100644 --- a/backend/models/access_list.js +++ b/backend/models/access_list.js @@ -12,7 +12,7 @@ import User from "./user.js"; Model.knex(db()); -const boolFields = ["is_deleted", "satisfy_any", "pass_auth"]; +const boolFields = ["satisfy_any", "pass_auth"]; class AccessList extends Model { $beforeInsert() { @@ -20,9 +20,7 @@ class AccessList extends Model { this.modified_on = now(); // Default for meta - if (typeof this.meta === "undefined") { - this.meta = {}; - } + this.meta ??= {}; } $beforeUpdate() { @@ -30,7 +28,7 @@ class AccessList extends Model { } $parseDatabaseJson(json) { - const thisJson = super.$parseDatabaseJson(json); + const { is_deleted, meta, ...thisJson } = super.$parseDatabaseJson(json); return convertIntFieldsToBool(thisJson, boolFields); } diff --git a/backend/models/access_list_auth.js b/backend/models/access_list_auth.js index 75bf435294..df07467584 100644 --- a/backend/models/access_list_auth.js +++ b/backend/models/access_list_auth.js @@ -14,15 +14,18 @@ class AccessListAuth extends Model { this.modified_on = now(); // Default for meta - if (typeof this.meta === "undefined") { - this.meta = {}; - } + this.meta ??= {}; } $beforeUpdate() { this.modified_on = now(); } + $parseDatabaseJson(json) { + const { meta, ...thisJson } = super.$parseDatabaseJson(json); + return thisJson; + } + static get name() { return "AccessListAuth"; } diff --git a/backend/models/access_list_client.js b/backend/models/access_list_client.js index 91165fe13d..da9d014374 100644 --- a/backend/models/access_list_client.js +++ b/backend/models/access_list_client.js @@ -14,15 +14,18 @@ class AccessListClient extends Model { this.modified_on = now(); // Default for meta - if (typeof this.meta === "undefined") { - this.meta = {}; - } + this.meta ??= {}; } $beforeUpdate() { this.modified_on = now(); } + $parseDatabaseJson(json) { + const { meta, ...thisJson } = super.$parseDatabaseJson(json); + return thisJson; + } + static get name() { return "AccessListClient"; } diff --git a/backend/models/audit-log.js b/backend/models/audit-log.js index 6e2d398f58..c4d015bc34 100644 --- a/backend/models/audit-log.js +++ b/backend/models/audit-log.js @@ -3,6 +3,7 @@ import { Model } from "objection"; import db from "../db.js"; +import { jsonReplacer } from "../lib/helpers.js"; import now from "./now_helper.js"; import User from "./user.js"; @@ -14,9 +15,8 @@ class AuditLog extends Model { this.modified_on = now(); // Default for meta - if (typeof this.meta === "undefined") { - this.meta = {}; - } + this.meta ??= {}; + this.meta = JSON.parse(JSON.stringify(this.meta, jsonReplacer)); } $beforeUpdate() { diff --git a/backend/models/auth.js b/backend/models/auth.js index 246c9d42eb..317317c8d8 100644 --- a/backend/models/auth.js +++ b/backend/models/auth.js @@ -5,14 +5,11 @@ import bcrypt from "bcryptjs"; import { Model } from "objection"; import db from "../db.js"; import { hash, verify } from "../lib/argon2.js"; -import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; import now from "./now_helper.js"; import User from "./user.js"; Model.knex(db()); -const boolFields = ["is_deleted"]; - async function encryptPassword() { if (this.type === "password" && this.secret) { this.secret = await hash(this.secret); @@ -28,9 +25,7 @@ class Auth extends Model { this.modified_on = now(); // Default for meta - if (typeof this.meta === "undefined") { - this.meta = {}; - } + this.meta ??= {}; return encryptPassword.apply(this, queryContext); } @@ -41,13 +36,8 @@ class Auth extends Model { } $parseDatabaseJson(json) { - const thisJson = super.$parseDatabaseJson(json); - return convertIntFieldsToBool(thisJson, boolFields); - } - - $formatDatabaseJson(json) { - const thisJson = convertBoolFieldsToInt(json, boolFields); - return super.$formatDatabaseJson(thisJson); + const { is_deleted, meta, ...thisJson } = super.$parseDatabaseJson(json); + return thisJson; } /** diff --git a/backend/models/certificate.js b/backend/models/certificate.js index 37826a1687..e08eca551a 100644 --- a/backend/models/certificate.js +++ b/backend/models/certificate.js @@ -13,7 +13,7 @@ import userModel from "./user.js"; Model.knex(db()); -const boolFields = ["is_deleted"]; +const boolFields = ["npmplus_reuse_key", "npmplus_dns_challenge"]; class Certificate extends Model { $beforeInsert() { @@ -21,19 +21,14 @@ class Certificate extends Model { this.modified_on = now(); // Default for expires_on - if (typeof this.expires_on === "undefined") { - this.expires_on = now(); - } + this.expires_on ??= now(); // Default for domain_names - if (typeof this.domain_names === "undefined") { - this.domain_names = []; - } + this.domain_names ??= []; // Default for meta - if (typeof this.meta === "undefined") { - this.meta = {}; - } + this.meta ??= {}; + this.npmplus_dns_provider_credentials ??= ""; } $beforeUpdate() { @@ -41,7 +36,7 @@ class Certificate extends Model { } $parseDatabaseJson(json) { - const thisJson = super.$parseDatabaseJson(json); + const { is_deleted, meta, ...thisJson } = super.$parseDatabaseJson(json); return convertIntFieldsToBool(thisJson, boolFields); } @@ -119,6 +114,50 @@ class Certificate extends Model { qb.where("stream.is_deleted", 0); }, }, + mtls_proxy_hosts: { + relation: Model.HasManyRelation, + modelClass: proxyHostModel, + join: { + 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, + modelClass: deadHostModel, + join: { + 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, + modelClass: redirectionHostModel, + join: { + from: "certificate.id", + to: "redirection_host.npmplus_mtls_certificate_id", + }, + modify: (qb) => { + qb.where("redirection_host.is_deleted", 0); + }, + }, + mtls_streams: { + relation: Model.HasManyRelation, + modelClass: streamModel, + join: { + 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 c6a1def7de..9f2f6d7fa5 100644 --- a/backend/models/dead_host.js +++ b/backend/models/dead_host.js @@ -3,7 +3,7 @@ import { Model } from "objection"; import db from "../db.js"; -import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; +import { convertBoolFieldsToInt, convertIntFieldsToBool, removeCertificateFields } from "../lib/helpers.js"; import Certificate from "./certificate.js"; import now from "./now_helper.js"; import User from "./user.js"; @@ -11,13 +11,13 @@ import User from "./user.js"; Model.knex(db()); const boolFields = [ - "is_deleted", "ssl_forced", - "http2_support", "npmplus_http3_support", "enabled", "hsts_enabled", "hsts_subdomains", + "npmplus_nginx_online", + "npmplus_mtls_verify_client_optional", ]; class DeadHost extends Model { @@ -26,14 +26,12 @@ class DeadHost extends Model { this.modified_on = now(); // Default for domain_names - if (typeof this.domain_names === "undefined") { - this.domain_names = []; - } + this.domain_names ??= []; // Default for meta - if (typeof this.meta === "undefined") { - this.meta = {}; - } + this.meta ??= {}; + this.advanced_config ??= ""; + this.npmplus_nginx_err ??= ""; } $beforeUpdate() { @@ -41,12 +39,12 @@ class DeadHost extends Model { } $parseDatabaseJson(json) { - const thisJson = super.$parseDatabaseJson(json); + const { is_deleted, meta, http2_support, ...thisJson } = super.$parseDatabaseJson(json); return convertIntFieldsToBool(thisJson, boolFields); } $formatDatabaseJson(json) { - const thisJson = convertBoolFieldsToInt(json, boolFields); + const thisJson = convertBoolFieldsToInt(removeCertificateFields(json), boolFields); return super.$formatDatabaseJson(thisJson); } @@ -62,10 +60,6 @@ class DeadHost extends Model { return ["domain_names", "meta"]; } - static get defaultAllowGraph() { - return "[owner,certificate]"; - } - static get relationMappings() { return { owner: { diff --git a/backend/models/proxy_host.js b/backend/models/proxy_host.js index 97863ad55e..dd8fd95ae5 100644 --- a/backend/models/proxy_host.js +++ b/backend/models/proxy_host.js @@ -3,7 +3,7 @@ import { Model } from "objection"; import db from "../db.js"; -import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; +import { convertBoolFieldsToInt, convertIntFieldsToBool, removeCertificateFields } from "../lib/helpers.js"; import AccessList from "./access_list.js"; import Certificate from "./certificate.js"; import now from "./now_helper.js"; @@ -12,23 +12,19 @@ import User from "./user.js"; Model.knex(db()); const boolFields = [ - "is_deleted", "ssl_forced", - "caching_enabled", - "block_exploits", - "allow_websocket_upgrade", - "http2_support", "npmplus_http3_support", "enabled", "hsts_enabled", "hsts_subdomains", - "trust_forwarded_proto", "npmplus_noindex", "npmplus_crowdsec_appsec", "npmplus_proxy_request_buffering", "npmplus_proxy_response_buffering", "npmplus_upstream_compression", "npmplus_fancyindex", + "npmplus_nginx_online", + "npmplus_mtls_verify_client_optional", ]; class ProxyHost extends Model { @@ -37,24 +33,20 @@ class ProxyHost extends Model { this.modified_on = now(); // Default for domain_names - if (typeof this.domain_names === "undefined") { - this.domain_names = []; - } + this.domain_names ??= []; // Default for meta - if (typeof this.meta === "undefined") { - this.meta = {}; - } + this.meta ??= {}; + this.advanced_config ??= ""; + this.npmplus_location_config ??= ""; + this.npmplus_nginx_err ??= ""; + this.locations ??= []; // Default for access list type - if (typeof this.npmplus_access_list_type === "undefined") { - this.npmplus_access_list_type = "public"; - } + this.npmplus_access_list_type ??= "public"; // Default for access list ids - if (typeof this.npmplus_access_list_ids === "undefined") { - this.npmplus_access_list_ids = []; - } + this.npmplus_access_list_ids ??= []; } $beforeUpdate() { @@ -62,12 +54,21 @@ class ProxyHost extends Model { } $parseDatabaseJson(json) { - const thisJson = super.$parseDatabaseJson(json); + const { + is_deleted, + access_list_id, + caching_enabled, + block_exploits, + allow_websocket_upgrade, + http2_support, + trust_forwarded_proto, + ...thisJson + } = super.$parseDatabaseJson(json); return convertIntFieldsToBool(thisJson, boolFields); } $formatDatabaseJson(json) { - const thisJson = convertBoolFieldsToInt(json, boolFields); + const thisJson = convertBoolFieldsToInt(removeCertificateFields(json), boolFields); return super.$formatDatabaseJson(thisJson); } @@ -83,10 +84,6 @@ class ProxyHost extends Model { return ["domain_names", "meta", "locations", "npmplus_access_list_ids"]; } - static get defaultAllowGraph() { - return "[owner,access_lists.[clients,items],certificate]"; - } - static get relationMappings() { return { owner: { diff --git a/backend/models/redirection_host.js b/backend/models/redirection_host.js index db2b422fe3..090222f9dc 100644 --- a/backend/models/redirection_host.js +++ b/backend/models/redirection_host.js @@ -3,7 +3,7 @@ import { Model } from "objection"; import db from "../db.js"; -import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; +import { convertBoolFieldsToInt, convertIntFieldsToBool, removeCertificateFields } from "../lib/helpers.js"; import Certificate from "./certificate.js"; import now from "./now_helper.js"; import User from "./user.js"; @@ -11,15 +11,14 @@ import User from "./user.js"; Model.knex(db()); const boolFields = [ - "is_deleted", "enabled", "preserve_path", "ssl_forced", - "block_exploits", "hsts_enabled", "hsts_subdomains", - "http2_support", "npmplus_http3_support", + "npmplus_nginx_online", + "npmplus_mtls_verify_client_optional", ]; class RedirectionHost extends Model { @@ -28,14 +27,12 @@ class RedirectionHost extends Model { this.modified_on = now(); // Default for domain_names - if (typeof this.domain_names === "undefined") { - this.domain_names = []; - } + this.domain_names ??= []; // Default for meta - if (typeof this.meta === "undefined") { - this.meta = {}; - } + this.meta ??= {}; + this.advanced_config ??= ""; + this.npmplus_nginx_err ??= ""; } $beforeUpdate() { @@ -43,12 +40,12 @@ class RedirectionHost extends Model { } $parseDatabaseJson(json) { - const thisJson = super.$parseDatabaseJson(json); + const { is_deleted, meta, block_exploits, http2_support, ...thisJson } = super.$parseDatabaseJson(json); return convertIntFieldsToBool(thisJson, boolFields); } $formatDatabaseJson(json) { - const thisJson = convertBoolFieldsToInt(json, boolFields); + const thisJson = convertBoolFieldsToInt(removeCertificateFields(json), boolFields); return super.$formatDatabaseJson(thisJson); } @@ -64,10 +61,6 @@ class RedirectionHost extends Model { return ["domain_names", "meta"]; } - static get defaultAllowGraph() { - return "[owner,certificate]"; - } - static get relationMappings() { return { owner: { diff --git a/backend/models/setting.js b/backend/models/setting.js index 1fbef374cc..6252fc532b 100644 --- a/backend/models/setting.js +++ b/backend/models/setting.js @@ -9,9 +9,7 @@ Model.knex(db()); class Setting extends Model { $beforeInsert() { // Default for meta - if (typeof this.meta === "undefined") { - this.meta = {}; - } + this.meta ??= {}; } static get name() { diff --git a/backend/models/stream.js b/backend/models/stream.js index aef6629405..9f77214f63 100644 --- a/backend/models/stream.js +++ b/backend/models/stream.js @@ -3,14 +3,21 @@ import { Model } from "objection"; import db from "../db.js"; -import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; +import { convertBoolFieldsToInt, convertIntFieldsToBool, removeCertificateFields } from "../lib/helpers.js"; import Certificate from "./certificate.js"; import now from "./now_helper.js"; import User from "./user.js"; Model.knex(db()); -const boolFields = ["is_deleted", "enabled", "tcp_forwarding", "udp_forwarding", "npmplus_proxy_tls"]; +const boolFields = [ + "enabled", + "tcp_forwarding", + "udp_forwarding", + "npmplus_proxy_tls", + "npmplus_nginx_online", + "npmplus_mtls_verify_client_optional", +]; class Stream extends Model { $beforeInsert() { @@ -18,9 +25,9 @@ class Stream extends Model { this.modified_on = now(); // Default for meta - if (typeof this.meta === "undefined") { - this.meta = {}; - } + this.meta ??= {}; + this.npmplus_advanced_config ??= ""; + this.npmplus_nginx_err ??= ""; } $beforeUpdate() { @@ -28,12 +35,12 @@ class Stream extends Model { } $parseDatabaseJson(json) { - const thisJson = super.$parseDatabaseJson(json); + const { is_deleted, meta, ...thisJson } = super.$parseDatabaseJson(json); return convertIntFieldsToBool(thisJson, boolFields); } $formatDatabaseJson(json) { - const thisJson = convertBoolFieldsToInt(json, boolFields); + const thisJson = convertBoolFieldsToInt(removeCertificateFields(json), boolFields); return super.$formatDatabaseJson(thisJson); } @@ -49,10 +56,6 @@ class Stream extends Model { return ["meta"]; } - static get defaultAllowGraph() { - return "[owner,certificate]"; - } - static get relationMappings() { return { owner: { diff --git a/backend/models/user.js b/backend/models/user.js index 68a314466a..5341a2c92f 100644 --- a/backend/models/user.js +++ b/backend/models/user.js @@ -9,7 +9,7 @@ import UserPermission from "./user_permission.js"; Model.knex(db()); -const boolFields = ["is_deleted", "is_disabled"]; +const boolFields = ["is_disabled"]; class User extends Model { $beforeInsert() { @@ -17,9 +17,7 @@ class User extends Model { this.modified_on = now(); // Default for roles - if (typeof this.roles === "undefined") { - this.roles = []; - } + this.roles ??= []; } $beforeUpdate() { @@ -27,7 +25,7 @@ class User extends Model { } $parseDatabaseJson(json) { - const thisJson = super.$parseDatabaseJson(json); + const { is_deleted, nickname, ...thisJson } = super.$parseDatabaseJson(json); return convertIntFieldsToBool(thisJson, boolFields); } diff --git a/backend/routes/audit-log.js b/backend/routes/audit-log.js index e02bc57939..a798a6c9b1 100644 --- a/backend/routes/audit-log.js +++ b/backend/routes/audit-log.js @@ -8,6 +8,7 @@ const listSchema = { properties: { expand: { $ref: "common#/properties/expand", + items: { enum: ["user"] }, }, query: { $ref: "common#/properties/query", @@ -24,6 +25,7 @@ const eventSchema = { }, expand: { $ref: "common#/properties/expand", + items: { enum: ["user"] }, }, }, }; @@ -46,7 +48,7 @@ router * * Retrieve all logs */ - .get(async (req, res, _next) => { + .get(async (req, res) => { const data = await validator(listSchema, { expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, query: typeof req.query.query === "string" ? req.query.query : null, @@ -69,7 +71,7 @@ router * * Retrieve a specific entry */ - .get(async (req, res, _next) => { + .get(async (req, res) => { const data = await validator(eventSchema, { event_id: req.params.event_id, expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, diff --git a/backend/routes/docs.js b/backend/routes/docs.js index b80433c439..7310869fbc 100644 --- a/backend/routes/docs.js +++ b/backend/routes/docs.js @@ -18,7 +18,7 @@ router /** * GET / (Now serves the Swagger UI interface) */ - .get(async (_req, res, _next) => { + .get(async (_, res) => { const swaggerJSON = await getCompiledSchema(); swaggerJSON.info.version = PACKAGE.version; swaggerJSON.servers[0].url = "/api"; diff --git a/backend/routes/nginx/access_lists.js b/backend/routes/nginx/access_lists.js index f5390fdd45..3e8af75283 100644 --- a/backend/routes/nginx/access_lists.js +++ b/backend/routes/nginx/access_lists.js @@ -10,6 +10,7 @@ const listSchema = { properties: { expand: { $ref: "common#/properties/expand", + items: { enum: ["owner", "items", "clients"] }, }, query: { $ref: "common#/properties/query", @@ -26,6 +27,7 @@ const accessListSchema = { }, expand: { $ref: "common#/properties/expand", + items: { enum: ["items", "clients"] }, }, }, }; @@ -48,7 +50,7 @@ router * * Retrieve all access-lists */ - .get(async (req, res, _next) => { + .get(async (req, res) => { const data = await validator(listSchema, { expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, query: typeof req.query.query === "string" ? req.query.query : null, @@ -62,7 +64,7 @@ router * * Create a new access-list */ - .post(async (req, res, _next) => { + .post(async (req, res) => { const payload = apiValidator(getValidationSchema("/nginx/access-lists", "post"), req.body); const result = await internalAccessList.create(res.locals.access, payload); res.status(201).send(result); @@ -82,7 +84,7 @@ router * * Retrieve a specific access-list */ - .get(async (req, res, _next) => { + .get(async (req, res) => { const data = await validator(accessListSchema, { list_id: req.params.list_id, expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, @@ -99,7 +101,7 @@ router * * Update and existing access-list */ - .put(async (req, res, _next) => { + .put(async (req, res) => { const payload = apiValidator(getValidationSchema("/nginx/access-lists/{listID}", "put"), req.body); payload.id = Number.parseInt(req.params.list_id, 10); const result = await internalAccessList.update(res.locals.access, payload); @@ -111,7 +113,7 @@ router * * Delete and existing access-list */ - .delete(async (req, res, _next) => { + .delete(async (req, res) => { const result = await internalAccessList.delete(res.locals.access, { id: Number.parseInt(req.params.list_id, 10), }); diff --git a/backend/routes/nginx/certificates.js b/backend/routes/nginx/certificates.js index a394dff13f..7828a8c5c0 100644 --- a/backend/routes/nginx/certificates.js +++ b/backend/routes/nginx/certificates.js @@ -5,8 +5,8 @@ import { rateLimit } from "express-rate-limit"; import multer from "multer"; import dnsPlugins from "../../certbot/dns-plugins.json" with { type: "json" }; import internalCertificate from "../../internal/certificate.js"; -import errs from "../../lib/error.js"; import jwtdecode from "../../lib/express/jwt-decode.js"; +import requireLogin from "../../lib/express/require-login.js"; import apiValidator from "../../lib/validator/api.js"; import validator from "../../lib/validator/index.js"; import { debug, express as logger } from "../../logger.js"; @@ -17,6 +17,19 @@ const listSchema = { properties: { expand: { $ref: "common#/properties/expand", + items: { + enum: [ + "owner", + "proxy_hosts", + "redirection_hosts", + "dead_hosts", + "streams", + "mtls_proxy_hosts", + "mtls_redirection_hosts", + "mtls_dead_hosts", + "mtls_streams", + ], + }, }, query: { $ref: "common#/properties/query", @@ -31,9 +44,6 @@ const certificateSchema = { certificate_id: { $ref: "common#/properties/id", }, - expand: { - $ref: "common#/properties/expand", - }, }, }; @@ -77,7 +87,7 @@ router * * Retrieve all certificates */ - .get(async (req, res, _next) => { + .get(async (req, res) => { const data = await validator(listSchema, { expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, query: typeof req.query.query === "string" ? req.query.query : null, @@ -91,7 +101,7 @@ router * * Create a new certificate */ - .post(async (req, res, _next) => { + .post(async (req, res) => { const payload = apiValidator(getValidationSchema("/nginx/certificates", "post"), req.body); req.setTimeout(900000); // 15 minutes timeout const result = await internalCertificate.create(res.locals.access, payload); @@ -103,17 +113,14 @@ router */ router .route("/dns-providers") - .all(jwtdecode()) + .all(requireLogin()) /** * GET /api/nginx/certificates/dns-providers * * Get list of all supported DNS providers */ - .get((_req, res, _next) => { - if (!res.locals.access.token.getUserId()) { - throw new errs.PermissionError("Login required"); - } + .get((_, res) => { const clean = Object.keys(dnsPlugins).map((key) => ({ id: key, name: dnsPlugins[key].name, @@ -138,7 +145,7 @@ router * * Test HTTP challenge for domains */ - .post(async (req, res, _next) => { + .post(async (req, res) => { const payload = apiValidator(getValidationSchema("/nginx/certificates/test-http", "post"), req.body); req.setTimeout(60000); // 1 minute timeout @@ -160,7 +167,7 @@ router * * Validate certificates */ - .post(parseCertFiles, (req, res, _next) => { + .post(parseCertFiles, (req, res) => { if (!req.files?.certificate) return res.status(400).send({ error: "certificate file is required" }); const result = internalCertificate.validate(res.locals.access, { @@ -183,14 +190,12 @@ router * * Retrieve a specific certificate */ - .get(async (req, res, _next) => { + .get(async (req, res) => { const data = await validator(certificateSchema, { certificate_id: req.params.certificate_id, - expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, }); const row = await internalCertificate.get(res.locals.access, { id: Number.parseInt(data.certificate_id, 10), - expand: data.expand, }); res.status(200).send(row); }) @@ -200,7 +205,7 @@ router * * Update and existing certificate */ - .delete(async (req, res, _next) => { + .delete(async (req, res) => { const result = await internalCertificate.delete(res.locals.access, { id: Number.parseInt(req.params.certificate_id, 10), }); @@ -221,7 +226,7 @@ router * * Upload certificates */ - .post(parseCertFiles, async (req, res, _next) => { + .post(parseCertFiles, async (req, res) => { if (!req.files?.certificate) return res.status(400).send({ error: "certificate file is required" }); const result = await internalCertificate.upload(res.locals.access, { @@ -245,7 +250,7 @@ router * * Renew certificate */ - .post(async (req, res, _next) => { + .post(async (req, res) => { req.setTimeout(900000); // 15 minutes timeout const result = await internalCertificate.renew(res.locals.access, { id: Number.parseInt(req.params.certificate_id, 10), diff --git a/backend/routes/nginx/dead_hosts.js b/backend/routes/nginx/dead_hosts.js index e3a46c0411..f00d005219 100644 --- a/backend/routes/nginx/dead_hosts.js +++ b/backend/routes/nginx/dead_hosts.js @@ -10,6 +10,7 @@ const listSchema = { properties: { expand: { $ref: "common#/properties/expand", + items: { enum: ["owner", "certificate"] }, }, query: { $ref: "common#/properties/query", @@ -24,9 +25,6 @@ const hostSchema = { host_id: { $ref: "common#/properties/id", }, - expand: { - $ref: "common#/properties/expand", - }, }, }; @@ -48,7 +46,7 @@ router * * Retrieve all dead-hosts */ - .get(async (req, res, _next) => { + .get(async (req, res) => { const data = await validator(listSchema, { expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, query: typeof req.query.query === "string" ? req.query.query : null, @@ -62,7 +60,7 @@ router * * Create a new dead-host */ - .post(async (req, res, _next) => { + .post(async (req, res) => { const payload = apiValidator(getValidationSchema("/nginx/dead-hosts", "post"), req.body); const result = await internalDeadHost.create(res.locals.access, payload); res.status(201).send(result); @@ -82,14 +80,12 @@ router * * Retrieve a specific dead-host */ - .get(async (req, res, _next) => { + .get(async (req, res) => { const data = await validator(hostSchema, { host_id: req.params.host_id, - expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, }); const row = await internalDeadHost.get(res.locals.access, { id: Number.parseInt(data.host_id, 10), - expand: data.expand, }); res.status(200).send(row); }) @@ -99,7 +95,7 @@ router * * Update an existing dead-host */ - .put(async (req, res, _next) => { + .put(async (req, res) => { const payload = apiValidator(getValidationSchema("/nginx/dead-hosts/{hostID}", "put"), req.body); payload.id = Number.parseInt(req.params.host_id, 10); const result = await internalDeadHost.update(res.locals.access, payload); @@ -111,7 +107,7 @@ router * * Delete a dead-host */ - .delete(async (req, res, _next) => { + .delete(async (req, res) => { const result = await internalDeadHost.delete(res.locals.access, { id: Number.parseInt(req.params.host_id, 10), }); @@ -130,7 +126,7 @@ router /** * POST /api/nginx/dead-hosts/123/enable */ - .post(async (req, res, _next) => { + .post(async (req, res) => { const result = await internalDeadHost.enable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10), }); @@ -149,7 +145,7 @@ router /** * POST /api/nginx/dead-hosts/123/disable */ - .post(async (req, res, _next) => { + .post(async (req, res) => { const result = await internalDeadHost.disable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10), }); diff --git a/backend/routes/nginx/proxy_hosts.js b/backend/routes/nginx/proxy_hosts.js index 7e874b1633..784b78e2a1 100644 --- a/backend/routes/nginx/proxy_hosts.js +++ b/backend/routes/nginx/proxy_hosts.js @@ -4,7 +4,6 @@ import internalProxyHostAccessList from "../../internal/proxy-host-access-list.j import jwtdecode from "../../lib/express/jwt-decode.js"; import apiValidator from "../../lib/validator/api.js"; import validator from "../../lib/validator/index.js"; -import { debug, express as logger } from "../../logger.js"; import { getValidationSchema } from "../../schema/index.js"; const listSchema = { @@ -12,6 +11,7 @@ const listSchema = { properties: { expand: { $ref: "common#/properties/expand", + items: { enum: ["owner", "access_lists", "certificate"] }, }, query: { $ref: "common#/properties/query", @@ -26,9 +26,6 @@ const hostSchema = { host_id: { $ref: "common#/properties/id", }, - expand: { - $ref: "common#/properties/expand", - }, }, }; @@ -50,7 +47,7 @@ router * * Retrieve all proxy-hosts */ - .get(async (req, res, _next) => { + .get(async (req, res) => { const data = await validator(listSchema, { expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, query: typeof req.query.query === "string" ? req.query.query : null, @@ -64,18 +61,10 @@ router * * Create a new proxy-host */ - .post(async (req, res, next) => { - try { - const payload = apiValidator(getValidationSchema("/nginx/proxy-hosts", "post"), req.body); - const result = await internalProxyHost.create(res.locals.access, payload); - res.status(201).send(result); - } catch (err) { - debug( - logger, - `${req.method.toUpperCase()} ${req.originalUrl}: ${err} ${JSON.stringify(err.debug, null, 2)}`, - ); - next(err); - } + .post(async (req, res) => { + const payload = apiValidator(getValidationSchema("/nginx/proxy-hosts", "post"), req.body); + const result = await internalProxyHost.create(res.locals.access, payload); + res.status(201).send(result); }); /** @@ -92,14 +81,12 @@ router * * Retrieve a specific proxy-host */ - .get(async (req, res, _next) => { + .get(async (req, res) => { const data = await validator(hostSchema, { host_id: req.params.host_id, - expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, }); const row = await internalProxyHost.get(res.locals.access, { id: Number.parseInt(data.host_id, 10), - expand: data.expand, }); res.status(200).send(internalProxyHostAccessList.maskAccessListItems(row)); }) @@ -109,7 +96,7 @@ router * * Update an existing proxy-host */ - .put(async (req, res, _next) => { + .put(async (req, res) => { const payload = apiValidator(getValidationSchema("/nginx/proxy-hosts/{hostID}", "put"), req.body); payload.id = Number.parseInt(req.params.host_id, 10); const result = await internalProxyHost.update(res.locals.access, payload); @@ -121,7 +108,7 @@ router * * Delete a proxy-host */ - .delete(async (req, res, _next) => { + .delete(async (req, res) => { const result = await internalProxyHost.delete(res.locals.access, { id: Number.parseInt(req.params.host_id, 10), }); @@ -140,7 +127,7 @@ router /** * POST /api/nginx/proxy-hosts/123/enable */ - .post(async (req, res, _next) => { + .post(async (req, res) => { const result = await internalProxyHost.enable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10), }); @@ -159,7 +146,7 @@ router /** * POST /api/nginx/proxy-hosts/123/disable */ - .post(async (req, res, _next) => { + .post(async (req, res) => { const result = await internalProxyHost.disable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10), }); diff --git a/backend/routes/nginx/redirection_hosts.js b/backend/routes/nginx/redirection_hosts.js index 58f4e356b2..d5ea6899c2 100644 --- a/backend/routes/nginx/redirection_hosts.js +++ b/backend/routes/nginx/redirection_hosts.js @@ -10,6 +10,7 @@ const listSchema = { properties: { expand: { $ref: "common#/properties/expand", + items: { enum: ["owner", "certificate"] }, }, query: { $ref: "common#/properties/query", @@ -24,9 +25,6 @@ const hostSchema = { host_id: { $ref: "common#/properties/id", }, - expand: { - $ref: "common#/properties/expand", - }, }, }; @@ -48,7 +46,7 @@ router * * Retrieve all redirection-hosts */ - .get(async (req, res, _next) => { + .get(async (req, res) => { const data = await validator(listSchema, { expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, query: typeof req.query.query === "string" ? req.query.query : null, @@ -62,7 +60,7 @@ router * * Create a new redirection-host */ - .post(async (req, res, _next) => { + .post(async (req, res) => { const payload = apiValidator(getValidationSchema("/nginx/redirection-hosts", "post"), req.body); const result = await internalRedirectionHost.create(res.locals.access, payload); res.status(201).send(result); @@ -82,14 +80,12 @@ router * * Retrieve a specific redirection-host */ - .get(async (req, res, _next) => { + .get(async (req, res) => { const data = await validator(hostSchema, { host_id: req.params.host_id, - expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, }); const row = await internalRedirectionHost.get(res.locals.access, { id: Number.parseInt(data.host_id, 10), - expand: data.expand, }); res.status(200).send(row); }) @@ -99,7 +95,7 @@ router * * Update an existing redirection-host */ - .put(async (req, res, _next) => { + .put(async (req, res) => { const payload = apiValidator(getValidationSchema("/nginx/redirection-hosts/{hostID}", "put"), req.body); payload.id = Number.parseInt(req.params.host_id, 10); const result = await internalRedirectionHost.update(res.locals.access, payload); @@ -111,7 +107,7 @@ router * * Delete a redirection-host */ - .delete(async (req, res, _next) => { + .delete(async (req, res) => { const result = await internalRedirectionHost.delete(res.locals.access, { id: Number.parseInt(req.params.host_id, 10), }); @@ -130,7 +126,7 @@ router /** * POST /api/nginx/redirection-hosts/123/enable */ - .post(async (req, res, _next) => { + .post(async (req, res) => { const result = await internalRedirectionHost.enable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10), }); @@ -149,7 +145,7 @@ router /** * POST /api/nginx/redirection-hosts/123/disable */ - .post(async (req, res, _next) => { + .post(async (req, res) => { const result = await internalRedirectionHost.disable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10), }); diff --git a/backend/routes/nginx/streams.js b/backend/routes/nginx/streams.js index e69255fd09..e3f395fc7b 100644 --- a/backend/routes/nginx/streams.js +++ b/backend/routes/nginx/streams.js @@ -10,6 +10,7 @@ const listSchema = { properties: { expand: { $ref: "common#/properties/expand", + items: { enum: ["owner", "certificate"] }, }, query: { $ref: "common#/properties/query", @@ -24,9 +25,6 @@ const streamSchema = { stream_id: { $ref: "common#/properties/id", }, - expand: { - $ref: "common#/properties/expand", - }, }, }; @@ -48,7 +46,7 @@ router * * Retrieve all streams */ - .get(async (req, res, _next) => { + .get(async (req, res) => { const data = await validator(listSchema, { expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, query: typeof req.query.query === "string" ? req.query.query : null, @@ -62,7 +60,7 @@ router * * Create a new stream */ - .post(async (req, res, _next) => { + .post(async (req, res) => { const payload = apiValidator(getValidationSchema("/nginx/streams", "post"), req.body); const result = await internalStream.create(res.locals.access, payload); res.status(201).send(result); @@ -82,14 +80,12 @@ router * * Retrieve a specific stream */ - .get(async (req, res, _next) => { + .get(async (req, res) => { const data = await validator(streamSchema, { stream_id: req.params.stream_id, - expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, }); const row = await internalStream.get(res.locals.access, { id: Number.parseInt(data.stream_id, 10), - expand: data.expand, }); res.status(200).send(row); }) @@ -99,7 +95,7 @@ router * * Update an existing stream */ - .put(async (req, res, _next) => { + .put(async (req, res) => { const payload = apiValidator(getValidationSchema("/nginx/streams/{streamID}", "put"), req.body); payload.id = Number.parseInt(req.params.stream_id, 10); const result = await internalStream.update(res.locals.access, payload); @@ -111,7 +107,7 @@ router * * Delete a stream */ - .delete(async (req, res, _next) => { + .delete(async (req, res) => { const result = await internalStream.delete(res.locals.access, { id: Number.parseInt(req.params.stream_id, 10), }); @@ -130,7 +126,7 @@ router /** * POST /api/nginx/streams/123/enable */ - .post(async (req, res, _next) => { + .post(async (req, res) => { const result = await internalStream.enable(res.locals.access, { id: Number.parseInt(req.params.stream_id, 10), }); @@ -149,7 +145,7 @@ router /** * POST /api/nginx/streams/123/disable */ - .post(async (req, res, _next) => { + .post(async (req, res) => { const result = await internalStream.disable(res.locals.access, { id: Number.parseInt(req.params.stream_id, 10), }); diff --git a/backend/routes/reports.js b/backend/routes/reports.js index 6421777263..17c2bd2fc8 100644 --- a/backend/routes/reports.js +++ b/backend/routes/reports.js @@ -15,7 +15,7 @@ router /** * GET /reports/hosts */ - .get(async (_req, res, _next) => { + .get(async (_, res) => { const data = await internalReport.getHostsReport(res.locals.access); res.status(200).send(data); }); diff --git a/backend/routes/schema.js b/backend/routes/schema.js index 45c4bb8c4a..a3cc4e4fa4 100644 --- a/backend/routes/schema.js +++ b/backend/routes/schema.js @@ -16,11 +16,13 @@ router /** * GET /schema */ - .get(async (_req, res, _next) => { + .get(async (req, res) => { const swaggerJSON = await getCompiledSchema(); swaggerJSON.info.version = PACKAGE.version; swaggerJSON.servers[0].url = "/api"; - res.status(200).send(swaggerJSON); + res.status(200) + .type("json") + .send(JSON.stringify(swaggerJSON, null, req.app.get("json spaces"))); }); export default router; diff --git a/backend/routes/settings.js b/backend/routes/settings.js index ca2a2fa258..8a9398a69a 100644 --- a/backend/routes/settings.js +++ b/backend/routes/settings.js @@ -34,7 +34,7 @@ router * * Retrieve all settings */ - .get(async (_req, res, _next) => { + .get(async (_, res) => { const rows = await internalSetting.getAll(res.locals.access); res.status(200).send(rows); }); @@ -53,7 +53,7 @@ router * * Retrieve a specific setting */ - .get(async (req, res, _next) => { + .get(async (req, res) => { const data = await validator(settingSchema, { setting_id: req.params.setting_id, }); @@ -68,7 +68,7 @@ router * * Update and existing setting */ - .put(async (req, res, _next) => { + .put(async (req, res) => { const payload = apiValidator(getValidationSchema("/settings/{settingID}", "put"), req.body); payload.id = req.params.setting_id; const result = await internalSetting.update(res.locals.access, payload); diff --git a/backend/routes/tokens.js b/backend/routes/tokens.js index 667af94aea..f2f130dcff 100644 --- a/backend/routes/tokens.js +++ b/backend/routes/tokens.js @@ -98,7 +98,7 @@ router * * Create a new Token */ - .post(async (req, res, _next) => { + .post(async (req, res) => { if (process.env.OIDC_DISABLE_PASSWORD === "true") { throw new errs.PermissionError("Non OIDC login is disabled"); } @@ -163,30 +163,25 @@ router * * Verify TOTP code and get full token */ - .post(async (req, res, next) => { - try { - const { code } = apiValidator(getValidationSchema("/tokens/totp", "post"), req.body); - const result = await internalToken.verifyTotp(req.signedCookies?.["__Host-Http-challenge_token"], code); - const { token, ...responseBody } = result; + .post(async (req, res) => { + const { code } = apiValidator(getValidationSchema("/tokens/totp", "post"), req.body); + const result = await internalToken.verifyTotp(req.signedCookies?.["__Host-Http-challenge_token"], code); + const { token, ...responseBody } = result; - res.cookie("__Host-Http-token", token, { - signed: true, - httpOnly: true, - secure: true, - sameSite: "Strict", - expires: new Date(result.expires), - }); - res.clearCookie("__Host-Http-challenge_token", { - httpOnly: true, - secure: true, - sameSite: "Strict", - }); + res.cookie("__Host-Http-token", token, { + signed: true, + httpOnly: true, + secure: true, + sameSite: "Strict", + expires: new Date(result.expires), + }); + res.clearCookie("__Host-Http-challenge_token", { + httpOnly: true, + secure: true, + sameSite: "Strict", + }); - res.status(200).send(responseBody); - } catch (err) { - debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); - next(err); - } + res.status(200).send(responseBody); }); export default router; diff --git a/backend/routes/users.js b/backend/routes/users.js index 398715e4b1..d01816d6b5 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -11,7 +11,7 @@ import jwtdecode from "../lib/express/jwt-decode.js"; import userIdFromMe from "../lib/express/user-id-from-me.js"; import apiValidator from "../lib/validator/api.js"; import validator from "../lib/validator/index.js"; -import { debug, express as logger } from "../logger.js"; +import { express as logger } from "../logger.js"; import { getValidationSchema } from "../schema/index.js"; import { isSetup, removeSetupToken, verifySetupToken } from "../setup.js"; @@ -20,9 +20,6 @@ let setupCreationInProgress = false; const listSchema = { additionalProperties: false, properties: { - expand: { - $ref: "common#/properties/expand", - }, query: { $ref: "common#/properties/query", }, @@ -38,6 +35,7 @@ const userSchema = { }, expand: { $ref: "common#/properties/expand", + items: { enum: ["permissions"] }, }, }, }; @@ -83,12 +81,11 @@ router * * Retrieve all users */ - .get(async (req, res, _next) => { + .get(async (req, res) => { const data = await validator(listSchema, { - expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, query: typeof req.query.query === "string" ? req.query.query : null, }); - const users = await internalUser.getAll(res.locals.access, data.expand, data.query); + const users = await internalUser.getAll(res.locals.access, data.query); res.status(200).send(users); }) @@ -97,7 +94,7 @@ router * * Create a new User */ - .post(async (req, res, next) => { + .post(async (req, res) => { let claimedSetup = false; try { @@ -132,9 +129,6 @@ router const user = await internalUser.create(res.locals.access, payload); if (claimedSetup) await removeSetupToken(); res.status(201).send(user); - } catch (err) { - debug(logger, `${req.method.toUpperCase()} ${req.originalUrl}: ${err}`); - next(err); } finally { if (claimedSetup) setupCreationInProgress = false; } @@ -155,7 +149,7 @@ router * * Retrieve a specific user */ - .get(async (req, res, _next) => { + .get(async (req, res) => { const data = await validator(userSchema, { user_id: req.params.user_id, expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, @@ -173,7 +167,7 @@ router * * Update and existing user */ - .put(async (req, res, _next) => { + .put(async (req, res) => { const payload = apiValidator(getValidationSchema("/users/{userID}", "put"), req.body); payload.id = req.params.user_id; const result = await internalUser.update(res.locals.access, payload); @@ -185,7 +179,7 @@ router * * Update and existing user */ - .delete(async (req, res, _next) => { + .delete(async (req, res) => { const result = await internalUser.delete(res.locals.access, { id: req.params.user_id, }); @@ -207,7 +201,7 @@ router * * Update password for a user */ - .put(async (req, res, _next) => { + .put(async (req, res) => { const payload = apiValidator(getValidationSchema("/users/{userID}/auth", "put"), req.body); payload.id = req.params.user_id; const result = await internalUser.setPassword(res.locals.access, payload); @@ -241,7 +235,7 @@ router * * Set some or all permissions for a user */ - .put(async (req, res, _next) => { + .put(async (req, res) => { const payload = apiValidator(getValidationSchema("/users/{userID}/permissions", "put"), req.body); payload.id = req.params.user_id; const result = await internalUser.setPermissions(res.locals.access, payload); @@ -263,14 +257,9 @@ router * * Get MFA status for a user (all factors and backup codes) */ - .get(async (req, res, next) => { - try { - const status = await internalMfa.getStatus(res.locals.access, req.params.user_id); - res.status(200).send(status); - } catch (err) { - debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); - next(err); - } + .get(async (req, res) => { + const status = await internalMfa.getStatus(res.locals.access, req.params.user_id); + res.status(200).send(status); }) /** @@ -278,14 +267,9 @@ router * * Admin reset: disable all second factors and backup codes for a user */ - .delete(async (req, res, next) => { - try { - await internalMfa.adminDisable(res.locals.access, req.params.user_id); - res.status(200).send(true); - } catch (err) { - debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); - next(err); - } + .delete(async (req, res) => { + await internalMfa.adminDisable(res.locals.access, req.params.user_id); + res.status(200).send(true); }); /** @@ -303,31 +287,9 @@ router * * Start TOTP setup, returns QR code URL */ - .post(async (req, res, next) => { - try { - const result = await internalTotp.startSetup(res.locals.access, req.params.user_id); - res.status(200).send(result); - } catch (err) { - debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); - next(err); - } - }) - - /** - * DELETE /api/users/123/mfa/totp?code=XXXXXX - * - * Disable TOTP for a user - */ - .delete(async (req, res, next) => { - try { - const code = typeof req.query.code === "string" ? req.query.code : null; - if (!code) throw new errs.ValidationError("Missing required parameter: code"); - await internalMfa.disableTotp(res.locals.access, req.params.user_id, code); - res.status(200).send(true); - } catch (err) { - debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); - next(err); - } + .post(async (req, res) => { + const result = await internalTotp.startSetup(res.locals.access, req.params.user_id); + res.status(200).send(result); }); /** @@ -345,23 +307,34 @@ router * * Verify code and enable TOTP */ - .post(async (req, res, next) => { - try { - const { code } = apiValidator(getValidationSchema("/users/{userID}/mfa/totp/enable", "post"), req.body); - const result = await internalMfa.enableTotp(res.locals.access, req.params.user_id, code); - const data = await internalToken.getFreshToken(res.locals.access, true); - res.cookie("__Host-Http-token", data.token, { - signed: true, - httpOnly: true, - secure: true, - sameSite: "Strict", - expires: new Date(data.expires), - }); - res.status(200).send(result); - } catch (err) { - debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); - next(err); - } + .post(async (req, res) => { + const { code } = apiValidator(getValidationSchema("/users/{userID}/mfa/totp/enable", "post"), req.body); + const result = await internalMfa.enableTotp(res.locals.access, req.params.user_id, code); + const data = await internalToken.getFreshToken(res.locals.access, true); + res.cookie("__Host-Http-token", data.token, { + signed: true, + httpOnly: true, + secure: true, + sameSite: "Strict", + expires: new Date(data.expires), + }); + res.status(200).send(result); + }); + +router + .route("/:user_id/mfa/totp/disable") + .all(jwtdecode()) + .all(userIdFromMe) + + /** + * POST /api/users/123/mfa/totp/disable + * + * Disable TOTP for a user + */ + .post(async (req, res) => { + const { code } = apiValidator(getValidationSchema("/users/{userID}/mfa/totp/disable", "post"), req.body); + await internalMfa.disableTotp(res.locals.access, req.params.user_id, code); + res.status(200).send(true); }); /** @@ -379,23 +352,18 @@ router * * Regenerate backup codes */ - .post(async (req, res, next) => { - try { - const { code } = apiValidator(getValidationSchema("/users/{userID}/mfa/backup-codes", "post"), req.body); - const result = await internalMfa.regenerateBackupCodes(res.locals.access, req.params.user_id, code); - const data = await internalToken.getFreshToken(res.locals.access, true); - res.cookie("__Host-Http-token", data.token, { - signed: true, - httpOnly: true, - secure: true, - sameSite: "Strict", - expires: new Date(data.expires), - }); - res.status(200).send(result); - } catch (err) { - debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); - next(err); - } + .post(async (req, res) => { + const { code } = apiValidator(getValidationSchema("/users/{userID}/mfa/backup-codes", "post"), req.body); + const result = await internalMfa.regenerateBackupCodes(res.locals.access, req.params.user_id, code); + const data = await internalToken.getFreshToken(res.locals.access, true); + res.cookie("__Host-Http-token", data.token, { + signed: true, + httpOnly: true, + secure: true, + sameSite: "Strict", + expires: new Date(data.expires), + }); + res.status(200).send(result); }); router @@ -408,7 +376,7 @@ router * * Revoke all of a user's sessions (self or admin) */ - .delete(async (req, res, _next) => { + .delete(async (req, res) => { await internalUser.revokeSessions(res.locals.access, req.params.user_id); if (Number(req.params.user_id) === res.locals.access.token.getUserId(0)) { res.clearCookie("__Host-Http-token", { @@ -447,7 +415,7 @@ router storage: multer.memoryStorage(), limits: { fileSize: 1024 * 1024, files: 1, fields: 0, parts: 1, fieldNameSize: 32 }, }).single("avatar"), - async (req, res, _next) => { + async (req, res) => { const result = await internalUser.setAvatar(res.locals.access, req.params.user_id, req.file); res.status(200).send(result); }, @@ -458,7 +426,7 @@ router * * Remove the custom avatar, falling back to gravatar */ - .delete(async (req, res, _next) => { + .delete(async (req, res) => { const result = await internalUser.deleteAvatar(res.locals.access, req.params.user_id); res.status(200).send(result); }); diff --git a/backend/routes/version.js b/backend/routes/version.js index bd333ee9ca..9a7faead59 100644 --- a/backend/routes/version.js +++ b/backend/routes/version.js @@ -1,7 +1,6 @@ import express from "express"; import internalRemoteVersion from "../internal/remote-version.js"; import requireLogin from "../lib/express/require-login.js"; -import { debug, express as logger } from "../logger.js"; const router = express.Router({ caseSensitive: true, @@ -21,19 +20,8 @@ router * * Check for available updates */ - .get(async (req, res, _next) => { - try { - const data = await internalRemoteVersion.get(); - res.status(200).send(data); - } catch (error) { - debug(logger, `${req.method.toUpperCase()} ${req.originalUrl}: ${error}`); - // Send 200 even though there's an error to avoid triggering update checks repeatedly - res.status(200).send({ - current: null, - latest: null, - update_available: false, - }); - } + .get(async (_, res) => { + res.status(200).send(await internalRemoteVersion.get()); }); export default router; diff --git a/backend/schema/common.json b/backend/schema/common.json index dae1b0100e..a8251d0318 100644 --- a/backend/schema/common.json +++ b/backend/schema/common.json @@ -18,6 +18,7 @@ { "type": "array", "minItems": 1, + "uniqueItems": true, "items": { "type": "string" } @@ -70,12 +71,6 @@ ], "example": 5 }, - "access_list_id": { - "description": "Access List ID", - "type": "integer", - "minimum": 0, - "example": 3 - }, "domain_names": { "description": "Domain Names array", "type": "array", @@ -118,30 +113,39 @@ "pattern": "^(letsencrypt|other|mtls)$", "example": "letsencrypt" }, - "http2_support": { - "description": "HTTP2 Protocol Support", - "type": "boolean", - "example": true - }, "npmplus_http3_support": { "description": "HTTP3 Protocol Support", "type": "boolean", "example": true }, - "block_exploits": { - "description": "This is always disabled. Your value will be ignored", + "npmplus_nginx_online": { + "description": "Whether the nginx config of this host is loaded", "type": "boolean", - "example": false + "readOnly": true, + "example": true }, - "caching_enabled": { - "description": "This is always disabled. Your value will be ignored", - "type": "boolean", - "example": false + "npmplus_nginx_err": { + "description": "nginx error if the config of this host could not be loaded", + "type": "string", + "readOnly": true, + "example": "" + }, + "npmplus_directory": { + "description": "Directory to group this host in the UI", + "type": "string", + "maxLength": 255, + "example": "Production" }, - "allow_websocket_upgrade": { - "description": "This is always enabled. Your value will be ignored", + "npmplus_mtls_certificate_id": { + "description": "mTLS Certificate ID", + "type": "integer", + "minimum": 0, + "example": 0 + }, + "npmplus_mtls_verify_client_optional": { + "description": "Make the mTLS client verification optional", "type": "boolean", - "example": true + "example": false }, "npmplus_noindex": { "description": "Send noindex header and block some user agents", diff --git a/backend/schema/components/access-list-object.json b/backend/schema/components/access-list-object.json index 97e418c857..c12c02e680 100644 --- a/backend/schema/components/access-list-object.json +++ b/backend/schema/components/access-list-object.json @@ -7,7 +7,6 @@ "modified_on", "owner_user_id", "name", - "meta", "satisfy_any", "pass_auth", "proxy_host_count" @@ -31,10 +30,6 @@ "maxLength": 255, "example": "My Access List" }, - "meta": { - "type": "object", - "example": {} - }, "satisfy_any": { "type": "boolean", "example": true diff --git a/backend/schema/components/certificate-object.json b/backend/schema/components/certificate-object.json index 0af7d036da..7dd897de94 100644 --- a/backend/schema/components/certificate-object.json +++ b/backend/schema/components/certificate-object.json @@ -10,7 +10,10 @@ "nice_name", "domain_names", "expires_on", - "meta" + "npmplus_reuse_key", + "npmplus_dns_challenge", + "npmplus_dns_provider", + "npmplus_propagation_seconds" ], "additionalProperties": false, "properties": { @@ -58,41 +61,33 @@ "owner": { "$ref": "./user-object.json" }, - "meta": { - "type": "object", - "additionalProperties": false, - "properties": { - "certificate": { - "type": "string", - "minLength": 1 - }, - "certificate_key": { - "type": "string", - "minLength": 1 - }, - "reuse_key": { - "type": "boolean" - }, - "dns_challenge": { - "type": "boolean" - }, - "dns_provider_credentials": { - "type": "string" - }, - "dns_provider": { - "type": "string" - }, - "letsencrypt_certificate": { - "type": "object" - }, - "propagation_seconds": { - "type": "integer", - "minimum": 0 - } - }, - "example": { - "dns_challenge": false - } + "npmplus_reuse_key": { + "description": "Reuse the private key on renewal", + "type": "boolean", + "example": false + }, + "npmplus_dns_challenge": { + "description": "Use the DNS challenge", + "type": "boolean", + "example": false + }, + "npmplus_dns_provider": { + "description": "DNS provider", + "type": "string", + "maxLength": 255, + "example": "cloudflare" + }, + "npmplus_dns_provider_credentials": { + "description": "DNS provider credentials", + "type": "string", + "writeOnly": true, + "example": "dns_cloudflare_api_token=123" + }, + "npmplus_propagation_seconds": { + "description": "DNS propagation seconds, 0 uses the default of the DNS plugin", + "type": "integer", + "minimum": 0, + "example": 0 } } } diff --git a/backend/schema/components/dead-host-object.json b/backend/schema/components/dead-host-object.json index 8e00647eb4..e8e8091ae0 100644 --- a/backend/schema/components/dead-host-object.json +++ b/backend/schema/components/dead-host-object.json @@ -11,11 +11,14 @@ "ssl_forced", "hsts_enabled", "hsts_subdomains", - "http2_support", "npmplus_http3_support", "advanced_config", "enabled", - "meta" + "npmplus_nginx_online", + "npmplus_nginx_err", + "npmplus_directory", + "npmplus_mtls_certificate_id", + "npmplus_mtls_verify_client_optional" ], "additionalProperties": false, "properties": { @@ -46,9 +49,6 @@ "hsts_subdomains": { "$ref": "../common.json#/properties/hsts_subdomains" }, - "http2_support": { - "$ref": "../common.json#/properties/http2_support" - }, "npmplus_http3_support": { "$ref": "../common.json#/properties/npmplus_http3_support" }, @@ -59,9 +59,20 @@ "enabled": { "$ref": "../common.json#/properties/enabled" }, - "meta": { - "type": "object", - "example": {} + "npmplus_nginx_online": { + "$ref": "../common.json#/properties/npmplus_nginx_online" + }, + "npmplus_nginx_err": { + "$ref": "../common.json#/properties/npmplus_nginx_err" + }, + "npmplus_directory": { + "$ref": "../common.json#/properties/npmplus_directory" + }, + "npmplus_mtls_certificate_id": { + "$ref": "../common.json#/properties/npmplus_mtls_certificate_id" + }, + "npmplus_mtls_verify_client_optional": { + "$ref": "../common.json#/properties/npmplus_mtls_verify_client_optional" }, "certificate": { "oneOf": [ diff --git a/backend/schema/components/proxy-host-object.json b/backend/schema/components/proxy-host-object.json index 429042de60..e4ef78c3e6 100644 --- a/backend/schema/components/proxy-host-object.json +++ b/backend/schema/components/proxy-host-object.json @@ -13,20 +13,19 @@ "npmplus_access_list_type", "certificate_id", "ssl_forced", - "caching_enabled", - "block_exploits", "advanced_config", "npmplus_location_config", - "meta", - "allow_websocket_upgrade", - "http2_support", + "npmplus_nginx_online", + "npmplus_nginx_err", + "npmplus_directory", + "npmplus_mtls_certificate_id", + "npmplus_mtls_verify_client_optional", "npmplus_http3_support", "forward_scheme", "enabled", "locations", "hsts_enabled", "hsts_subdomains", - "trust_forwarded_proto", "npmplus_noindex", "npmplus_crowdsec_appsec", "npmplus_proxy_request_buffering", @@ -82,15 +81,6 @@ "ssl_forced": { "$ref": "../common.json#/properties/ssl_forced" }, - "caching_enabled": { - "$ref": "../common.json#/properties/caching_enabled" - }, - "block_exploits": { - "$ref": "../common.json#/properties/block_exploits" - }, - "allow_websocket_upgrade": { - "$ref": "../common.json#/properties/allow_websocket_upgrade" - }, "npmplus_noindex": { "$ref": "../common.json#/properties/npmplus_noindex" }, @@ -148,15 +138,20 @@ "type": "string", "example": "" }, - "meta": { - "type": "object", - "example": { - "nginx_online": true, - "nginx_err": null - } + "npmplus_nginx_online": { + "$ref": "../common.json#/properties/npmplus_nginx_online" + }, + "npmplus_nginx_err": { + "$ref": "../common.json#/properties/npmplus_nginx_err" }, - "http2_support": { - "$ref": "../common.json#/properties/http2_support" + "npmplus_directory": { + "$ref": "../common.json#/properties/npmplus_directory" + }, + "npmplus_mtls_certificate_id": { + "$ref": "../common.json#/properties/npmplus_mtls_certificate_id" + }, + "npmplus_mtls_verify_client_optional": { + "$ref": "../common.json#/properties/npmplus_mtls_verify_client_optional" }, "npmplus_http3_support": { "$ref": "../common.json#/properties/npmplus_http3_support" @@ -225,20 +220,6 @@ "forward_port": { "$ref": "#/properties/forward_port" }, - "caching_enabled": { - "$ref": "#/properties/caching_enabled" - }, - "block_exploits": { - "$ref": "#/properties/block_exploits" - }, - "allow_websocket_upgrade": { - "$ref": "#/properties/allow_websocket_upgrade" - }, - "npmplus_fancyindex_upstream_compression": { - "description": "This just exists so that old custom locations don't break", - "type": "boolean", - "example": false - }, "npmplus_noindex": { "$ref": "#/properties/npmplus_noindex" }, @@ -251,16 +232,6 @@ "npmplus_proxy_response_buffering": { "$ref": "#/properties/npmplus_proxy_response_buffering" }, - "npmplus_disable_uri_sanitisation": { - "description": "This just exists so that old custom locations don't break", - "type": "boolean", - "example": false - }, - "npmplus_spoof_host_header": { - "description": "This just exists so that old custom locations don't break", - "type": "boolean", - "example": false - }, "npmplus_upstream_compression": { "$ref": "#/properties/npmplus_upstream_compression" }, @@ -279,9 +250,6 @@ "advanced_config": { "type": "string" }, - "npmplus_location_config": { - "type": "string" - }, "npmplus_access_list_ids":{ "$ref": "../common.json#/properties/npmplus_access_list_ids" }, @@ -307,11 +275,6 @@ "hsts_subdomains": { "$ref": "../common.json#/properties/hsts_subdomains" }, - "trust_forwarded_proto":{ - "type": "boolean", - "description": "Trust the forwarded headers", - "example": false - }, "certificate": { "oneOf": [ { diff --git a/backend/schema/components/redirection-host-object.json b/backend/schema/components/redirection-host-object.json index e954d3195c..23232e9bdf 100644 --- a/backend/schema/components/redirection-host-object.json +++ b/backend/schema/components/redirection-host-object.json @@ -15,12 +15,14 @@ "ssl_forced", "hsts_enabled", "hsts_subdomains", - "http2_support", "npmplus_http3_support", - "block_exploits", "advanced_config", "enabled", - "meta" + "npmplus_nginx_online", + "npmplus_nginx_err", + "npmplus_directory", + "npmplus_mtls_certificate_id", + "npmplus_mtls_verify_client_optional" ], "additionalProperties": false, "properties": { @@ -81,15 +83,9 @@ "hsts_subdomains": { "$ref": "../common.json#/properties/hsts_subdomains" }, - "http2_support": { - "$ref": "../common.json#/properties/http2_support" - }, "npmplus_http3_support": { "$ref": "../common.json#/properties/npmplus_http3_support" }, - "block_exploits": { - "$ref": "../common.json#/properties/block_exploits" - }, "advanced_config": { "type": "string", "example": "" @@ -97,12 +93,20 @@ "enabled": { "$ref": "../common.json#/properties/enabled" }, - "meta": { - "type": "object", - "example": { - "nginx_online": true, - "nginx_err": null - } + "npmplus_nginx_online": { + "$ref": "../common.json#/properties/npmplus_nginx_online" + }, + "npmplus_nginx_err": { + "$ref": "../common.json#/properties/npmplus_nginx_err" + }, + "npmplus_directory": { + "$ref": "../common.json#/properties/npmplus_directory" + }, + "npmplus_mtls_certificate_id": { + "$ref": "../common.json#/properties/npmplus_mtls_certificate_id" + }, + "npmplus_mtls_verify_client_optional": { + "$ref": "../common.json#/properties/npmplus_mtls_verify_client_optional" }, "certificate": { "oneOf": [ diff --git a/backend/schema/components/stream-object.json b/backend/schema/components/stream-object.json index 7c7ad4f36e..5c474f72bb 100644 --- a/backend/schema/components/stream-object.json +++ b/backend/schema/components/stream-object.json @@ -17,7 +17,11 @@ "npmplus_advanced_config", "npmplus_description", "certificate_id", - "meta" + "npmplus_nginx_online", + "npmplus_nginx_err", + "npmplus_directory", + "npmplus_mtls_certificate_id", + "npmplus_mtls_verify_client_optional" ], "additionalProperties": false, "properties": { @@ -82,9 +86,20 @@ "type": "string", "example": "" }, - "meta": { - "type": "object", - "example": {} + "npmplus_nginx_online": { + "$ref": "../common.json#/properties/npmplus_nginx_online" + }, + "npmplus_nginx_err": { + "$ref": "../common.json#/properties/npmplus_nginx_err" + }, + "npmplus_directory": { + "$ref": "../common.json#/properties/npmplus_directory" + }, + "npmplus_mtls_certificate_id": { + "$ref": "../common.json#/properties/npmplus_mtls_certificate_id" + }, + "npmplus_mtls_verify_client_optional": { + "$ref": "../common.json#/properties/npmplus_mtls_verify_client_optional" }, "npmplus_description": { "type": "string", diff --git a/backend/schema/paths/nginx/access-lists/get.json b/backend/schema/paths/nginx/access-lists/get.json index 8c8083a94c..38bc7e3dc0 100644 --- a/backend/schema/paths/nginx/access-lists/get.json +++ b/backend/schema/paths/nginx/access-lists/get.json @@ -19,8 +19,7 @@ "enum": [ "owner", "items", - "clients", - "proxy_hosts" + "clients" ] } } @@ -36,7 +35,6 @@ "modified_on": "2024-10-08T22:15:40.000Z", "owner_user_id": 1, "name": "test1234", - "meta": {}, "satisfy_any": true, "pass_auth": false, "proxy_host_count": 0 diff --git a/backend/schema/paths/nginx/access-lists/listID/delete.json b/backend/schema/paths/nginx/access-lists/listID/delete.json index eda633b7fb..f80b14b53c 100644 --- a/backend/schema/paths/nginx/access-lists/listID/delete.json +++ b/backend/schema/paths/nginx/access-lists/listID/delete.json @@ -29,11 +29,48 @@ "application/json": { "examples": { "default": { - "value": true + "value": { + "id": 1, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:34:34.000Z", + "owner_user_id": 1, + "name": "test123!!", + "satisfy_any": true, + "pass_auth": false, + "proxy_host_count": 0, + "items": [ + { + "id": 1, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "access_list_id": 1, + "username": "admin", + "password": "" + }, + { + "id": 2, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "access_list_id": 1, + "username": "asdad", + "password": "" + } + ], + "clients": [ + { + "id": 1, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "access_list_id": 1, + "address": "127.0.0.1", + "directive": "allow" + } + ] + } } }, "schema": { - "type": "boolean" + "$ref": "../../../../components/access-list-object.json" } } } diff --git a/backend/schema/paths/nginx/access-lists/listID/get.json b/backend/schema/paths/nginx/access-lists/listID/get.json index 837c469ac3..c430c585d2 100644 --- a/backend/schema/paths/nginx/access-lists/listID/get.json +++ b/backend/schema/paths/nginx/access-lists/listID/get.json @@ -35,7 +35,6 @@ "modified_on": "2025-10-29T22:48:20.000Z", "owner_user_id": 1, "name": "My Access List", - "meta": {}, "satisfy_any": false, "pass_auth": false, "proxy_host_count": 1 diff --git a/backend/schema/paths/nginx/access-lists/listID/put.json b/backend/schema/paths/nginx/access-lists/listID/put.json index 1d58010b10..5c9d5f5849 100644 --- a/backend/schema/paths/nginx/access-lists/listID/put.json +++ b/backend/schema/paths/nginx/access-lists/listID/put.json @@ -82,22 +82,9 @@ "modified_on": "2024-10-08T22:34:34.000Z", "owner_user_id": 1, "name": "test123!!", - "meta": {}, "satisfy_any": true, "pass_auth": false, "proxy_host_count": 0, - "owner": { - "id": 1, - "created_on": "2024-10-07T22:43:55.000Z", - "modified_on": "2024-10-08T12:52:54.000Z", - "is_disabled": false, - "email": "admin@example.com", - "name": "Administrator", - "avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm", - "roles": [ - "admin" - ] - }, "items": [ { "id": 1, @@ -105,8 +92,7 @@ "modified_on": "2024-10-08T22:15:40.000Z", "access_list_id": 1, "username": "admin", - "password": "", - "meta": {} + "password": "" }, { "id": 2, @@ -114,8 +100,7 @@ "modified_on": "2024-10-08T22:15:40.000Z", "access_list_id": 1, "username": "asdad", - "password": "", - "meta": {} + "password": "" } ], "clients": [ @@ -125,11 +110,9 @@ "modified_on": "2024-10-08T22:15:40.000Z", "access_list_id": 1, "address": "127.0.0.1", - "directive": "allow", - "meta": {} + "directive": "allow" } - ], - "proxy_hosts": [] + ] } } }, diff --git a/backend/schema/paths/nginx/access-lists/post.json b/backend/schema/paths/nginx/access-lists/post.json index 45afb866dd..7b8546e564 100644 --- a/backend/schema/paths/nginx/access-lists/post.json +++ b/backend/schema/paths/nginx/access-lists/post.json @@ -71,22 +71,9 @@ "modified_on": "2024-10-08T22:15:40.000Z", "owner_user_id": 1, "name": "test1234", - "meta": {}, "satisfy_any": true, "pass_auth": false, "proxy_host_count": 0, - "owner": { - "id": 1, - "created_on": "2024-10-07T22:43:55.000Z", - "modified_on": "2024-10-08T12:52:54.000Z", - "is_disabled": false, - "email": "admin@example.com", - "name": "Administrator", - "avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm", - "roles": [ - "admin" - ] - }, "items": [ { "id": 1, @@ -94,8 +81,7 @@ "modified_on": "2024-10-08T22:15:40.000Z", "access_list_id": 1, "username": "admin", - "password": "", - "meta": {} + "password": "" }, { "id": 2, @@ -103,11 +89,9 @@ "modified_on": "2024-10-08T22:15:40.000Z", "access_list_id": 1, "username": "asdad", - "password": "", - "meta": {} + "password": "" } ], - "proxy_hosts": [], "clients": [ { "id": 1, @@ -115,8 +99,7 @@ "modified_on": "2024-10-08T22:15:40.000Z", "access_list_id": 1, "address": "127.0.0.1", - "directive": "allow", - "meta": {} + "directive": "allow" } ] } diff --git a/backend/schema/paths/nginx/certificates/certID/delete.json b/backend/schema/paths/nginx/certificates/certID/delete.json index c51a1d50b7..76e460e205 100644 --- a/backend/schema/paths/nginx/certificates/certID/delete.json +++ b/backend/schema/paths/nginx/certificates/certID/delete.json @@ -29,11 +29,26 @@ "application/json": { "examples": { "default": { - "value": true + "value": { + "id": 4, + "created_on": "2024-10-09T05:31:58.000Z", + "modified_on": "2024-10-09T05:32:11.000Z", + "owner_user_id": 1, + "provider": "letsencrypt", + "nice_name": "test.example.com", + "domain_names": [ + "test.example.com" + ], + "expires_on": "2025-01-07T04:34:18.000Z", + "npmplus_reuse_key": false, + "npmplus_dns_challenge": false, + "npmplus_dns_provider": "", + "npmplus_propagation_seconds": 0 + } } }, "schema": { - "type": "boolean" + "$ref": "../../../../components/certificate-object.json" } } } diff --git a/backend/schema/paths/nginx/certificates/certID/get.json b/backend/schema/paths/nginx/certificates/certID/get.json index 966bb14fc8..f5b4ad6482 100644 --- a/backend/schema/paths/nginx/certificates/certID/get.json +++ b/backend/schema/paths/nginx/certificates/certID/get.json @@ -40,9 +40,10 @@ "test.example.com" ], "expires_on": "2025-01-07T04:34:18.000Z", - "meta": { - "dns_challenge": false - } + "npmplus_reuse_key": false, + "npmplus_dns_challenge": false, + "npmplus_dns_provider": "", + "npmplus_propagation_seconds": 0 } } }, diff --git a/backend/schema/paths/nginx/certificates/certID/renew/post.json b/backend/schema/paths/nginx/certificates/certID/renew/post.json index 4080be09e0..be4805787d 100644 --- a/backend/schema/paths/nginx/certificates/certID/renew/post.json +++ b/backend/schema/paths/nginx/certificates/certID/renew/post.json @@ -40,9 +40,10 @@ "domain_names": [ "test.jc21.supernerd.pro" ], - "meta": { - "dns_challenge": false - } + "npmplus_reuse_key": false, + "npmplus_dns_challenge": false, + "npmplus_dns_provider": "", + "npmplus_propagation_seconds": 0 } } }, diff --git a/backend/schema/paths/nginx/certificates/certID/upload/post.json b/backend/schema/paths/nginx/certificates/certID/upload/post.json index 5c92bc4c71..4b45691166 100644 --- a/backend/schema/paths/nginx/certificates/certID/upload/post.json +++ b/backend/schema/paths/nginx/certificates/certID/upload/post.json @@ -33,30 +33,25 @@ "examples": { "default": { "value": { - "certificate": "-----BEGIN CERTIFICATE-----\nMIIEYDCCAsigAwIBAgIRAPoSC0hvitb26ODMlsH6YbowDQYJKoZIhvcNAQELBQAw\ngZExHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEzMDEGA1UECwwqamN1\ncm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJub3cpMTowOAYDVQQD\nDDFta2NlcnQgamN1cm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJu\nb3cpMB4XDTI0MTAwOTA3MjIxN1oXDTI3MDEwOTA3MjIxN1owXjEnMCUGA1UEChMe\nbWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTMwMQYDVQQLDCpqY3Vybm93\nQEphbWllcy1MYXB0b3AubG9jYWwgKEphbWllIEN1cm5vdykwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQC1n9j9C5Bes1ndqACDckERauxXVNKCnUlUM1bu\nGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2wrbmvZvLuPmXePOKbIKS+XXh+\n2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHgeYz6Cv/Si2/LJPCh/CoBfM4hU\nQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQoxRAHiOR9081Xn1WeoKr7kVB\nIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7ZEo+nS8Wr/4QWicatIWZXpVaE\nOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79XzGONeH1PAgMBAAGjZTBjMA4G\nA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBSB\n/vfmBUd4W7CvyEMl7YpMVQs8vTAbBgNVHREEFDASghB0ZXN0LmV4YW1wbGUuY29t\nMA0GCSqGSIb3DQEBCwUAA4IBgQASwON/jPAHzcARSenY0ZGY1m5OVTYoQ/JWH0oy\nl8SyFCQFEXt7UHDD/eTtLT0vMyc190nP57P8lTnZGf7hSinZz1B1d6V4cmzxpk0s\nVXZT+irL6bJVJoMBHRpllKAhGULIo33baTrWFKA0oBuWx4AevSWKcLW5j87kEawn\nATCuMQ1I3ifR1mSlB7X8fb+vF+571q0NGuB3a42j6rdtXJ6SmH4+9B4qO0sfHDNt\nIImpLCH/tycDpcYrGSCn1QrekFG1bSEh+Bb9i8rqMDSDsYrTFPZTuOQ3EtjGni9u\nm+rEP3OyJg+md8c+0LVP7/UU4QWWnw3/Wolo5kSCxE8vNTFqi4GhVbdLnUtcIdTV\nXxuR6cKyW87Snj1a0nG76ZLclt/akxDhtzqeV60BO0p8pmiev8frp+E94wFNYCmp\n1cr3CnMEGRaficLSDFC6EBENzlZW2BQT6OMIV+g0NBgSyQe39s2zcdEl5+SzDVuw\nhp8bJUp/QN7pnOVCDbjTQ+HVMXw=\n-----END CERTIFICATE-----\n", - "certificate_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1n9j9C5Bes1nd\nqACDckERauxXVNKCnUlUM1buGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2w\nrbmvZvLuPmXePOKbIKS+XXh+2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHge\nYz6Cv/Si2/LJPCh/CoBfM4hUQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQ\noxRAHiOR9081Xn1WeoKr7kVBIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7Z\nEo+nS8Wr/4QWicatIWZXpVaEOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79X\nzGONeH1PAgMBAAECggEAANb3Wtwl07pCjRrMvc7WbC0xYIn82yu8/g2qtjkYUJcU\nia5lQbYN7RGCS85Oc/tkq48xQEG5JQWNH8b918jDEMTrFab0aUEyYcru1q9L8PL6\nYHaNgZSrMrDcHcS8h0QOXNRJT5jeGkiHJaTR0irvB526tqF3knbK9yW22KTfycUe\na0Z9voKn5xRk1DCbHi/nk2EpT7xnjeQeLFaTIRXbS68omkr4YGhwWm5OizoyEGZu\nW0Zum5BkQyMr6kor3wdxOTG97ske2rcyvvHi+ErnwL0xBv0qY0Dhe8DpuXpDezqw\no72yY8h31Fu84i7sAj24YuE5Df8DozItFXQpkgbQ6QKBgQDPrufhvIFm2S/MzBdW\nH8JxY7CJlJPyxOvc1NIl9RczQGAQR90kx52cgIcuIGEG6/wJ/xnGfMmW40F0DnQ+\nN+oLgB9SFxeLkRb7s9Z/8N3uIN8JJFYcerEOiRQeN2BXEEWJ7bUThNtsVrAcKoUh\nELsDmnHW/3V+GKwhd0vpk842+wKBgQDf4PGLG9PTE5tlAoyHFodJRd2RhTJQkwsU\nMDNjLJ+KecLv+Nl+QiJhoflG1ccqtSFlBSCG067CDQ5LV0xm3mLJ7pfJoMgjcq31\nqjEmX4Ls91GuVOPtbwst3yFKjsHaSoKB5fBvWRcKFpBUezM7Qcw2JP3+dQT+bQIq\ncMTkRWDSvQKBgQDOdCQFDjxg/lR7NQOZ1PaZe61aBz5P3pxNqa7ClvMaOsuEQ7w9\nvMYcdtRq8TsjA2JImbSI0TIg8gb2FQxPcYwTJKl+FICOeIwtaSg5hTtJZpnxX5LO\nutTaC0DZjNkTk5RdOdWA8tihyUdGqKoxJY2TVmwGe2rUEDjFB++J4inkEwKBgB6V\ng0nmtkxanFrzOzFlMXwgEEHF+Xaqb9QFNa/xs6XeNnREAapO7JV75Cr6H2hFMFe1\nmJjyqCgYUoCWX3iaHtLJRnEkBtNY4kzyQB6m46LtsnnnXO/dwKA2oDyoPfFNRoDq\nYatEd3JIXNU9s2T/+x7WdOBjKhh72dTkbPFmTPDdAoGAU6rlPBevqOFdObYxdPq8\nEQWu44xqky3Mf5sBpOwtu6rqCYuziLiN7K4sjN5GD5mb1cEU+oS92ZiNcUQ7MFXk\n8yTYZ7U0VcXyAcpYreWwE8thmb0BohJBr+Mp3wLTx32x0HKdO6vpUa0d35LUTUmM\nRrKmPK/msHKK/sVHiL+NFqo=\n-----END PRIVATE KEY-----\n" + "id": 4, + "created_on": "2024-10-09T05:31:58.000Z", + "modified_on": "2024-10-09T05:32:11.000Z", + "owner_user_id": 1, + "provider": "letsencrypt", + "nice_name": "test.example.com", + "domain_names": [ + "test.example.com" + ], + "expires_on": "2025-01-07T04:34:18.000Z", + "npmplus_reuse_key": false, + "npmplus_dns_challenge": false, + "npmplus_dns_provider": "", + "npmplus_propagation_seconds": 0 } } }, "schema": { - "type": "object", - "additionalProperties": false, - "required": [ - "certificate", - "certificate_key" - ], - "properties": { - "certificate": { - "type": "string", - "minLength": 1, - "example": "-----BEGIN CERTIFICATE-----\nMIID...-----END CERTIFICATE-----" - }, - "certificate_key": { - "type": "string", - "minLength": 1, - "example": "-----BEGIN CERTIFICATE-----\nMIID...-----END CERTIFICATE-----" - } - } + "$ref": "../../../../../components/certificate-object.json" } } } diff --git a/backend/schema/paths/nginx/certificates/get.json b/backend/schema/paths/nginx/certificates/get.json index 1e739ce7e4..c69cefae10 100644 --- a/backend/schema/paths/nginx/certificates/get.json +++ b/backend/schema/paths/nginx/certificates/get.json @@ -17,7 +17,15 @@ "schema": { "type": "string", "enum": [ - "owner" + "owner", + "proxy_hosts", + "redirection_hosts", + "dead_hosts", + "streams", + "mtls_proxy_hosts", + "mtls_redirection_hosts", + "mtls_dead_hosts", + "mtls_streams" ] } } @@ -41,9 +49,10 @@ "test.example.com" ], "expires_on": "2025-01-07T04:34:18.000Z", - "meta": { - "dns_challenge": false - } + "npmplus_reuse_key": false, + "npmplus_dns_challenge": false, + "npmplus_dns_provider": "", + "npmplus_propagation_seconds": 0 } ] } diff --git a/backend/schema/paths/nginx/certificates/post.json b/backend/schema/paths/nginx/certificates/post.json index 8208071a3e..112759560f 100644 --- a/backend/schema/paths/nginx/certificates/post.json +++ b/backend/schema/paths/nginx/certificates/post.json @@ -30,8 +30,20 @@ "domain_names": { "$ref": "../../../components/certificate-object.json#/properties/domain_names" }, - "meta": { - "$ref": "../../../components/certificate-object.json#/properties/meta" + "npmplus_reuse_key": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_reuse_key" + }, + "npmplus_dns_challenge": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_dns_challenge" + }, + "npmplus_dns_provider": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_dns_provider" + }, + "npmplus_dns_provider_credentials": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_dns_provider_credentials" + }, + "npmplus_propagation_seconds": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_propagation_seconds" } } }, @@ -39,10 +51,7 @@ "provider": "letsencrypt", "domain_names": [ "test.example.com" - ], - "meta": { - "dns_challenge": false - } + ] } } } @@ -65,17 +74,10 @@ "domain_names": [ "test.example.com" ], - "meta": { - "dns_challenge": false, - "letsencrypt_certificate": { - "cn": "test.example.com", - "issuer": "C = US, O = Let's Encrypt, CN = E5", - "dates": { - "from": 1728448218, - "to": 1736224217 - } - } - } + "npmplus_reuse_key": false, + "npmplus_dns_challenge": false, + "npmplus_dns_provider": "", + "npmplus_propagation_seconds": 0 } } }, diff --git a/backend/schema/paths/nginx/dead-hosts/get.json b/backend/schema/paths/nginx/dead-hosts/get.json index 8fedc35ee8..cc5643d7f3 100644 --- a/backend/schema/paths/nginx/dead-hosts/get.json +++ b/backend/schema/paths/nginx/dead-hosts/get.json @@ -42,11 +42,11 @@ "certificate_id": 0, "ssl_forced": false, "advanced_config": "", - "meta": { - "nginx_online": true, - "nginx_err": null - }, - "http2_support": false, + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, "npmplus_http3_support": false, "enabled": true, "hsts_enabled": false, diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/delete.json b/backend/schema/paths/nginx/dead-hosts/hostID/delete.json index 2044ae9498..500d8e2a51 100644 --- a/backend/schema/paths/nginx/dead-hosts/hostID/delete.json +++ b/backend/schema/paths/nginx/dead-hosts/hostID/delete.json @@ -29,11 +29,31 @@ "application/json": { "examples": { "default": { - "value": true + "value": { + "id": 1, + "created_on": "2024-10-09T01:38:52.000Z", + "modified_on": "2024-10-09T01:46:06.000Z", + "owner_user_id": 1, + "domain_names": [ + "test.example.com" + ], + "certificate_id": 0, + "ssl_forced": false, + "advanced_config": "", + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, + "npmplus_http3_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false + } } }, "schema": { - "type": "boolean" + "$ref": "../../../../components/dead-host-object.json" } } } diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/disable/post.json b/backend/schema/paths/nginx/dead-hosts/hostID/disable/post.json index e1367042d3..6834f1cc18 100644 --- a/backend/schema/paths/nginx/dead-hosts/hostID/disable/post.json +++ b/backend/schema/paths/nginx/dead-hosts/hostID/disable/post.json @@ -29,11 +29,31 @@ "application/json": { "examples": { "default": { - "value": true + "value": { + "id": 1, + "created_on": "2024-10-09T01:38:52.000Z", + "modified_on": "2024-10-09T01:46:06.000Z", + "owner_user_id": 1, + "domain_names": [ + "test.example.com" + ], + "certificate_id": 0, + "ssl_forced": false, + "advanced_config": "", + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, + "npmplus_http3_support": false, + "enabled": false, + "hsts_enabled": false, + "hsts_subdomains": false + } } }, "schema": { - "type": "boolean" + "$ref": "../../../../../components/dead-host-object.json" } } } diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/enable/post.json b/backend/schema/paths/nginx/dead-hosts/hostID/enable/post.json index e77e67415e..c70e515f38 100644 --- a/backend/schema/paths/nginx/dead-hosts/hostID/enable/post.json +++ b/backend/schema/paths/nginx/dead-hosts/hostID/enable/post.json @@ -29,11 +29,31 @@ "application/json": { "examples": { "default": { - "value": true + "value": { + "id": 1, + "created_on": "2024-10-09T01:38:52.000Z", + "modified_on": "2024-10-09T01:46:06.000Z", + "owner_user_id": 1, + "domain_names": [ + "test.example.com" + ], + "certificate_id": 0, + "ssl_forced": false, + "advanced_config": "", + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, + "npmplus_http3_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false + } } }, "schema": { - "type": "boolean" + "$ref": "../../../../../components/dead-host-object.json" } } } diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/get.json b/backend/schema/paths/nginx/dead-hosts/hostID/get.json index 5d807c101d..e451485500 100644 --- a/backend/schema/paths/nginx/dead-hosts/hostID/get.json +++ b/backend/schema/paths/nginx/dead-hosts/hostID/get.json @@ -40,11 +40,11 @@ "certificate_id": 0, "ssl_forced": false, "advanced_config": "", - "meta": { - "nginx_online": true, - "nginx_err": null - }, - "http2_support": false, + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, "npmplus_http3_support": false, "enabled": true, "hsts_enabled": false, diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/put.json b/backend/schema/paths/nginx/dead-hosts/hostID/put.json index 191f4cd347..babc468107 100644 --- a/backend/schema/paths/nginx/dead-hosts/hostID/put.json +++ b/backend/schema/paths/nginx/dead-hosts/hostID/put.json @@ -47,17 +47,35 @@ "hsts_subdomains": { "$ref": "../../../../components/dead-host-object.json#/properties/hsts_subdomains" }, - "http2_support": { - "$ref": "../../../../components/dead-host-object.json#/properties/http2_support" - }, "npmplus_http3_support": { "$ref": "../../../../components/dead-host-object.json#/properties/npmplus_http3_support" }, "advanced_config": { "$ref": "../../../../components/dead-host-object.json#/properties/advanced_config" }, - "meta": { - "$ref": "../../../../components/dead-host-object.json#/properties/meta" + "npmplus_directory": { + "$ref": "../../../../components/dead-host-object.json#/properties/npmplus_directory" + }, + "npmplus_mtls_certificate_id": { + "$ref": "../../../../components/dead-host-object.json#/properties/npmplus_mtls_certificate_id" + }, + "npmplus_mtls_verify_client_optional": { + "$ref": "../../../../components/dead-host-object.json#/properties/npmplus_mtls_verify_client_optional" + }, + "npmplus_reuse_key": { + "$ref": "../../../../components/certificate-object.json#/properties/npmplus_reuse_key" + }, + "npmplus_dns_challenge": { + "$ref": "../../../../components/certificate-object.json#/properties/npmplus_dns_challenge" + }, + "npmplus_dns_provider": { + "$ref": "../../../../components/certificate-object.json#/properties/npmplus_dns_provider" + }, + "npmplus_dns_provider_credentials": { + "$ref": "../../../../components/certificate-object.json#/properties/npmplus_dns_provider_credentials" + }, + "npmplus_propagation_seconds": { + "$ref": "../../../../components/certificate-object.json#/properties/npmplus_propagation_seconds" } } } @@ -82,28 +100,15 @@ "certificate_id": 0, "ssl_forced": false, "advanced_config": "", - "meta": { - "nginx_online": true, - "nginx_err": null - }, - "http2_support": false, + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, "npmplus_http3_support": false, "enabled": true, "hsts_enabled": false, - "hsts_subdomains": false, - "owner": { - "id": 1, - "created_on": "2024-10-09T00:59:56.000Z", - "modified_on": "2024-10-09T00:59:56.000Z", - "is_disabled": false, - "email": "admin@example.com", - "name": "Administrator", - "avatar": "", - "roles": [ - "admin" - ] - }, - "certificate": null + "hsts_subdomains": false } } }, diff --git a/backend/schema/paths/nginx/dead-hosts/post.json b/backend/schema/paths/nginx/dead-hosts/post.json index 71b0cc9e5e..0e310dc140 100644 --- a/backend/schema/paths/nginx/dead-hosts/post.json +++ b/backend/schema/paths/nginx/dead-hosts/post.json @@ -36,17 +36,35 @@ "hsts_subdomains": { "$ref": "../../../components/dead-host-object.json#/properties/hsts_subdomains" }, - "http2_support": { - "$ref": "../../../components/dead-host-object.json#/properties/http2_support" - }, "npmplus_http3_support": { "$ref": "../../../components/dead-host-object.json#/properties/npmplus_http3_support" }, "advanced_config": { "$ref": "../../../components/dead-host-object.json#/properties/advanced_config" }, - "meta": { - "$ref": "../../../components/dead-host-object.json#/properties/meta" + "npmplus_directory": { + "$ref": "../../../components/dead-host-object.json#/properties/npmplus_directory" + }, + "npmplus_mtls_certificate_id": { + "$ref": "../../../components/dead-host-object.json#/properties/npmplus_mtls_certificate_id" + }, + "npmplus_mtls_verify_client_optional": { + "$ref": "../../../components/dead-host-object.json#/properties/npmplus_mtls_verify_client_optional" + }, + "npmplus_reuse_key": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_reuse_key" + }, + "npmplus_dns_challenge": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_dns_challenge" + }, + "npmplus_dns_provider": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_dns_provider" + }, + "npmplus_dns_provider_credentials": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_dns_provider_credentials" + }, + "npmplus_propagation_seconds": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_propagation_seconds" } } }, @@ -57,11 +75,9 @@ "certificate_id": 0, "ssl_forced": false, "advanced_config": "", - "http2_support": false, "npmplus_http3_support": false, "hsts_enabled": false, - "hsts_subdomains": false, - "meta": {} + "hsts_subdomains": false } } } @@ -84,25 +100,15 @@ "certificate_id": 0, "ssl_forced": false, "advanced_config": "", - "meta": {}, - "http2_support": false, + "npmplus_nginx_online": false, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, "npmplus_http3_support": false, "enabled": true, "hsts_enabled": false, - "hsts_subdomains": false, - "certificate": null, - "owner": { - "id": 1, - "created_on": "2024-10-09T00:59:56.000Z", - "modified_on": "2024-10-09T00:59:56.000Z", - "is_disabled": false, - "email": "admin@example.com", - "name": "Administrator", - "avatar": "", - "roles": [ - "admin" - ] - } + "hsts_subdomains": false } } }, diff --git a/backend/schema/paths/nginx/proxy-hosts/get.json b/backend/schema/paths/nginx/proxy-hosts/get.json index c3ece3a458..44fd37a387 100644 --- a/backend/schema/paths/nginx/proxy-hosts/get.json +++ b/backend/schema/paths/nginx/proxy-hosts/get.json @@ -46,9 +46,6 @@ "npmplus_access_list_type": "public", "certificate_id": 1, "ssl_forced": false, - "caching_enabled": false, - "block_exploits": false, - "allow_websocket_upgrade": false, "npmplus_noindex": false, "npmplus_crowdsec_appsec": false, "npmplus_proxy_request_buffering": false, @@ -60,18 +57,17 @@ "npmplus_auth_request_upstream": "", "advanced_config": "", "npmplus_location_config": "", - "meta": { - "nginx_online": true, - "nginx_err": null - }, - "http2_support": false, + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, "npmplus_http3_support": false, "forward_scheme": "http", "enabled": true, "locations": [], "hsts_enabled": false, - "hsts_subdomains": false, - "trust_forwarded_proto": false + "hsts_subdomains": false } ] } diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/delete.json b/backend/schema/paths/nginx/proxy-hosts/hostID/delete.json index b70d25d453..7675accc5c 100644 --- a/backend/schema/paths/nginx/proxy-hosts/hostID/delete.json +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/delete.json @@ -29,11 +29,47 @@ "application/json": { "examples": { "default": { - "value": true + "value": { + "id": 3, + "created_on": "2025-10-30T01:12:05.000Z", + "modified_on": "2025-10-30T01:17:06.000Z", + "owner_user_id": 1, + "domain_names": [ + "test.example.com" + ], + "forward_host": "127.0.0.1", + "forward_port": 8080, + "npmplus_access_list_ids": [], + "npmplus_access_list_type": "public", + "certificate_id": 0, + "ssl_forced": false, + "npmplus_noindex": false, + "npmplus_crowdsec_appsec": false, + "npmplus_proxy_request_buffering": false, + "npmplus_proxy_response_buffering": false, + "npmplus_upstream_compression": false, + "npmplus_fancyindex": false, + "npmplus_x_frame_options": "DENY", + "npmplus_auth_request": "none", + "npmplus_auth_request_upstream": "", + "advanced_config": "", + "npmplus_location_config": "", + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, + "npmplus_http3_support": false, + "forward_scheme": "http", + "enabled": true, + "locations": [], + "hsts_enabled": false, + "hsts_subdomains": false + } } }, "schema": { - "type": "boolean" + "$ref": "../../../../components/proxy-host-object.json" } } } 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 8b0c87de0c..cbb078656b 100644 --- a/backend/schema/paths/nginx/proxy-hosts/hostID/disable/post.json +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/disable/post.json @@ -29,11 +29,47 @@ "application/json": { "examples": { "default": { - "value": true + "value": { + "id": 3, + "created_on": "2025-10-30T01:12:05.000Z", + "modified_on": "2025-10-30T01:17:06.000Z", + "owner_user_id": 1, + "domain_names": [ + "test.example.com" + ], + "forward_host": "127.0.0.1", + "forward_port": 8080, + "npmplus_access_list_ids": [], + "npmplus_access_list_type": "public", + "certificate_id": 0, + "ssl_forced": false, + "npmplus_noindex": false, + "npmplus_crowdsec_appsec": false, + "npmplus_proxy_request_buffering": false, + "npmplus_proxy_response_buffering": false, + "npmplus_upstream_compression": false, + "npmplus_fancyindex": false, + "npmplus_x_frame_options": "DENY", + "npmplus_auth_request": "none", + "npmplus_auth_request_upstream": "", + "advanced_config": "", + "npmplus_location_config": "", + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, + "npmplus_http3_support": false, + "forward_scheme": "http", + "enabled": false, + "locations": [], + "hsts_enabled": false, + "hsts_subdomains": false + } } }, "schema": { - "type": "boolean" + "$ref": "../../../../../components/proxy-host-object.json" } } } 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 59dbe42bed..5caa795ac4 100644 --- a/backend/schema/paths/nginx/proxy-hosts/hostID/enable/post.json +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/enable/post.json @@ -29,11 +29,47 @@ "application/json": { "examples": { "default": { - "value": true + "value": { + "id": 3, + "created_on": "2025-10-30T01:12:05.000Z", + "modified_on": "2025-10-30T01:17:06.000Z", + "owner_user_id": 1, + "domain_names": [ + "test.example.com" + ], + "forward_host": "127.0.0.1", + "forward_port": 8080, + "npmplus_access_list_ids": [], + "npmplus_access_list_type": "public", + "certificate_id": 0, + "ssl_forced": false, + "npmplus_noindex": false, + "npmplus_crowdsec_appsec": false, + "npmplus_proxy_request_buffering": false, + "npmplus_proxy_response_buffering": false, + "npmplus_upstream_compression": false, + "npmplus_fancyindex": false, + "npmplus_x_frame_options": "DENY", + "npmplus_auth_request": "none", + "npmplus_auth_request_upstream": "", + "advanced_config": "", + "npmplus_location_config": "", + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, + "npmplus_http3_support": false, + "forward_scheme": "http", + "enabled": true, + "locations": [], + "hsts_enabled": false, + "hsts_subdomains": false + } } }, "schema": { - "type": "boolean" + "$ref": "../../../../../components/proxy-host-object.json" } } } diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/get.json b/backend/schema/paths/nginx/proxy-hosts/hostID/get.json index e1aa39db6d..75dfd78828 100644 --- a/backend/schema/paths/nginx/proxy-hosts/hostID/get.json +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/get.json @@ -43,9 +43,6 @@ "npmplus_access_list_type": "public", "certificate_id": 0, "ssl_forced": false, - "caching_enabled": false, - "block_exploits": false, - "allow_websocket_upgrade": false, "npmplus_noindex": false, "npmplus_crowdsec_appsec": false, "npmplus_proxy_request_buffering": false, @@ -57,30 +54,17 @@ "npmplus_auth_request_upstream": "", "advanced_config": "", "npmplus_location_config": "", - "meta": { - "nginx_online": true, - "nginx_err": null - }, - "http2_support": false, + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, "npmplus_http3_support": false, "forward_scheme": "http", "enabled": true, "locations": [], "hsts_enabled": false, - "hsts_subdomains": false, - "trust_forwarded_proto": false, - "owner": { - "id": 1, - "created_on": "2025-10-28T00:50:24.000Z", - "modified_on": "2025-10-28T00:50:24.000Z", - "is_disabled": false, - "email": "jc@jc21.com", - "name": "jamiec", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ] - } + "hsts_subdomains": false } } }, diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json index dee7d4cd5b..818b8a4b04 100644 --- a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json @@ -56,24 +56,9 @@ "hsts_subdomains": { "$ref": "../../../../components/proxy-host-object.json#/properties/hsts_subdomains" }, - "trust_forwarded_proto": { - "$ref": "../../../../components/proxy-host-object.json#/properties/trust_forwarded_proto" - }, - "http2_support": { - "$ref": "../../../../components/proxy-host-object.json#/properties/http2_support" - }, "npmplus_http3_support": { "$ref": "../../../../components/proxy-host-object.json#/properties/npmplus_http3_support" }, - "block_exploits": { - "$ref": "../../../../components/proxy-host-object.json#/properties/block_exploits" - }, - "caching_enabled": { - "$ref": "../../../../components/proxy-host-object.json#/properties/caching_enabled" - }, - "allow_websocket_upgrade": { - "$ref": "../../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade" - }, "npmplus_noindex": { "$ref": "../../../../components/proxy-host-object.json#/properties/npmplus_noindex" }, @@ -113,8 +98,29 @@ "npmplus_location_config": { "$ref": "../../../../components/proxy-host-object.json#/properties/npmplus_location_config" }, - "meta": { - "$ref": "../../../../components/proxy-host-object.json#/properties/meta" + "npmplus_directory": { + "$ref": "../../../../components/proxy-host-object.json#/properties/npmplus_directory" + }, + "npmplus_mtls_certificate_id": { + "$ref": "../../../../components/proxy-host-object.json#/properties/npmplus_mtls_certificate_id" + }, + "npmplus_mtls_verify_client_optional": { + "$ref": "../../../../components/proxy-host-object.json#/properties/npmplus_mtls_verify_client_optional" + }, + "npmplus_reuse_key": { + "$ref": "../../../../components/certificate-object.json#/properties/npmplus_reuse_key" + }, + "npmplus_dns_challenge": { + "$ref": "../../../../components/certificate-object.json#/properties/npmplus_dns_challenge" + }, + "npmplus_dns_provider": { + "$ref": "../../../../components/certificate-object.json#/properties/npmplus_dns_provider" + }, + "npmplus_dns_provider_credentials": { + "$ref": "../../../../components/certificate-object.json#/properties/npmplus_dns_provider_credentials" + }, + "npmplus_propagation_seconds": { + "$ref": "../../../../components/certificate-object.json#/properties/npmplus_propagation_seconds" }, "locations": { "$ref": "../../../../components/proxy-host-object.json#/properties/locations" @@ -145,9 +151,6 @@ "npmplus_access_list_type": "public", "certificate_id": 0, "ssl_forced": false, - "caching_enabled": false, - "block_exploits": false, - "allow_websocket_upgrade": false, "npmplus_noindex": false, "npmplus_crowdsec_appsec": false, "npmplus_proxy_request_buffering": false, @@ -159,32 +162,17 @@ "npmplus_auth_request_upstream": "", "advanced_config": "", "npmplus_location_config": "", - "meta": { - "nginx_online": true, - "nginx_err": null - }, - "http2_support": false, + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, "npmplus_http3_support": false, "forward_scheme": "http", "enabled": true, "locations": [], "hsts_enabled": false, - "hsts_subdomains": false, - "trust_forwarded_proto": false, - "owner": { - "id": 1, - "created_on": "2025-10-28T00:50:24.000Z", - "modified_on": "2025-10-28T00:50:24.000Z", - "is_disabled": false, - "email": "jc@jc21.com", - "name": "jamiec", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ] - }, - "certificate": null, - "access_lists": [] + "hsts_subdomains": false } } }, diff --git a/backend/schema/paths/nginx/proxy-hosts/post.json b/backend/schema/paths/nginx/proxy-hosts/post.json index 29126b9123..f42d5a6c21 100644 --- a/backend/schema/paths/nginx/proxy-hosts/post.json +++ b/backend/schema/paths/nginx/proxy-hosts/post.json @@ -48,24 +48,9 @@ "hsts_subdomains": { "$ref": "../../../components/proxy-host-object.json#/properties/hsts_subdomains" }, - "trust_forwarded_proto": { - "$ref": "../../../components/proxy-host-object.json#/properties/trust_forwarded_proto" - }, - "http2_support": { - "$ref": "../../../components/proxy-host-object.json#/properties/http2_support" - }, "npmplus_http3_support": { "$ref": "../../../components/proxy-host-object.json#/properties/npmplus_http3_support" }, - "block_exploits": { - "$ref": "../../../components/proxy-host-object.json#/properties/block_exploits" - }, - "caching_enabled": { - "$ref": "../../../components/proxy-host-object.json#/properties/caching_enabled" - }, - "allow_websocket_upgrade": { - "$ref": "../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade" - }, "npmplus_access_list_ids": { "$ref": "../../../components/proxy-host-object.json#/properties/npmplus_access_list_ids" }, @@ -105,8 +90,29 @@ "npmplus_location_config": { "$ref": "../../../components/proxy-host-object.json#/properties/npmplus_location_config" }, - "meta": { - "$ref": "../../../components/proxy-host-object.json#/properties/meta" + "npmplus_directory": { + "$ref": "../../../components/proxy-host-object.json#/properties/npmplus_directory" + }, + "npmplus_mtls_certificate_id": { + "$ref": "../../../components/proxy-host-object.json#/properties/npmplus_mtls_certificate_id" + }, + "npmplus_mtls_verify_client_optional": { + "$ref": "../../../components/proxy-host-object.json#/properties/npmplus_mtls_verify_client_optional" + }, + "npmplus_reuse_key": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_reuse_key" + }, + "npmplus_dns_challenge": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_dns_challenge" + }, + "npmplus_dns_provider": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_dns_provider" + }, + "npmplus_dns_provider_credentials": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_dns_provider_credentials" + }, + "npmplus_propagation_seconds": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_propagation_seconds" }, "locations": { "$ref": "../../../components/proxy-host-object.json#/properties/locations" @@ -145,9 +151,6 @@ "npmplus_access_list_type": "public", "certificate_id": 0, "ssl_forced": false, - "caching_enabled": false, - "block_exploits": false, - "allow_websocket_upgrade": false, "npmplus_noindex": false, "npmplus_crowdsec_appsec": false, "npmplus_proxy_request_buffering": false, @@ -159,29 +162,17 @@ "npmplus_auth_request_upstream": "", "advanced_config": "", "npmplus_location_config": "", - "meta": {}, - "http2_support": false, + "npmplus_nginx_online": false, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, "npmplus_http3_support": false, "forward_scheme": "http", "enabled": true, "locations": [], "hsts_enabled": false, - "hsts_subdomains": false, - "trust_forwarded_proto": false, - "certificate": null, - "owner": { - "id": 1, - "created_on": "2025-10-28T00:50:24.000Z", - "modified_on": "2025-10-28T00:50:24.000Z", - "is_disabled": false, - "email": "jc@jc21.com", - "name": "jamiec", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ] - }, - "access_lists": [] + "hsts_subdomains": false } } }, diff --git a/backend/schema/paths/nginx/redirection-hosts/get.json b/backend/schema/paths/nginx/redirection-hosts/get.json index 7b4f4f9b2f..b684dd4f2a 100644 --- a/backend/schema/paths/nginx/redirection-hosts/get.json +++ b/backend/schema/paths/nginx/redirection-hosts/get.json @@ -43,13 +43,12 @@ "preserve_path": false, "certificate_id": 0, "ssl_forced": false, - "block_exploits": false, "advanced_config": "", - "meta": { - "nginx_online": true, - "nginx_err": null - }, - "http2_support": false, + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, "npmplus_http3_support": false, "enabled": true, "hsts_enabled": false, diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/delete.json b/backend/schema/paths/nginx/redirection-hosts/hostID/delete.json index 5354f10dc6..cfe291d6c0 100644 --- a/backend/schema/paths/nginx/redirection-hosts/hostID/delete.json +++ b/backend/schema/paths/nginx/redirection-hosts/hostID/delete.json @@ -29,11 +29,35 @@ "application/json": { "examples": { "default": { - "value": true + "value": { + "id": 1, + "created_on": "2024-10-09T01:13:12.000Z", + "modified_on": "2024-10-09T01:18:11.000Z", + "owner_user_id": 1, + "domain_names": [ + "test.example.com" + ], + "forward_domain_name": "something-else.com", + "preserve_path": false, + "certificate_id": 0, + "ssl_forced": false, + "advanced_config": "", + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, + "npmplus_http3_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false, + "forward_scheme": "http", + "forward_http_code": 301 + } } }, "schema": { - "type": "boolean" + "$ref": "../../../../components/redirection-host-object.json" } } } diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/disable/post.json b/backend/schema/paths/nginx/redirection-hosts/hostID/disable/post.json index d8f503bf68..d17808cf27 100644 --- a/backend/schema/paths/nginx/redirection-hosts/hostID/disable/post.json +++ b/backend/schema/paths/nginx/redirection-hosts/hostID/disable/post.json @@ -29,11 +29,35 @@ "application/json": { "examples": { "default": { - "value": true + "value": { + "id": 1, + "created_on": "2024-10-09T01:13:12.000Z", + "modified_on": "2024-10-09T01:18:11.000Z", + "owner_user_id": 1, + "domain_names": [ + "test.example.com" + ], + "forward_domain_name": "something-else.com", + "preserve_path": false, + "certificate_id": 0, + "ssl_forced": false, + "advanced_config": "", + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, + "npmplus_http3_support": false, + "enabled": false, + "hsts_enabled": false, + "hsts_subdomains": false, + "forward_scheme": "http", + "forward_http_code": 301 + } } }, "schema": { - "type": "boolean" + "$ref": "../../../../../components/redirection-host-object.json" } } } diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/enable/post.json b/backend/schema/paths/nginx/redirection-hosts/hostID/enable/post.json index bec8df77a6..e1801975e0 100644 --- a/backend/schema/paths/nginx/redirection-hosts/hostID/enable/post.json +++ b/backend/schema/paths/nginx/redirection-hosts/hostID/enable/post.json @@ -29,11 +29,35 @@ "application/json": { "examples": { "default": { - "value": true + "value": { + "id": 1, + "created_on": "2024-10-09T01:13:12.000Z", + "modified_on": "2024-10-09T01:18:11.000Z", + "owner_user_id": 1, + "domain_names": [ + "test.example.com" + ], + "forward_domain_name": "something-else.com", + "preserve_path": false, + "certificate_id": 0, + "ssl_forced": false, + "advanced_config": "", + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, + "npmplus_http3_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false, + "forward_scheme": "http", + "forward_http_code": 301 + } } }, "schema": { - "type": "boolean" + "$ref": "../../../../../components/redirection-host-object.json" } } } diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/get.json b/backend/schema/paths/nginx/redirection-hosts/hostID/get.json index 4eabbd604e..1a2f00043d 100644 --- a/backend/schema/paths/nginx/redirection-hosts/hostID/get.json +++ b/backend/schema/paths/nginx/redirection-hosts/hostID/get.json @@ -41,13 +41,12 @@ "preserve_path": false, "certificate_id": 0, "ssl_forced": false, - "block_exploits": false, "advanced_config": "", - "meta": { - "nginx_online": true, - "nginx_err": null - }, - "http2_support": false, + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, "npmplus_http3_support": false, "enabled": true, "hsts_enabled": false, diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/put.json b/backend/schema/paths/nginx/redirection-hosts/hostID/put.json index 96940ad66b..59f3d8b616 100644 --- a/backend/schema/paths/nginx/redirection-hosts/hostID/put.json +++ b/backend/schema/paths/nginx/redirection-hosts/hostID/put.json @@ -59,20 +59,35 @@ "hsts_subdomains": { "$ref": "../../../../components/redirection-host-object.json#/properties/hsts_subdomains" }, - "http2_support": { - "$ref": "../../../../components/redirection-host-object.json#/properties/http2_support" - }, "npmplus_http3_support": { "$ref": "../../../../components/redirection-host-object.json#/properties/npmplus_http3_support" }, - "block_exploits": { - "$ref": "../../../../components/redirection-host-object.json#/properties/block_exploits" - }, "advanced_config": { "$ref": "../../../../components/redirection-host-object.json#/properties/advanced_config" }, - "meta": { - "$ref": "../../../../components/redirection-host-object.json#/properties/meta" + "npmplus_directory": { + "$ref": "../../../../components/redirection-host-object.json#/properties/npmplus_directory" + }, + "npmplus_mtls_certificate_id": { + "$ref": "../../../../components/redirection-host-object.json#/properties/npmplus_mtls_certificate_id" + }, + "npmplus_mtls_verify_client_optional": { + "$ref": "../../../../components/redirection-host-object.json#/properties/npmplus_mtls_verify_client_optional" + }, + "npmplus_reuse_key": { + "$ref": "../../../../components/certificate-object.json#/properties/npmplus_reuse_key" + }, + "npmplus_dns_challenge": { + "$ref": "../../../../components/certificate-object.json#/properties/npmplus_dns_challenge" + }, + "npmplus_dns_provider": { + "$ref": "../../../../components/certificate-object.json#/properties/npmplus_dns_provider" + }, + "npmplus_dns_provider_credentials": { + "$ref": "../../../../components/certificate-object.json#/properties/npmplus_dns_provider_credentials" + }, + "npmplus_propagation_seconds": { + "$ref": "../../../../components/certificate-object.json#/properties/npmplus_propagation_seconds" } } } @@ -98,32 +113,18 @@ "preserve_path": false, "certificate_id": 0, "ssl_forced": false, - "block_exploits": false, "advanced_config": "", - "meta": { - "nginx_online": true, - "nginx_err": null - }, - "http2_support": false, + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, "npmplus_http3_support": false, "enabled": true, "hsts_enabled": false, "hsts_subdomains": false, "forward_scheme": "http", - "forward_http_code": 301, - "owner": { - "id": 1, - "created_on": "2024-10-09T00:59:56.000Z", - "modified_on": "2024-10-09T00:59:56.000Z", - "is_disabled": false, - "email": "admin@example.com", - "name": "Administrator", - "avatar": "", - "roles": [ - "admin" - ] - }, - "certificate": null + "forward_http_code": 301 } } }, diff --git a/backend/schema/paths/nginx/redirection-hosts/post.json b/backend/schema/paths/nginx/redirection-hosts/post.json index 5fafab4105..9619e20d93 100644 --- a/backend/schema/paths/nginx/redirection-hosts/post.json +++ b/backend/schema/paths/nginx/redirection-hosts/post.json @@ -51,20 +51,35 @@ "hsts_subdomains": { "$ref": "../../../components/redirection-host-object.json#/properties/hsts_subdomains" }, - "http2_support": { - "$ref": "../../../components/redirection-host-object.json#/properties/http2_support" - }, "npmplus_http3_support": { "$ref": "../../../components/redirection-host-object.json#/properties/npmplus_http3_support" }, - "block_exploits": { - "$ref": "../../../components/redirection-host-object.json#/properties/block_exploits" - }, "advanced_config": { "$ref": "../../../components/redirection-host-object.json#/properties/advanced_config" }, - "meta": { - "$ref": "../../../components/redirection-host-object.json#/properties/meta" + "npmplus_directory": { + "$ref": "../../../components/redirection-host-object.json#/properties/npmplus_directory" + }, + "npmplus_mtls_certificate_id": { + "$ref": "../../../components/redirection-host-object.json#/properties/npmplus_mtls_certificate_id" + }, + "npmplus_mtls_verify_client_optional": { + "$ref": "../../../components/redirection-host-object.json#/properties/npmplus_mtls_verify_client_optional" + }, + "npmplus_reuse_key": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_reuse_key" + }, + "npmplus_dns_challenge": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_dns_challenge" + }, + "npmplus_dns_provider": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_dns_provider" + }, + "npmplus_dns_provider_credentials": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_dns_provider_credentials" + }, + "npmplus_propagation_seconds": { + "$ref": "../../../components/certificate-object.json#/properties/npmplus_propagation_seconds" } } }, @@ -76,15 +91,12 @@ "forward_scheme": "auto", "forward_http_code": 301, "preserve_path": false, - "block_exploits": false, "certificate_id": 0, "ssl_forced": false, - "http2_support": false, "npmplus_http3_support": false, "hsts_enabled": false, "hsts_subdomains": false, - "advanced_config": "", - "meta": {} + "advanced_config": "" } } } @@ -108,29 +120,18 @@ "preserve_path": false, "certificate_id": 0, "ssl_forced": false, - "block_exploits": false, "advanced_config": "", - "meta": {}, - "http2_support": false, + "npmplus_nginx_online": false, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, "npmplus_http3_support": false, "enabled": true, "hsts_enabled": false, "hsts_subdomains": false, "forward_scheme": "auto", - "forward_http_code": 301, - "certificate": null, - "owner": { - "id": 1, - "created_on": "2025-10-28T00:50:24.000Z", - "modified_on": "2025-10-28T00:50:24.000Z", - "is_disabled": false, - "email": "jc@jc21.com", - "name": "jamiec", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ] - } + "forward_http_code": 301 } } }, diff --git a/backend/schema/paths/nginx/streams/get.json b/backend/schema/paths/nginx/streams/get.json index 6056edf670..19906fa756 100644 --- a/backend/schema/paths/nginx/streams/get.json +++ b/backend/schema/paths/nginx/streams/get.json @@ -44,10 +44,11 @@ "npmplus_proxy_protocol_forwarding": 0, "npmplus_proxy_tls": false, "npmplus_advanced_config": "", - "meta": { - "nginx_online": true, - "nginx_err": null - }, + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, "enabled": true, "certificate_id": 0 } diff --git a/backend/schema/paths/nginx/streams/post.json b/backend/schema/paths/nginx/streams/post.json index cda10ca049..52bbe6b0a3 100644 --- a/backend/schema/paths/nginx/streams/post.json +++ b/backend/schema/paths/nginx/streams/post.json @@ -50,8 +50,14 @@ "npmplus_advanced_config": { "$ref": "../../../components/stream-object.json#/properties/npmplus_advanced_config" }, - "meta": { - "$ref": "../../../components/stream-object.json#/properties/meta" + "npmplus_directory": { + "$ref": "../../../components/stream-object.json#/properties/npmplus_directory" + }, + "npmplus_mtls_certificate_id": { + "$ref": "../../../components/stream-object.json#/properties/npmplus_mtls_certificate_id" + }, + "npmplus_mtls_verify_client_optional": { + "$ref": "../../../components/stream-object.json#/properties/npmplus_mtls_verify_client_optional" }, "npmplus_description": { "$ref": "../../../components/stream-object.json#/properties/npmplus_description" @@ -64,8 +70,7 @@ "forwarding_port": 8080, "tcp_forwarding": true, "udp_forwarding": false, - "certificate_id": 0, - "meta": {} + "certificate_id": 0 } } } @@ -90,23 +95,12 @@ "npmplus_proxy_protocol_forwarding": 0, "npmplus_proxy_tls": false, "npmplus_advanced_config": "", - "meta": { - "nginx_online": true, - "nginx_err": null - }, + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, "enabled": true, - "owner": { - "id": 1, - "created_on": "2024-10-09T02:33:16.000Z", - "modified_on": "2024-10-09T02:33:16.000Z", - "is_disabled": false, - "email": "admin@example.com", - "name": "Administrator", - "avatar": "", - "roles": [ - "admin" - ] - }, "certificate_id": 0 } } diff --git a/backend/schema/paths/nginx/streams/streamID/delete.json b/backend/schema/paths/nginx/streams/streamID/delete.json index 3b6cdf7ee6..fb77a4d937 100644 --- a/backend/schema/paths/nginx/streams/streamID/delete.json +++ b/backend/schema/paths/nginx/streams/streamID/delete.json @@ -29,11 +29,31 @@ "application/json": { "examples": { "default": { - "value": true + "value": { + "id": 1, + "created_on": "2024-10-09T02:33:45.000Z", + "modified_on": "2024-10-09T02:33:45.000Z", + "owner_user_id": 1, + "incoming_port": 9090, + "forwarding_host": "router.internal", + "forwarding_port": 80, + "tcp_forwarding": true, + "udp_forwarding": false, + "npmplus_proxy_protocol_forwarding": 0, + "npmplus_proxy_tls": false, + "npmplus_advanced_config": "", + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, + "enabled": true, + "certificate_id": 0 + } } }, "schema": { - "type": "boolean" + "$ref": "../../../../components/stream-object.json" } } } diff --git a/backend/schema/paths/nginx/streams/streamID/disable/post.json b/backend/schema/paths/nginx/streams/streamID/disable/post.json index cffa00c826..271c0231e5 100644 --- a/backend/schema/paths/nginx/streams/streamID/disable/post.json +++ b/backend/schema/paths/nginx/streams/streamID/disable/post.json @@ -29,11 +29,31 @@ "application/json": { "examples": { "default": { - "value": true + "value": { + "id": 1, + "created_on": "2024-10-09T02:33:45.000Z", + "modified_on": "2024-10-09T02:33:45.000Z", + "owner_user_id": 1, + "incoming_port": 9090, + "forwarding_host": "router.internal", + "forwarding_port": 80, + "tcp_forwarding": true, + "udp_forwarding": false, + "npmplus_proxy_protocol_forwarding": 0, + "npmplus_proxy_tls": false, + "npmplus_advanced_config": "", + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, + "enabled": false, + "certificate_id": 0 + } } }, "schema": { - "type": "boolean" + "$ref": "../../../../../components/stream-object.json" } } } diff --git a/backend/schema/paths/nginx/streams/streamID/enable/post.json b/backend/schema/paths/nginx/streams/streamID/enable/post.json index f1e0d20820..a8281fbac3 100644 --- a/backend/schema/paths/nginx/streams/streamID/enable/post.json +++ b/backend/schema/paths/nginx/streams/streamID/enable/post.json @@ -29,11 +29,31 @@ "application/json": { "examples": { "default": { - "value": true + "value": { + "id": 1, + "created_on": "2024-10-09T02:33:45.000Z", + "modified_on": "2024-10-09T02:33:45.000Z", + "owner_user_id": 1, + "incoming_port": 9090, + "forwarding_host": "router.internal", + "forwarding_port": 80, + "tcp_forwarding": true, + "udp_forwarding": false, + "npmplus_proxy_protocol_forwarding": 0, + "npmplus_proxy_tls": false, + "npmplus_advanced_config": "", + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, + "enabled": true, + "certificate_id": 0 + } } }, "schema": { - "type": "boolean" + "$ref": "../../../../../components/stream-object.json" } } } diff --git a/backend/schema/paths/nginx/streams/streamID/get.json b/backend/schema/paths/nginx/streams/streamID/get.json index b0837f7c43..84f1b475da 100644 --- a/backend/schema/paths/nginx/streams/streamID/get.json +++ b/backend/schema/paths/nginx/streams/streamID/get.json @@ -42,10 +42,11 @@ "npmplus_proxy_protocol_forwarding": 0, "npmplus_proxy_tls": false, "npmplus_advanced_config": "", - "meta": { - "nginx_online": true, - "nginx_err": null - }, + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, "enabled": true, "certificate_id": 0 } diff --git a/backend/schema/paths/nginx/streams/streamID/put.json b/backend/schema/paths/nginx/streams/streamID/put.json index c33a0ea223..d8e493c92b 100644 --- a/backend/schema/paths/nginx/streams/streamID/put.json +++ b/backend/schema/paths/nginx/streams/streamID/put.json @@ -59,8 +59,14 @@ "npmplus_advanced_config": { "$ref": "../../../../components/stream-object.json#/properties/npmplus_advanced_config" }, - "meta": { - "$ref": "../../../../components/stream-object.json#/properties/meta" + "npmplus_directory": { + "$ref": "../../../../components/stream-object.json#/properties/npmplus_directory" + }, + "npmplus_mtls_certificate_id": { + "$ref": "../../../../components/stream-object.json#/properties/npmplus_mtls_certificate_id" + }, + "npmplus_mtls_verify_client_optional": { + "$ref": "../../../../components/stream-object.json#/properties/npmplus_mtls_verify_client_optional" }, "npmplus_description": { "$ref": "../../../../components/stream-object.json#/properties/npmplus_description" @@ -90,23 +96,12 @@ "npmplus_proxy_protocol_forwarding": 0, "npmplus_proxy_tls": false, "npmplus_advanced_config": "", - "meta": { - "nginx_online": true, - "nginx_err": null - }, + "npmplus_nginx_online": true, + "npmplus_nginx_err": "", + "npmplus_directory": "", + "npmplus_mtls_certificate_id": 0, + "npmplus_mtls_verify_client_optional": false, "enabled": true, - "owner": { - "id": 1, - "created_on": "2024-10-09T02:33:16.000Z", - "modified_on": "2024-10-09T02:33:16.000Z", - "is_disabled": false, - "email": "admin@example.com", - "name": "Administrator", - "avatar": "", - "roles": [ - "admin" - ] - }, "certificate_id": 0 } } diff --git a/backend/schema/paths/reports/hosts/get.json b/backend/schema/paths/reports/hosts/get.json index b229444dc8..39f8a80d37 100644 --- a/backend/schema/paths/reports/hosts/get.json +++ b/backend/schema/paths/reports/hosts/get.json @@ -20,7 +20,9 @@ "proxy": 20, "redirection": 1, "stream": 0, - "dead": 1 + "dead": 1, + "access_list": 2, + "certificate": 15 } } }, @@ -46,6 +48,16 @@ "type": "integer", "description": "404 Hosts Count", "example": 3 + }, + "access_list": { + "type": "integer", + "description": "Access Lists Count", + "example": 2 + }, + "certificate": { + "type": "integer", + "description": "Certificates Count", + "example": 15 } } } diff --git a/backend/schema/paths/settings/settingID/put.json b/backend/schema/paths/settings/settingID/put.json index 1aabfc9508..ffec663de3 100644 --- a/backend/schema/paths/settings/settingID/put.json +++ b/backend/schema/paths/settings/settingID/put.json @@ -53,7 +53,8 @@ "additionalProperties": false, "properties": { "redirect": { - "type": "string" + "type": "string", + "pattern": "^\\S*$" }, "html": { "type": "string" diff --git a/backend/schema/paths/users/get.json b/backend/schema/paths/users/get.json index 349b295472..e622627e21 100644 --- a/backend/schema/paths/users/get.json +++ b/backend/schema/paths/users/get.json @@ -9,19 +9,6 @@ "cookieAuth": [] } ], - "parameters": [ - { - "in": "query", - "name": "expand", - "description": "Expansions", - "schema": { - "type": "string", - "enum": [ - "permissions" - ] - } - } - ], "responses": { "200": { "description": "200 response", @@ -43,31 +30,6 @@ ] } ] - }, - "withPermissions": { - "value": [ - { - "id": 1, - "created_on": "2020-01-30T09:36:08.000Z", - "modified_on": "2020-01-30T09:41:04.000Z", - "is_disabled": false, - "email": "jc@jc21.com", - "name": "Jamie Curnow", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ], - "permissions": { - "visibility": "all", - "proxy_hosts": "manage", - "redirection_hosts": "manage", - "dead_hosts": "manage", - "streams": "manage", - "access_lists": "manage", - "certificates": "manage" - } - } - ] } }, "schema": { diff --git a/backend/schema/paths/users/post.json b/backend/schema/paths/users/post.json index df8600f6ef..e123f97ccb 100644 --- a/backend/schema/paths/users/post.json +++ b/backend/schema/paths/users/post.json @@ -76,20 +76,7 @@ "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", "roles": [ "admin" - ], - "permissions": { - "id": 3, - "created_on": "2020-01-30T09:41:04.000Z", - "modified_on": "2020-01-30T09:41:04.000Z", - "user_id": 2, - "visibility": "user", - "proxy_hosts": "manage", - "redirection_hosts": "manage", - "dead_hosts": "manage", - "streams": "manage", - "access_lists": "manage", - "certificates": "manage" - } + ] } } }, diff --git a/backend/schema/paths/users/userID/delete.json b/backend/schema/paths/users/userID/delete.json index 8d4d58127e..dfbeb26548 100644 --- a/backend/schema/paths/users/userID/delete.json +++ b/backend/schema/paths/users/userID/delete.json @@ -29,11 +29,22 @@ "application/json": { "examples": { "default": { - "value": true + "value": { + "id": 1, + "created_on": "2020-01-30T09:36:08.000Z", + "modified_on": "2020-01-30T09:41:04.000Z", + "is_disabled": false, + "email": "jc@jc21.com", + "name": "Jamie Curnow", + "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", + "roles": [ + "admin" + ] + } } }, "schema": { - "type": "boolean" + "$ref": "../../../components/user-object.json" } } } diff --git a/backend/schema/paths/users/userID/mfa/totp/delete.json b/backend/schema/paths/users/userID/mfa/totp/disable/post.json similarity index 57% rename from backend/schema/paths/users/userID/mfa/totp/delete.json rename to backend/schema/paths/users/userID/mfa/totp/disable/post.json index 50fa962368..ea0949179d 100644 --- a/backend/schema/paths/users/userID/mfa/totp/delete.json +++ b/backend/schema/paths/users/userID/mfa/totp/disable/post.json @@ -18,21 +18,32 @@ "required": true, "description": "User ID", "example": 2 - }, - { - "in": "query", - "name": "code", - "schema": { - "type": "string", - "minLength": 6, - "maxLength": 8, - "example": "012345" - }, - "required": true, - "description": "TOTP code or backup code", - "example": "012345" } ], + "requestBody": { + "description": "Verification Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "code": { + "minLength": 6, + "maxLength": 8, + "type": "string", + "example": "123456" + } + }, + "required": ["code"], + "type": "object" + }, + "example": { + "code": "123456" + } + } + } + }, "responses": { "200": { "content": { diff --git a/backend/schema/swagger.json b/backend/schema/swagger.json index 36b6932d47..13aa79db0b 100644 --- a/backend/schema/swagger.json +++ b/backend/schema/swagger.json @@ -386,9 +386,6 @@ "/users/{userID}/mfa/totp": { "post": { "$ref": "./paths/users/userID/mfa/totp/post.json" - }, - "delete": { - "$ref": "./paths/users/userID/mfa/totp/delete.json" } }, "/users/{userID}/mfa/totp/enable": { @@ -396,6 +393,11 @@ "$ref": "./paths/users/userID/mfa/totp/enable/post.json" } }, + "/users/{userID}/mfa/totp/disable": { + "post": { + "$ref": "./paths/users/userID/mfa/totp/disable/post.json" + } + }, "/users/{userID}/mfa/backup-codes": { "post": { "$ref": "./paths/users/userID/mfa/backup-codes/post.json" diff --git a/backend/setup.js b/backend/setup.js index ce3278cd41..69b81557a8 100644 --- a/backend/setup.js +++ b/backend/setup.js @@ -152,14 +152,14 @@ const setupCertbotPlugins = async () => { const plugins = []; for (const certificate of certificates) { - if (certificate.meta && certificate.meta.dns_challenge === true) { - if (plugins.indexOf(certificate.meta.dns_provider) === -1) { - plugins.push(certificate.meta.dns_provider); + if (certificate.npmplus_dns_challenge) { + if (plugins.indexOf(certificate.npmplus_dns_provider) === -1) { + plugins.push(certificate.npmplus_dns_provider); } await writeFile( `/tmp/certbot-credentials/credentials-${certificate.id}`, - certificate.meta.dns_provider_credentials, + certificate.npmplus_dns_provider_credentials, { mode: 0o600 }, ); } @@ -190,7 +190,7 @@ const regenerateAllHosts = async () => { .query() .where("is_deleted", 0) .andWhere("enabled", 1) - .withGraphFetched(proxyModel.defaultAllowGraph); + .withGraphFetched("[access_lists.[clients,items],certificate]"); if (proxyHosts?.length > 0) { // locations dont contain access list objects, so prepopulate them before generating the nginx files @@ -208,7 +208,7 @@ const regenerateAllHosts = async () => { .query() .where("is_deleted", 0) .andWhere("enabled", 1) - .withGraphFetched(redirectionModel.defaultAllowGraph); + .withGraphFetched("certificate"); if (redirectionHosts?.length > 0) { await internalNginx.bulkGenerateConfigs(redirectionModel, "redirection_host", redirectionHosts, { @@ -220,7 +220,7 @@ const regenerateAllHosts = async () => { .query() .where("is_deleted", 0) .andWhere("enabled", 1) - .withGraphFetched(deadModel.defaultAllowGraph); + .withGraphFetched("certificate"); if (deadHosts?.length > 0) { await internalNginx.bulkGenerateConfigs(deadModel, "dead_host", deadHosts, { skipReload: true }); @@ -230,7 +230,7 @@ const regenerateAllHosts = async () => { .query() .where("is_deleted", 0) .andWhere("enabled", 1) - .withGraphFetched(streamModel.defaultAllowGraph); + .withGraphFetched("certificate"); if (streamHosts?.length > 0) { await internalNginx.bulkGenerateConfigs(streamModel, "stream", streamHosts, { skipReload: true }); @@ -260,6 +260,9 @@ const setupAio = async () => { forward_scheme: "http", forward_host: "127.0.0.1", forward_port: 11000, + npmplus_crowdsec_appsec: false, + npmplus_proxy_request_buffering: true, + npmplus_proxy_response_buffering: true, certificate_id: "new", ssl_forced: true, hsts_enabled: true, diff --git a/backend/templates/_common.conf b/backend/templates/_common.conf index 9abdb77887..b347360573 100644 --- a/backend/templates/_common.conf +++ b/backend/templates/_common.conf @@ -7,14 +7,14 @@ listen unix:/run/nginx.sock; {% if env.DISABLE_IPV6 == "false" %}listen {{ env.IPV6_BINDING }}:{{ env.HTTP_PORT }}{% if env.LISTEN_PROXY_PROTOCOL_HTTP == "true" %} proxy_protocol{% endif %};{% endif %} {% endif %} -{% if certificate and certificate_id > 0 %} +{% if certificate %} listen {{ env.IPV4_BINDING }}:{{ env.HTTPS_PORT }} ssl{% if env.LISTEN_PROXY_PROTOCOL_HTTPS == "true" %} proxy_protocol{% endif %}; {% if env.DISABLE_IPV6 == "false" %}listen {{ env.IPV6_BINDING }}:{{ env.HTTPS_PORT }} ssl{% if env.LISTEN_PROXY_PROTOCOL_HTTPS == "true" %} proxy_protocol{% endif %};{% endif %} {% if env.DISABLE_H3_QUIC == "false" %} listen {{ env.IPV4_BINDING }}:{{ env.HTTPS_PORT }} quic; {% if env.DISABLE_IPV6 == "false" %}listen {{ env.IPV6_BINDING }}:{{ env.HTTPS_PORT }} quic;{% endif %} - {% if npmplus_http3_support %}more_set_headers 'Alt-Svc: h3=":$alt_svc_port"; ma=86400';{% endif %} + {% if npmplus_http3_support == true %}more_set_headers 'Alt-Svc: h3=":$alt_svc_port"; ma=86400';{% endif %} {% endif %} {% if certificate.provider == "letsencrypt" %} @@ -39,20 +39,20 @@ listen unix:/run/nginx.sock; {% endif %} {% endif %} - {% if meta and meta.npmplus_mtls_certificate_id and meta.npmplus_mtls_certificate_id > 0 %} - ssl_verify_client {% if meta.npmplus_mtls_verify_client_optional == true %}optional{% else %}on{% endif %}; - ssl_client_certificate /data/tls/mtls/npm-{{ meta.npmplus_mtls_certificate_id }}.pem; + {% if npmplus_mtls_certificate_id > 0 %} + ssl_verify_client {% if npmplus_mtls_verify_client_optional == true %}optional{% else %}on{% endif %}; + ssl_client_certificate /data/tls/mtls/npm-{{ npmplus_mtls_certificate_id }}.pem; {% endif %} - {% if ssl_forced %} + {% if ssl_forced == true %} if ($scheme = "http") { return 301 https://$host$is_request_port$request_port$request_uri; } if ($http_x_forwarded_proto = "http") { return 301 https://$host$is_request_port$request_port$request_uri; } - {% if hsts_enabled %} - {% if hsts_subdomains %} + {% if hsts_enabled == true %} + {% if hsts_subdomains == true %} more_set_headers "Strict-Transport-Security: $hsts_includeSubDomains_header"; {% else %} more_set_headers "Strict-Transport-Security: $hsts_header"; diff --git a/backend/templates/_proxy_host_custom_location.conf b/backend/templates/_proxy_host_custom_location.conf index f0494d1756..0df8725021 100644 --- a/backend/templates/_proxy_host_custom_location.conf +++ b/backend/templates/_proxy_host_custom_location.conf @@ -1,24 +1,22 @@ {% assign forward_host_last_char = forward_host | slice: -1 -%} -{% assign has_location_auth_items = access_list and access_list.items and access_list.items.size > 0 %} -{% assign has_location_access_rules = access_list and access_list.clients and access_list.clients.size > 0 %} location {{ location_type }}{{ path }} { - {% if has_location_auth_items and filename %} + {% if access_list.items.size > 0 %} # Authorization auth_basic "basic access authentication required"; auth_basic_user_file {{ filename }}; - {% if not access_list.pass_auth %} + {% if access_list.pass_auth != true %} proxy_set_header Authorization ""; {% endif %} {% endif %} - {% if has_location_access_rules %} + {% if access_list.clients.size > 0 %} # Access Rules: {{ access_list.clients | size }} total {% for client in access_list.clients %} {{ client | nginxAccessRule }} {% endfor %} {% endif %} - {% if access_list.satisfy_any %} + {% if access_list.satisfy_any == true %} satisfy any; {% endif %} @@ -40,7 +38,7 @@ location {{ location_type }}{{ path }} { {% if npmplus_upstream_compression != true %}proxy_set_header Accept-Encoding "";{% endif %} {% if npmplus_proxy_request_buffering == true and npmplus_crowdsec_appsec == true %}proxy_request_buffering off;{% endif %} {% if npmplus_proxy_response_buffering == true %}proxy_buffering off;{% endif %} - proxy_pass {{ forward_scheme }}://{{ forward_upstream_name }}{% if forward_path != null %}{{ forward_path }}{% endif %}; + proxy_pass {{ forward_scheme }}://{{ forward_upstream_name }}{{ forward_path }}; {% elsif forward_scheme == "grpc" or forward_scheme == "grpcs" %} diff --git a/backend/templates/dead_host.conf b/backend/templates/dead_host.conf index ff3b761bbe..6e113a5fb9 100644 --- a/backend/templates/dead_host.conf +++ b/backend/templates/dead_host.conf @@ -3,7 +3,6 @@ # DO NOT EDIT THIS FILE DIRECTLY, CHANGES WILL BE LOST WHEN UPDATING! # ---------------------------------------------------------------------- -{% if enabled %} server { {% include "_common.conf" %} @@ -22,4 +21,3 @@ include /data/custom_nginx/server_http.conf; include /data/custom_nginx/server_dead.conf; } -{% endif %} diff --git a/backend/templates/proxy_host.conf b/backend/templates/proxy_host.conf index c9f8c5feed..cbbc5c89fc 100644 --- a/backend/templates/proxy_host.conf +++ b/backend/templates/proxy_host.conf @@ -3,10 +3,7 @@ # DO NOT EDIT THIS FILE DIRECTLY, CHANGES WILL BE LOST WHEN UPDATING! # ---------------------------------------------------------------------- {% assign forward_host_last_char = forward_host | slice: -1 -%} -{% assign has_host_auth_items = access_list and access_list.items and access_list.items.size > 0 %} -{% assign has_host_access_rules = access_list and access_list.clients and access_list.clients.size > 0 %} -{% if enabled %} {{ upstreams }} {% assign auth_request_anubis_upstream_resolved = auth_request_anubis_upstream_resolved | default: env.AUTH_REQUEST_ANUBIS_UPSTREAM %} @@ -153,21 +150,21 @@ location / { - {% if has_host_auth_items and filename %} + {% if access_list.items.size > 0 %} # Authorization auth_basic "basic access authentication required"; auth_basic_user_file {{ filename }}; - {% if not access_list.pass_auth %} + {% if access_list.pass_auth != true %} proxy_set_header Authorization ""; {% endif %} {% endif %} - {% if has_host_access_rules %} + {% if access_list.clients.size > 0 %} # Access Rules: {{ access_list.clients | size }} total {% for client in access_list.clients %} {{ client | nginxAccessRule }} {% endfor %} {% endif %} - {% if access_list.satisfy_any %} + {% if access_list.satisfy_any == true %} satisfy any; {% endif %} @@ -189,7 +186,7 @@ {% if npmplus_upstream_compression != true %}proxy_set_header Accept-Encoding "";{% endif %} {% if npmplus_proxy_request_buffering == true and npmplus_crowdsec_appsec == true %}proxy_request_buffering off;{% endif %} {% if npmplus_proxy_response_buffering == true %}proxy_buffering off;{% endif %} - proxy_pass {{ forward_scheme }}://{{ forward_upstream_name }}{% if forward_path != null %}{{ forward_path }}{% endif %}; + proxy_pass {{ forward_scheme }}://{{ forward_upstream_name }}{{ forward_path }}; {% elsif forward_scheme == "grpc" or forward_scheme == "grpcs" %} @@ -451,4 +448,3 @@ include /data/custom_nginx/server_http.conf; include /data/custom_nginx/server_proxy.conf; } -{% endif %} diff --git a/backend/templates/redirection_host.conf b/backend/templates/redirection_host.conf index 617ab2e639..82e966ee16 100644 --- a/backend/templates/redirection_host.conf +++ b/backend/templates/redirection_host.conf @@ -3,11 +3,10 @@ # DO NOT EDIT THIS FILE DIRECTLY, CHANGES WILL BE LOST WHEN UPDATING! # ---------------------------------------------------------------------- -{% if enabled %} server { {% include "_common.conf" %} - location / { return {{ forward_http_code }} {{ forward_scheme }}://{{ forward_domain_name }}{% if preserve_path %}$request_uri{% endif %}; } + location / { return {{ forward_http_code }} {{ forward_scheme }}://{{ forward_domain_name }}{% if preserve_path == true %}$request_uri{% endif %}; } {{ advanced_config }} @@ -15,4 +14,3 @@ include /data/custom_nginx/server_http.conf; include /data/custom_nginx/server_redirect.conf; } -{% endif %} diff --git a/backend/templates/stream.conf b/backend/templates/stream.conf index 9b21d9ef6c..4836a5dfc2 100644 --- a/backend/templates/stream.conf +++ b/backend/templates/stream.conf @@ -3,7 +3,6 @@ # DO NOT EDIT THIS FILE DIRECTLY, CHANGES WILL BE LOST WHEN UPDATING! # -------------------------------------------------------------------------- -{% if enabled %} {% assign forwarding_host_prefix = forwarding_host | slice: 0, 3 %} {% if forwarding_port != "$server_port" and forwarding_host_prefix != "cu_" %} @@ -13,16 +12,16 @@ } {% endif %} - {% if tcp_forwarding %} + {% if tcp_forwarding == true %} server { - listen {{ env.IPV4_BINDING }}:{{ incoming_port }}{% if certificate and certificate_id > 0 %} ssl{% endif %} reuseport deferred{% if env.ENABLE_MPTCP == "true" %} multipath{% endif %} so_keepalive=on; - {% if env.DISABLE_IPV6 == "false" %}listen {{ env.IPV6_BINDING }}:{{ incoming_port }}{% if certificate and certificate_id > 0 %} ssl{% endif %} reuseport deferred{% if env.ENABLE_MPTCP == "true" %} multipath{% endif %} so_keepalive=on;{% endif %} + listen {{ env.IPV4_BINDING }}:{{ incoming_port }}{% if certificate %} ssl{% endif %} reuseport deferred{% if env.ENABLE_MPTCP == "true" %} multipath{% endif %} so_keepalive=on; + {% if env.DISABLE_IPV6 == "false" %}listen {{ env.IPV6_BINDING }}:{{ incoming_port }}{% if certificate %} ssl{% endif %} reuseport deferred{% if env.ENABLE_MPTCP == "true" %} multipath{% endif %} so_keepalive=on;{% endif %} {% if npmplus_proxy_protocol_forwarding == 1 %}proxy_protocol on;{% elsif npmplus_proxy_protocol_forwarding == 2 %}proxy_protocol v2;{% endif %} - {% if npmplus_proxy_tls %}proxy_ssl on;{% endif %} + {% if npmplus_proxy_tls == true %}proxy_ssl on;{% endif %} - {% if certificate and certificate_id > 0 %} + {% if certificate %} {% if certificate.provider == "letsencrypt" %} # Certbot TLS ssl_certificate /data/tls/certbot/live/npm-{{ certificate_id }}/fullchain.pem; @@ -45,9 +44,9 @@ {% endif %} {% endif %} - {% if meta and meta.npmplus_mtls_certificate_id and meta.npmplus_mtls_certificate_id > 0 %} - ssl_verify_client {% if meta.npmplus_mtls_verify_client_optional == true %}optional{% else %}on{% endif %}; - ssl_client_certificate /data/tls/mtls/npm-{{ meta.npmplus_mtls_certificate_id }}.pem; + {% if npmplus_mtls_certificate_id > 0 %} + ssl_verify_client {% if npmplus_mtls_verify_client_optional == true %}optional{% else %}on{% endif %}; + ssl_client_certificate /data/tls/mtls/npm-{{ npmplus_mtls_certificate_id }}.pem; {% endif %} {% endif %} @@ -67,7 +66,7 @@ } {% endif %} - {% if udp_forwarding %} + {% if udp_forwarding == true %} server { listen {{ env.IPV4_BINDING }}:{{ incoming_port }} udp reuseport; {% if env.DISABLE_IPV6 == "false" %}listen {{ env.IPV6_BINDING }}:{{ incoming_port }} udp reuseport;{% endif %} @@ -89,4 +88,3 @@ include /data/custom_nginx/server_stream_udp.conf; } {% endif %} -{% endif %} diff --git a/backend/test/certificate-dns.test.js b/backend/test/certificate-dns.test.js index 794ea6a960..7ee3af6c37 100644 --- a/backend/test/certificate-dns.test.js +++ b/backend/test/certificate-dns.test.js @@ -15,14 +15,14 @@ import utils from "../lib/utils.js"; const CREDENTIALS_DIR = "/tmp/certbot-credentials"; -const fakeCertificate = ({ id = 101, domain_names, meta: metaOverrides = {} } = {}) => ({ +const fakeCertificate = ({ id = 101, domain_names, ...overrides } = {}) => ({ id, domain_names: domain_names ?? ["example.com", "bücher.example"], - meta: { - dns_provider: "cloudflare", - dns_provider_credentials: "dns_cloudflare_api_token=SECRET-TOKEN", - ...metaOverrides, - }, + // the dns challenge fields live in npmplus_* columns since meta_to_columns + npmplus_dns_provider: "cloudflare", + npmplus_dns_provider_credentials: "dns_cloudflare_api_token=SECRET-TOKEN", + npmplus_propagation_seconds: 0, + ...overrides, }); // certbot reads its config from the environment at request time @@ -116,12 +116,12 @@ test("dns challenge request passes propagation seconds when configured", async ( return { stdout: "ok" }; }); - await internalCertificate.requestCertbotWithDnsChallenge(fakeCertificate({ meta: { propagation_seconds: "42" } })); + await internalCertificate.requestCertbotWithDnsChallenge(fakeCertificate({ npmplus_propagation_seconds: 42 })); const certbotArgs = calls.find(([cmd]) => cmd === "certbot")[1]; const flagIndex = certbotArgs.indexOf("--dns-cloudflare-propagation-seconds"); assert.ok(flagIndex > -1, "propagation flag missing"); - assert.equal(certbotArgs[flagIndex + 1], "42"); + assert.equal(certbotArgs[flagIndex + 1], 42); }); test("dns challenge request cleans up the credentials file when certbot fails", async (t) => { @@ -166,14 +166,12 @@ test("an unknown dns provider fails fast without touching pip or certbot", async await assert.rejects( internalCertificate.requestCertbotWithDnsChallenge( - fakeCertificate({ meta: { dns_provider: "no-such-provider" } }), + fakeCertificate({ npmplus_dns_provider: "no-such-provider" }), ), /Unknown DNS provider/, ); await assert.rejects( - internalCertificate.renewCertbotWithDnsChallenge( - fakeCertificate({ meta: { dns_provider: "no-such-provider" } }), - ), + internalCertificate.renewCertbotWithDnsChallenge(fakeCertificate({ npmplus_dns_provider: "no-such-provider" })), /Unknown DNS provider/, ); assert.deepEqual(calls, [], "no external command may run for an unknown provider"); diff --git a/backend/test/sqlite-upgrade.test.js b/backend/test/sqlite-upgrade.test.js index cab02451cc..b673880348 100644 --- a/backend/test/sqlite-upgrade.test.js +++ b/backend/test/sqlite-upgrade.test.js @@ -51,22 +51,38 @@ test("an existing SQLite MFA account and proxy survive the factor and replay mig secret: "Upgrade-Fixture-1", meta: { totp_enabled: true, totp_secret: secret }, }); - const proxy = await ProxyHost.query().insertAndFetch({ - owner_user_id: user.id, - domain_names: ["upgrade.example.test"], - forward_scheme: "http", - forward_host: "127.0.0.1", - forward_port: 8080, - }); + // Seed the legacy row with exactly the columns the pre-replay model wrote; + // the current model defaults npmplus_* columns that this schema predates. + const nowRaw = db().raw("datetime('now','localtime')"); + const proxyId = ( + await db()("proxy_host").insert({ + owner_user_id: user.id, + domain_names: JSON.stringify(["upgrade.example.test"]), + forward_scheme: "http", + forward_host: "127.0.0.1", + forward_port: 8080, + meta: JSON.stringify({}), + npmplus_access_list_type: "public", + npmplus_access_list_ids: JSON.stringify([]), + created_on: nowRaw, + modified_on: nowRaw, + }) + )[0]; // Switch from the historical migration source to the actual startup path. await migrateUp(); + const assertProxySurvived = async () => { + const survived = await ProxyHost.query().findById(proxyId); + assert.deepEqual(survived.domain_names, ["upgrade.example.test"]); + assert.equal(survived.forward_host, "127.0.0.1"); + assert.equal(survived.forward_port, 8080); + }; const upgraded = await Auth.query().findById(auth.id); // the second factor moved to its own row and the password row's meta is cleared assert.equal(upgraded.meta?.totp_secret, undefined); assert.equal(upgraded.secret, auth.secret); assert.equal(upgraded.npmplus_totp_last_used_step, null); assert.equal((await Auth.getTotpEnrollment(user.id)).secret, secret); - assert.deepEqual(await ProxyHost.query().findById(proxy.id), proxy); + await assertProxySurvived(); const challenge = await internalToken.getTokenFromEmail({ identity: user.email, secret: "Upgrade-Fixture-1" }); const code = await generate({ secret }); assert.ok((await internalToken.verifyTotp(challenge.token, code)).token); @@ -75,5 +91,5 @@ test("an existing SQLite MFA account and proxy survive the factor and replay mig assert.equal(await totp.verifyCode(user.id, code), false); epoch += 30; assert.equal(await totp.verifyCode(user.id, await generate({ secret })), true); - assert.deepEqual(await ProxyHost.query().findById(proxy.id), proxy); + await assertProxySurvived(); }); diff --git a/docs/ech.md b/docs/ech.md new file mode 100644 index 0000000000..bea9870b0e --- /dev/null +++ b/docs/ech.md @@ -0,0 +1,16 @@ +# Encrypted Client Hello (ECH) + +NPMplus supports generating and automatically rotating Encrypted Client Hello (ECH) keys. This guide covers the container's ECH hooks and how to drive them from your own cron script. + +- To enable and configure ECH, you need to set up a cron script that triggers the key generation and updates your DNS records. +- When the container starts, it automatically creates an empty file at `/opt/npmplus/tls/ech/cron.sh`. You need to fill this file with a script to handle your ECH keys. +- If this file is not empty, NPMplus will automatically execute it regularly, enable ECH in the nginx configuration, and reload nginx after execution. +- Inside your `cron.sh`, use the built-in `ech.sh` command to generate your keys. The syntax is: `ech.sh [max-name-length (default 64)]`. +- This command generates the keys in `/opt/npmplus/tls/ech/` (saving the current and previous keys) and outputs the Base64-encoded ECH config list to standard output, which you can capture to update your DNS records. +- Because ECH requires advertising your public key via an HTTPS DNS record, your `cron.sh` must push the newly generated config to your DNS provider. +- There is an example cron.sh script for Cloudflare in the repository root: [`ech-cron-cloudflare-example.sh`](../ech-cron-cloudflare-example.sh). You can adapt this script, add your API tokens, define your zones/records, and place its contents into `/opt/npmplus/tls/ech/cron.sh`. +- By default, the container will run your `cron.sh` script and reload nginx on container start and then every hour after container start. You can change this interval by setting the `ECH_ROTATION_INTERVAL` environment variable in your `compose.yaml`. +- Use your server's hostname/PTR record as the public name, and make sure a (dead) host with a valid cert for your public name exists. The "identifier" is only used as part of the filename. +- Use only one ECH key shared across all your hosts. If you configure multiple ECH keys then only the one with the alphabetically first "identifier" will be used in the retry_configs response. +- Do not set HTTPS records for FQDNs which use a CNAME record, but set them for the CNAME target, as only the HTTPS record of the CNAME target will be used by chromium. +- Deleting/clearing the cron.sh will disable ECH, but you still need to remove ECH from the HTTPS records yourself. \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index c19769696a..7f6ae64832 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -35,7 +35,7 @@ -
+
diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index 06d54c7ebf..0000000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,111 +0,0 @@ -:root { - color-scheme: light dark; -} - -.light { - color-scheme: light; -} -.dark { - color-scheme: dark; -} - -.modal-backdrop { - --tblr-backdrop-opacity: 0.8 !important; -} - -[data-bs-theme="dark"] .modal-content { - --tblr-modal-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; -} - -[data-bs-theme="dark"] .modal-backdrop { - --tblr-backdrop-bg: #000 !important; - --tblr-backdrop-opacity: 0.65 !important; -} - -.domain-name { - font-family: monospace; -} - -.mr-1 { - margin-right: 0.25rem; -} -.ml-1 { - margin-left: 0.25rem; -} - -::selection { - background-color: rgba(var(--tblr-primary-rgb), 0.4); - color: #ffffff; -} - -.react-select-container { - .react-select__control { - color: var(--tblr-body-color); - background-color: var(--tblr-bg-forms); - border: var(--tblr-border-width) solid var(--tblr-border-color); - - .react-select__input { - color: var(--tblr-body-color) !important; - } - - .react-select__single-value { - color: var(--tblr-body-color); - } - - .react-select__multi-value { - border: 1px solid var(--tblr-border-color); - background-color: var(--tblr-bg-surface-tertiary); - color: var(--tblr-secondary) !important; - - .react-select__multi-value__label { - color: var(--tblr-secondary) !important; - } - } - } - - .react-select__menu { - background-color: var(--tblr-bg-forms); - - .react-select__option { - background: rgba(var(--tblr-primary-rgb), 0.04); - color: inherit !important; - &.react-select__option--is-focused { - background: rgba(var(--tblr-primary-rgb), 0.1); - } - - &.react-select__option--is-focused.react-select__option--is-selected { - background: rgba(var(--tblr-primary-rgb), 0.2); - } - } - } -} - -label.row { - cursor: pointer; -} - -.input-group-select { - display: flex; - align-items: center; - padding: 0; - font-size: 0.875rem; - font-weight: 400; - line-height: 1.25rem; - color: var(--tblr-gray-500); - text-align: center; - white-space: nowrap; - background-color: var(--tblr-bg-surface-secondary); - border: var(--tblr-border-width) solid var(--tblr-border-color); - border-radius: var(--tblr-border-radius); - - .form-select { - border: none; - background-color: var(--tblr-bg-surface-secondary); - border-radius: var(--tblr-border-radius) 0 0 var(--tblr-border-radius); - } -} - -/* Fix for dropdown menus being clipped by table-responsive containers. */ -.table-responsive .dropdown { - position: static; -} diff --git a/frontend/src/api/backend/base.js b/frontend/src/api/backend/base.js index 6bded603bf..1e50c16e44 100644 --- a/frontend/src/api/backend/base.js +++ b/frontend/src/api/backend/base.js @@ -47,15 +47,12 @@ async function processResponse(response, reload = true) { } return camelizeKeys(payload); } -async function baseGet({ url, params }, abortSource) { +export async function get({ url, params, reload }, abortSource) { const apiUrl = buildUrl({ url, params }); const method = "GET"; const signal = getAbortSignal(abortSource); const response = await fetch(apiUrl, { method, signal }); - return response; -} -export async function get(args, abortSource) { - return processResponse(await baseGet(args, abortSource), args.reload); + return processResponse(response, reload); } export async function download({ url, params }, filename = "download.file") { const res = await fetch(buildUrl({ url, params })); diff --git a/frontend/src/api/backend/caseConvert.js b/frontend/src/api/backend/caseConvert.js index 3a99a02276..c50df1d4d1 100644 --- a/frontend/src/api/backend/caseConvert.js +++ b/frontend/src/api/backend/caseConvert.js @@ -2,17 +2,17 @@ const separatorPattern = /[-_\s]+(.)?/g; const firstCharPattern = /^./; const upperBoundaryPattern = /(?=[A-Z])/; -const camelize = (s) => - s.replace(separatorPattern, (_, c) => (c ? c.toUpperCase() : "")).replace(firstCharPattern, (c) => c.toLowerCase()); -const decamelize = (s) => s.split(upperBoundaryPattern).join("_").toLowerCase(); const mapKeys = (fn) => { const walk = (o) => { if (Array.isArray(o)) return o.map(walk); - if (o && typeof o === "object" && o.constructor === Object) + if (o && typeof o === "object" && o.constructor === Object) { return Object.fromEntries(Object.entries(o).map(([k, v]) => [fn(k), walk(v)])); + } return o; }; return walk; }; -export const camelizeKeys = mapKeys(camelize); -export const decamelizeKeys = mapKeys(decamelize); +export const camelizeKeys = mapKeys((s) => + s.replace(separatorPattern, (_, c) => (c ? c.toUpperCase() : "")).replace(firstCharPattern, (c) => c.toLowerCase()), +); +export const decamelizeKeys = mapKeys((s) => s.split(upperBoundaryPattern).join("_").toLowerCase()); diff --git a/frontend/src/api/backend/totp.js b/frontend/src/api/backend/totp.js index 261ff7ffff..ec8ff4795d 100644 --- a/frontend/src/api/backend/totp.js +++ b/frontend/src/api/backend/totp.js @@ -14,10 +14,8 @@ export async function enableTotp(userId, code) { } export async function disableTotp(userId, code) { - return await api.del({ - url: `/users/${userId}/mfa/totp`, - params: { - code, - }, + return await api.post({ + url: `/users/${userId}/mfa/totp/disable`, + data: { code }, }); } diff --git a/frontend/src/components/Button.jsx b/frontend/src/components/Button.jsx index 6cb181f3f9..9ddc730d4a 100644 --- a/frontend/src/components/Button.jsx +++ b/frontend/src/components/Button.jsx @@ -15,10 +15,6 @@ function Button({ disabled, ...buttonProps }) { - const myOnClick = () => { - if (!isLoading) onClick?.(); - }; - const cns = cn( "btn", className, @@ -38,7 +34,7 @@ function Button({ {...buttonProps} type={type || "button"} className={cns} - onClick={myOnClick} + onClick={() => !isLoading && onClick?.()} disabled={disabled || isLoading} > {children} diff --git a/frontend/src/components/EmptyData.jsx b/frontend/src/components/EmptyData.jsx index 57f8693ebd..c9ae05860b 100644 --- a/frontend/src/components/EmptyData.jsx +++ b/frontend/src/components/EmptyData.jsx @@ -30,7 +30,7 @@ function EmptyData({ {/* tables without a create action (e.g. the audit log) only get the headline */} {onNew || customAddBtn ? ( -

+

{customAddBtn ? ( diff --git a/frontend/src/components/Form/AccessClientFields.jsx b/frontend/src/components/Form/AccessClientFields.jsx index b002a1559e..6dfec0e3b8 100644 --- a/frontend/src/components/Form/AccessClientFields.jsx +++ b/frontend/src/components/Form/AccessClientFields.jsx @@ -45,32 +45,31 @@ export function AccessClientFields({ initialValues, name = "clients" }) { return ( <> -

+

{values.slice(0, -1).map((client, idx) => (
- - - +
-

+

- - - + (
@@ -140,9 +141,11 @@ export function AccessFields({ initialAccessListType, location, initialAccessLis
o.value === item) ?? null} options={options} components={{ Option }} diff --git a/frontend/src/components/Form/DNSProviderFields.jsx b/frontend/src/components/Form/DNSProviderFields.jsx index 023dc12219..02a5996233 100644 --- a/frontend/src/components/Form/DNSProviderFields.jsx +++ b/frontend/src/components/Form/DNSProviderFields.jsx @@ -4,7 +4,7 @@ import { useState } from "react"; import Select from "react-select"; import { useDnsProviders } from "src/hooks"; import { intl, T } from "src/locale"; -import styles from "./DNSProviderFields.module.css"; +import { selectClassNames } from "src/modules/Select"; export function DNSProviderFields({ showBoundaryBox = false }) { const { values, setFieldValue } = useFormikContext(); @@ -14,8 +14,8 @@ export function DNSProviderFields({ showBoundaryBox = false }) { const v = values || {}; const handleChange = (newValue, _actionMeta) => { - void setFieldValue("meta.dnsProvider", newValue?.value); - void setFieldValue("meta.dnsProviderCredentials", newValue?.credentials); + void setFieldValue("npmplusDnsProvider", newValue?.value); + void setFieldValue("npmplusDnsProviderCredentials", newValue?.credentials); setDnsProviderId(newValue?.value); }; @@ -27,21 +27,21 @@ export function DNSProviderFields({ showBoundaryBox = false }) { })) || []; return ( -
+

- + {({ field }) => (