Compare commits

...
385 Commits
Author SHA1 Message Date
Jamie Curnow 2cfd3395cf Adds integration tests for per path access lists, fixes ipv6 jsv,
and enforces host-wide access list when location access list is not set
2026-09-24 12:21:49 +10:00
jc21 c41ec008bb Merge pull request #5772 from drakhaw/develop
feat: per-path access lists, host logs modal
2026-09-24 08:38:14 +10:00
Dimas R. Wisnu 900bb1e480 fix: resolve merge conflicts breaking frontend CI
- LocationsFields: remove duplicated conflict block, integrate AccessField
  into the Row-based layout (fixes JSX parse error)
- ProxyHosts/Table: migrate to updated @tanstack/react-table API (useTable,
  createColumnHelper<Features, ProxyHost>)
- Add missing Estonian translations (action.logs, column.error)
- Re-sort en.json locale keys
2026-09-23 19:18:51 +07:00
Dimas Rakha Wisnu 633b653f79 Merge branch 'develop' into develop 2026-09-23 18:53:06 +07:00
jc21 58566c9d75 Merge branch 'master' into develop 2026-09-23 20:57:08 +10:00
jc21 c53f52ab51 Merge pull request #5878 from carlosalbertorg/feat/log-viewer
feat: Add a Logs viewer to the admin UI
2026-09-23 20:23:26 +10:00
carlosalbertorg 9445038b36 fix: Use forwarding_host instead of the renamed forward_ip column
The stream table's forward_ip column was renamed to forwarding_host
back in migration 20210423103500_stream_domain.js, so the stream log
source labels were always showing "undefined" for the forwarding
target. Spotted by @jc21 in review.
2026-09-23 07:00:19 -03:00
jc21 b1e0473a19 Merge pull request #5741 from addielaruee/fix/stream-ipv6-forward-host-brackets
fix(stream): bracket IPv6 forward host for valid nginx upstream
2026-09-23 14:29:27 +10:00
jc21 95f1b74757 Merge pull request #5862 from NginxProxyManager/dependabot/npm_and_yarn/frontend/dev-minor-updates-793a873af9
build(deps-dev): bump the dev-minor-updates group across 1 directory with 5 updates
2026-09-23 14:23:52 +10:00
jc21 8509e1b378 Merge pull request #5813 from vzagorovskiy/fix/regenerate-config-wrong-host-type
Use the row's own model and host type in regenerate-config
2026-09-23 14:11:52 +10:00
Jamie Curnow 5f35e4916f Bump version 2026-09-23 14:10:29 +10:00
jc21 da12ee5b0c Merge pull request #5814 from shawnhank/fix/dns-credentials-lifetime
Remove DNS provider credentials from disk after certbot runs
2026-09-23 14:08:29 +10:00
jc21 61af8e23d8 Merge pull request #5829 from NginxProxyManager/dependabot/npm_and_yarn/backend/qs-6.16.0
build(deps): bump qs from 6.15.3 to 6.16.0 in /backend
2026-09-23 14:06:28 +10:00
jc21 c4e421e679 Merge pull request #5830 from NginxProxyManager/dependabot/npm_and_yarn/test/humanfs/node-0.16.8
Bump @humanfs/node from 0.16.7 to 0.16.8 in /test
2026-09-23 14:06:13 +10:00
jc21 1e18bf9212 Merge pull request #5831 from cloudchristoph/fix/certbot-dns-azure-modern
fix(certbot): switch the Azure DNS plugin to certbot-dns-azure-modern
2026-09-23 14:06:00 +10:00
jc21 af1d4dd60f Merge pull request #5854 from hurole/patch-1
feat: add Tencent Cloud EdgeOne DNS provider
2026-09-23 13:59:16 +10:00
dependabot[bot] f47d85da9e build(deps-dev): bump the dev-minor-updates group across 1 directory with 5 updates
Bumps the dev-minor-updates group with 5 updates in the /frontend directory:

| Package | From | To |
| --- | --- | --- |
| [@tanstack/react-query-devtools](https://github.com/TanStack/query/tree/HEAD/packages/react-query-devtools) | `5.101.4` | `5.103.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.2.0` | `26.6.2` |
| [happy-dom](https://github.com/capricorn86/happy-dom) | `20.11.6` | `20.14.5` |
| [sass](https://github.com/sass/dart-sass) | `1.103.1` | `1.104.1` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.2.2` | `8.3.0` |



Updates `@tanstack/react-query-devtools` from 5.101.4 to 5.103.1
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query-devtools/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query-devtools@5.103.1/packages/react-query-devtools)

Updates `@types/node` from 26.2.0 to 26.6.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `happy-dom` from 20.11.6 to 20.14.5
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.11.6...v20.14.5)

Updates `sass` from 1.103.1 to 1.104.1
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.103.1...1.104.1)

Updates `vite` from 8.2.2 to 8.3.0
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@8.3.0/packages/vite)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query-devtools"
  dependency-version: 5.102.8
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: "@types/node"
  dependency-version: 26.5.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: happy-dom
  dependency-version: 20.14.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: sass
  dependency-version: 1.104.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: vite
  dependency-version: 8.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-23 03:56:05 +00:00
jc21 ee33ecdfc2 Merge pull request #5874 from NginxProxyManager/dependabot/npm_and_yarn/frontend/prod-patch-updates-d144f49a8f
build(deps): bump the prod-patch-updates group across 1 directory with 2 updates
2026-09-23 13:52:39 +10:00
jc21 f48c3e583a Merge pull request #5876 from NginxProxyManager/dependabot/npm_and_yarn/frontend/dev-patch-updates-846ffa5b48
build(deps-dev): bump the dev-patch-updates group across 1 directory with 5 updates
2026-09-23 13:52:21 +10:00
jc21 a1713b79e8 Merge pull request #5877 from hulkbig/fix/authenticated-login-redirect-5753
fix(router): redirect authenticated login visits to dashboard
2026-09-23 13:51:32 +10:00
carlosalbertorg 4282d6c6e9 feat: Add a Logs viewer to the admin UI
Nginx Proxy Manager had no way to inspect application or nginx logs
from the web UI - admins had to shell into the container or read
`docker logs`. This adds an admin-only Logs page that can tail:

- the backend application log, now also mirrored to
  /data/logs/backend.log (in addition to stdout) and rotated by the
  logrotate timer that already runs every 2 days
- the Let's Encrypt/certbot log, which certbot already writes to
  /data/logs/letsencrypt.log via its existing --logs-dir flag
- per-host nginx access/error logs (proxy, redirection, 404 and
  stream hosts), with the file path always resolved server-side from
  a validated host_type enum + numeric host_id, never from client
  input

Reads use a reverse chunked scan (64KB chunks, capped at 5MB scanned
per request) instead of loading whole files into memory, and the
frontend polls every 5s only while the tab is focused and "Live" is
on, so this stays cheap on both CPU and memory. No new runtime
dependencies were added on either side.

Purely additive: two new admin-only endpoints
(GET /api/logs/sources, GET /api/logs/tail), no existing behaviour
changed.
2026-09-22 13:11:38 -03:00
BigHulk 50144205a0 fix(router): redirect authenticated login visits to dashboard 2026-09-22 18:22:57 +08:00
jc21 73f784ea95 Merge pull request #5875 from NginxProxyManager/dependabot/npm_and_yarn/backend/prod-minor-updates-b9f1cef99c
build(deps): bump moment from 2.30.1 to 2.31.0 in /backend in the prod-minor-updates group
2026-09-22 07:12:03 +10:00
dependabot[bot] c8fe9abe38 build(deps-dev): bump the dev-patch-updates group across 1 directory with 5 updates
Bumps the dev-patch-updates group with 5 updates in the /frontend directory:

| Package | From | To |
| --- | --- | --- |
| [@formatjs/cli](https://github.com/formatjs/formatjs) | `6.16.19` | `6.16.30` |
| [@testing-library/dom](https://github.com/testing-library/dom-testing-library) | `10.4.1` | `10.4.2` |
| [@testing-library/react](https://github.com/testing-library/react-testing-library) | `16.3.2` | `16.3.3` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.1.0` | `6.1.1` |
| [postcss](https://github.com/postcss/postcss) | `8.5.26` | `8.5.28` |



Updates `@formatjs/cli` from 6.16.19 to 6.16.30
- [Release notes](https://github.com/formatjs/formatjs/releases)
- [Commits](https://github.com/formatjs/formatjs/compare/@formatjs/cli@6.16.19...@formatjs/cli@6.16.30)

Updates `@testing-library/dom` from 10.4.1 to 10.4.2
- [Release notes](https://github.com/testing-library/dom-testing-library/releases)
- [Changelog](https://github.com/testing-library/dom-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/dom-testing-library/compare/v10.4.1...v10.4.2)

Updates `@testing-library/react` from 16.3.2 to 16.3.3
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v16.3.2...v16.3.3)

Updates `@vitejs/plugin-react` from 6.1.0 to 6.1.1
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.1.1/packages/plugin-react)

Updates `postcss` from 8.5.26 to 8.5.28
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.26...8.5.28)

---
updated-dependencies:
- dependency-name: "@formatjs/cli"
  dependency-version: 6.16.30
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@testing-library/dom"
  dependency-version: 10.4.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@testing-library/react"
  dependency-version: 16.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.1.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: postcss
  dependency-version: 8.5.28
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-21 13:04:45 +00:00
dependabot[bot] 1cb352847a build(deps): bump moment in /backend in the prod-minor-updates group
Bumps the prod-minor-updates group in /backend with 1 update: [moment](https://github.com/moment/moment).


Updates `moment` from 2.30.1 to 2.31.0
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.30.1...2.31.0)

---
updated-dependencies:
- dependency-name: moment
  dependency-version: 2.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-21 13:04:26 +00:00
dependabot[bot] d11e0f6c51 build(deps): bump the prod-patch-updates group across 1 directory with 2 updates
Bumps the prod-patch-updates group with 2 updates in the /frontend directory: [query-string](https://github.com/sindresorhus/query-string) and [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom).


Updates `query-string` from 9.5.0 to 9.5.1
- [Release notes](https://github.com/sindresorhus/query-string/releases)
- [Commits](https://github.com/sindresorhus/query-string/compare/v9.5.0...v9.5.1)

Updates `react-router-dom` from 7.18.2 to 7.18.4
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/react-router-dom@7.18.4/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.18.4/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: query-string
  dependency-version: 9.5.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: react-router-dom
  dependency-version: 7.18.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-21 13:04:22 +00:00
jc21 9ad799c13f Merge pull request #5869 from zalaghi/feat/persian-locale
Add Persian locale
2026-09-21 07:25:22 +10:00
jc21 5d0be3a5d5 Merge pull request #5871 from qwist1233-cpu/tr-missing
Add missing Turkish (tr) translations
2026-09-21 07:20:43 +10:00
qwist1233-cpuandClaude Fable 5.1 735149210e Add missing Turkish (tr) translations
Adds the 31 keys present in en.json but missing from tr.json (2FA, certificate key type, trust forwarded proto).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 11:05:01 +03:00
hr.deng dba90c36be style: fix indentation in dns-plugins.json 2026-09-20 15:19:13 +08:00
Amir Zalaghi 0e152e8afe Add Persian locale 2026-09-18 22:12:13 +02:00
dependabot[bot] f2b1711549 build(deps): bump qs from 6.15.3 to 6.16.0 in /backend
Bumps [qs](https://github.com/ljharb/qs) from 6.15.3 to 6.16.0.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.15.3...v6.16.0)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.16.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-14 21:38:16 +00:00
jc21 568c105945 Merge pull request #5858 from NginxProxyManager/dependabot/npm_and_yarn/backend/prod-patch-updates-14e248b7a3
Bump the prod-patch-updates group across 1 directory with 3 updates
2026-09-15 07:35:48 +10:00
jc21 3105a23834 Merge pull request #5861 from NginxProxyManager/dependabot/npm_and_yarn/frontend/prod-minor-updates-2b7ce5bc75
Bump the prod-minor-updates group across 1 directory with 6 updates
2026-09-15 07:35:21 +10:00
dependabot[bot] 803f4968a2 Bump the prod-minor-updates group across 1 directory with 6 updates
Bumps the prod-minor-updates group with 6 updates in the /frontend directory:

| Package | From | To |
| --- | --- | --- |
| [@tabler/core](https://github.com/tabler/tabler) | `1.4.0` | `1.5.1` |
| [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.101.4` | `5.102.8` |
| [@tanstack/react-table](https://github.com/TanStack/table/tree/HEAD/packages/react-table) | `9.1.2` | `9.2.4` |
| [react](https://github.com/react/react/tree/HEAD/packages/react) | `19.2.8` | `19.3.0` |
| [react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom) | `19.2.8` | `19.3.0` |
| [react-intl](https://github.com/formatjs/formatjs) | `10.1.22` | `10.2.0` |



Updates `@tabler/core` from 1.4.0 to 1.5.1
- [Release notes](https://github.com/tabler/tabler/releases)
- [Changelog](https://github.com/tabler/tabler/blob/dev/docs/CHANGELOG.md)
- [Commits](https://github.com/tabler/tabler/compare/@tabler/core@1.4.0...@tabler/core@1.5.1)

Updates `@tanstack/react-query` from 5.101.4 to 5.102.8
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.102.8/packages/react-query)

Updates `@tanstack/react-table` from 9.1.2 to 9.2.4
- [Release notes](https://github.com/TanStack/table/releases)
- [Changelog](https://github.com/TanStack/table/blob/main/packages/react-table/CHANGELOG.md)
- [Commits](https://github.com/TanStack/table/commits/@tanstack/react-table@9.2.4/packages/react-table)

Updates `react` from 19.2.8 to 19.3.0
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.3.0/packages/react)

Updates `react-dom` from 19.2.8 to 19.3.0
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.3.0/packages/react-dom)

Updates `react-intl` from 10.1.22 to 10.2.0
- [Release notes](https://github.com/formatjs/formatjs/releases)
- [Commits](https://github.com/formatjs/formatjs/compare/react-intl@10.1.22...react-intl@10.2.0)

---
updated-dependencies:
- dependency-name: "@tabler/core"
  dependency-version: 1.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.102.8
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: "@tanstack/react-table"
  dependency-version: 9.2.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react
  dependency-version: 19.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react-dom
  dependency-version: 19.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react-intl
  dependency-version: 10.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-14 13:04:46 +00:00
dependabot[bot] d8488048ca Bump the prod-patch-updates group across 1 directory with 3 updates
Bumps the prod-patch-updates group with 3 updates in the /backend directory: [@apidevtools/json-schema-ref-parser](https://github.com/APIDevTools/json-schema-ref-parser), [compression](https://github.com/expressjs/compression) and [mysql2](https://github.com/sidorares/node-mysql2).


Updates `@apidevtools/json-schema-ref-parser` from 16.0.0 to 16.0.2
- [Release notes](https://github.com/APIDevTools/json-schema-ref-parser/releases)
- [Commits](https://github.com/APIDevTools/json-schema-ref-parser/compare/v16.0.0...v16.0.2)

Updates `compression` from 1.8.1 to 1.8.2
- [Release notes](https://github.com/expressjs/compression/releases)
- [Changelog](https://github.com/expressjs/compression/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/compression/compare/v1.8.1...v1.8.2)

Updates `mysql2` from 3.24.2 to 3.24.4
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.24.2...v3.24.4)

---
updated-dependencies:
- dependency-name: "@apidevtools/json-schema-ref-parser"
  dependency-version: 16.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: compression
  dependency-version: 1.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: mysql2
  dependency-version: 3.24.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-14 13:04:01 +00:00
Crazy D 0596581ec9 feat: add Tencent Cloud EdgeOne DNS provider
### Summary

    This PR adds support for **Tencent Cloud EdgeOne (TEO)** as a DNS provider for Let's Encrypt DNS-01 certificate validation using the [`certbot-dns-
  edgeone`](<https://pypi.org/project/certbot-dns-edgeone/>) plugin.

### Details

    - **Provider Name:** Tencent Cloud EdgeOne
    - **Plugin Name:** `dns-edgeone`
    - **PyPI Package:** [`certbot-dns-edgeone`](https://pypi.org/project/certbot-dns-edgeone/) (v0.1.0+)
    - **Plugin Repository:** https://github.com/hurole/certbot-dns-edgeone
    - **License:** Apache-2.0

    ### Credentials Template

    ```ini
    dns_edgeone_secret_id = YOUR_TENCENTCLOUD_SECRET_ID
    dns_edgeone_secret_key = YOUR_TENCENTCLOUD_SECRET_KEY
    ```

### Verification

  • Verified certbot-dns-edgeone package installation and entrypoint discovery with Certbot.
  • Verified DNS-01 TXT record creation and cleanup flows via unit tests.
  • Formatted in backend/certbot/dns-plugins.json in alphabetical order.
2026-09-12 18:25:54 +08:00
jc21 58605cbc70 Merge pull request #5848 from siimaarmaa/develop
Translate the entire user interface into Estonian and ensure that the Estonian flag is displayed./Tõlgi kogu kasutajaliides eesti keelde ja kinnita Eesti lipp.
2026-09-09 10:11:03 +10:00
jc21 280fcde528 Merge pull request #5834 from 12LuA/patch-1
Remove broken sorting from Audit Logs
2026-09-09 07:27:36 +10:00
jc21 c70fcb9f7c Merge pull request #5836 from jmrplens/fix/invalidate-tokens-on-password-change
Invalidate tokens issued before a password change
2026-09-09 07:27:00 +10:00
Cursor AgentandSiim Aarmaa f477946bd9 Tõlgi kogu kasutajaliides eesti keelde ja kinnita Eesti lipp.
UI-tekstid olid eesti keele valikus, aga sisu oli inglise keeles. Nüüd on et.json täielikult eesti keeles (sh puudunud võtmed), abitekstid ühtlustatud ja lipukoodiks jääb EE (Eesti), mitte ET (Etioopia). Kuupäevad kasutavad et-EE lokaati ja 24-tunnist kellaaega.

Muudatuse tegi Aarmaa IT - Siim Aarmaa

Co-authored-by: Siim Aarmaa <siimaarmaa@users.noreply.github.com>
2026-09-08 15:52:39 +00:00
jc21 18f1fe22b1 Merge pull request #5837 from jamalkamaladdin/feat/az-locale
Add Azerbaijani locale
2026-09-08 07:04:06 +10:00
jc21 9fc56a2209 Merge pull request #5840 from NginxProxyManager/dependabot/npm_and_yarn/test/mocha-12.0.0
Bump mocha from 11.8.0 to 12.0.0 in /test
2026-09-08 07:03:37 +10:00
jc21 37b19d892c Merge pull request #5841 from NginxProxyManager/dependabot/npm_and_yarn/test/cypress-16.0.0
Bump cypress from 15.21.1 to 16.0.0 in /test
2026-09-08 07:03:25 +10:00
jc21 17f126482f Merge pull request #5838 from NginxProxyManager/dependabot/npm_and_yarn/backend/apidevtools/swagger-parser-13.0.0
Bump @apidevtools/swagger-parser from 12.1.0 to 13.0.0 in /backend
2026-09-08 07:03:09 +10:00
dependabot[bot] 81ec7e8d6a Bump cypress from 15.21.1 to 16.0.0 in /test
Bumps [cypress](https://github.com/cypress-io/cypress) from 15.21.1 to 16.0.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v15.21.1...v16.0.0)

---
updated-dependencies:
- dependency-name: cypress
  dependency-version: 16.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-07 13:03:41 +00:00
dependabot[bot] dd45ad8f51 Bump mocha from 11.8.0 to 12.0.0 in /test
Bumps [mocha](https://github.com/mochajs/mocha) from 11.8.0 to 12.0.0.
- [Release notes](https://github.com/mochajs/mocha/releases)
- [Changelog](https://github.com/mochajs/mocha/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mochajs/mocha/compare/v11.8.0...v12.0.0)

---
updated-dependencies:
- dependency-name: mocha
  dependency-version: 12.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-07 13:03:33 +00:00
dependabot[bot] 33250a0e38 Bump @apidevtools/swagger-parser from 12.1.0 to 13.0.0 in /backend
Bumps [@apidevtools/swagger-parser](https://github.com/APIDevTools/swagger-parser) from 12.1.0 to 13.0.0.
- [Release notes](https://github.com/APIDevTools/swagger-parser/releases)
- [Changelog](https://github.com/APIDevTools/swagger-parser/blob/main/CHANGELOG.md)
- [Commits](https://github.com/APIDevTools/swagger-parser/compare/v12.1.0...v13.0.0)

---
updated-dependencies:
- dependency-name: "@apidevtools/swagger-parser"
  dependency-version: 13.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-07 13:03:22 +00:00
Jamal 2070fd186a Add Azerbaijani locale 2026-09-07 11:58:07 +04:00
José M. Requena Plens e4585ac688 Stamp the password change from the app clock, not the database one
Your CI caught this: the check passed on SQLite and never fired on the
stack where the database container runs on a different timezone from the
app, so a stale token stayed valid. The comparison was between a token's
`iat`, which is UTC seconds from Node, and `auth.modified_on`, which the
driver hands back interpreted in the app's timezone. With the app on
Australia/Brisbane and the database on UTC, that column comes back ten
hours in the past and the token always looks newer than the change.

Record the moment in `auth.meta.password_changed_at` instead, written by
`setPassword` with the same `Date.now()` clock that mints `iat`. Same
unit on both sides, one clock, and no timestamp parsing: the Date and
local-string branch is gone, and so is the whole-second flooring that
Postgres microseconds made necessary.

Rows written before this have no marker and revoke nothing until their
next password change, which is the safe direction to be wrong in.
2026-09-06 20:15:12 +02:00
José M. Requena Plens 1ffe3609f4 Invalidate tokens issued before a password change
Tokens are stateless JWTs, so changing a password left every session that
the old one had opened working until its own expiry, up to a day later.
That is the case the password change is meant to close: an administrator
resetting a compromised account did not evict whoever was already in it.

The auth row already records when the password last changed, so no
migration is needed: `Access.init()` reads it alongside the user it
already loads and refuses a token whose `iat` is older. Both sides are
compared as whole seconds, which is all `iat` carries, so a token minted
in the same second as the change is kept. Postgres stores that column to
the microsecond, which is why the comparison is not done in milliseconds.

It is reported as 401 rather than the usual 403 because that is what the
frontend clears the session on, so the browser holding the dead token
lands on the login page instead of a page full of errors, and `can()`
lets that one error through unwrapped for the same reason.

Only the password does this. A user row changing (a rename, an avatar,
permissions) does not, and a user with no password auth row, which is
what a login through an external provider looks like, is not affected.
2026-09-06 19:48:02 +02:00
Luca 24b072a120 fix: Rename user.avatar to owner and disable sorting 2026-09-06 17:31:47 +02:00
Christoph Vollmann e482522792 fix(certbot): shorten the Azure credentials template
The template was the longest of all plugins (1181 characters, nine comment lines) and carried the upstream example secret that trips secret scanning. It now shows what a user has to fill in: the service principal, and one zone line in the format ZONE_NAME:RESOURCE_GROUP_ID, plus a link to the docs for everything else.
2026-09-04 22:55:19 +02:00
Christoph Vollmann 23b4e7d6f7 fix(certbot): switch the Azure DNS plugin to certbot-dns-azure-modern
certbot-dns-azure 2.6.1 requires certbot<4.0; installing it into the image's certbot 5.x venv downgrades certbot and acme to 3.3.0, and acme 3.3.0 no longer imports against pyOpenSSL 26 (#5606). The maintained fork certbot-dns-azure-modern keeps module, entry point, flags and credentials format, requires certbot>=3.0 without an upper bound and declares its own azure-mgmt-dns range, so the extra dependency pin is no longer needed.
2026-09-04 22:24:38 +02:00
jc21 a2d427902a Merge pull request #5828 from NginxProxyManager/dependabot/npm_and_yarn/backend/fast-uri-3.1.7
Bump fast-uri from 3.1.5 to 3.1.7 in /backend
2026-09-04 07:42:57 +10:00
dependabot[bot] 05b867c21d Bump @humanfs/node from 0.16.7 to 0.16.8 in /test
Bumps [@humanfs/node](https://github.com/humanwhocodes/humanfs/tree/HEAD/packages/node) from 0.16.7 to 0.16.8.
- [Release notes](https://github.com/humanwhocodes/humanfs/releases)
- [Changelog](https://github.com/humanwhocodes/humanfs/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/humanwhocodes/humanfs/commits/node-v0.16.8/packages/node)

---
updated-dependencies:
- dependency-name: "@humanfs/node"
  dependency-version: 0.16.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 11:58:04 +00:00
dependabot[bot] ebbd262e06 Bump fast-uri from 3.1.5 to 3.1.7 in /backend
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.5 to 3.1.7.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.5...v3.1.7)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-02 22:41:08 +00:00
jc21 5e6c53f0e2 Merge pull request #5726 from NginxProxyManager/dependabot/npm_and_yarn/test/axios-1.18.0
Bump axios from 1.19.0 to 1.20.0 in /test
2026-09-01 07:10:25 +10:00
jc21 baa5683bba Merge pull request #5823 from NginxProxyManager/dependabot/npm_and_yarn/backend/prod-minor-updates-1a71c66db8
Bump mysql2 from 3.23.4 to 3.24.2 in /backend in the prod-minor-updates group
2026-09-01 07:10:12 +10:00
jc21 386a166025 Merge pull request #5820 from NginxProxyManager/dependabot/npm_and_yarn/test/cypress/grep-7.0.0
Bump @cypress/grep from 6.0.3 to 7.0.0 in /test
2026-09-01 07:09:32 +10:00
dependabot[bot] f7a696cf08 Bump mysql2 in /backend in the prod-minor-updates group
Bumps the prod-minor-updates group in /backend with 1 update: [mysql2](https://github.com/sidorares/node-mysql2).


Updates `mysql2` from 3.23.4 to 3.24.2
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.23.4...v3.24.2)

---
updated-dependencies:
- dependency-name: mysql2
  dependency-version: 3.24.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 13:04:27 +00:00
dependabot[bot] ec65105db2 Bump @cypress/grep from 6.0.3 to 7.0.0 in /test
Bumps [@cypress/grep](https://github.com/cypress-io/cypress) from 6.0.3 to 7.0.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/@cypress/grep-v6.0.3...@cypress/grep-v7.0.0)

---
updated-dependencies:
- dependency-name: "@cypress/grep"
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 13:04:01 +00:00
dependabot[bot] aa211ea423 Bump axios from 1.19.0 to 1.20.0 in /test
Bumps [axios](https://github.com/axios/axios) from 1.19.0 to 1.20.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.19.0...v1.20.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.18.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 10:43:00 +00:00
jc21 cd4448ebef Merge pull request #5713 from NginxProxyManager/dependabot/npm_and_yarn/test/systeminformation-5.31.17
Bump systeminformation from 5.31.6 to 5.31.17 in /test
2026-08-31 20:41:29 +10:00
jc21 e2546bfb51 Merge pull request #5737 from NginxProxyManager/dependabot/npm_and_yarn/frontend/immutable-5.1.9
Bump immutable from 5.1.5 to 5.1.9 in /frontend
2026-08-31 20:40:55 +10:00
jc21 c19182d978 Merge pull request #5743 from NginxProxyManager/dependabot/npm_and_yarn/test/tar-7.5.22
Bump tar from 7.5.15 to 7.5.22 in /test
2026-08-31 20:40:38 +10:00
jc21 1cbfe370ec Merge pull request #5756 from NginxProxyManager/dependabot/npm_and_yarn/docs/postcss-8.5.25
Bump postcss from 8.5.14 to 8.5.25 in /docs
2026-08-31 20:40:25 +10:00
jc21 6c2a57ad83 Merge pull request #5769 from NginxProxyManager/dependabot/npm_and_yarn/test/fast-uri-3.1.5
Bump fast-uri from 3.1.2 to 3.1.6 in /test
2026-08-31 20:40:10 +10:00
Shawn Hank 210366cca0 Stop recreating DNS credentials files on every backend restart
setupCertbotPlugins() wrote a credentials file for every DNS-01 certificate
each time the backend started, using flag "wx" so it only filled in missing
ones. That existed because the renew path did not write the file itself, so
something had to put it back before `certbot renew` looked for it.

With the previous commit the renew path writes the file immediately before
invoking certbot, so this is now the only thing putting those credentials back
on disk - and it does so for every certificate on every restart, which undoes
the cleanup entirely.

Removing the write leaves the `fs` import and the `promises` array unused. The
"Added Certbot plugins" log line is kept but now gates on plugins.length, since
it was previously gated on a promise array that only ever held credential
writes.
2026-08-29 00:11:15 -06:00
jc21 b75b0adaf2 Merge pull request #5812 from vzagorovskiy/fix/keep-failed-nginx-config-as-err
Keep the failed nginx config as a .conf.err file
2026-08-29 10:14:42 +10:00
Shawn Hank 918728fcac Remove DNS provider credentials from disk after certbot runs
The credentials file written for a DNS-01 challenge was only cleaned up when
certbot failed - the unlink sat in a catch block. On success the file stayed in
/etc/letsencrypt/credentials for the entire life of the certificate, holding a
live DNS provider API token in plaintext.

The file cannot simply be deleted at issuance, because certbot records its path
in the renewal config and reads it back on every `certbot renew`. So the renew
path now writes the file itself immediately before invoking certbot, and both
paths remove it in a finally block.

Net effect: the credentials exist on disk for the duration of a certbot run
rather than permanently. The value still lives in the certificates table, which
is unavoidable - it has to come from somewhere to be written at all.

renewLetsEncryptSslWithDnsChallenge reads the row directly from the model
because renew() sources its certificate from internalCertificate.get(), which
strips meta.dns_provider_credentials via omissions().
2026-08-28 17:40:21 -06:00
vzagorovskiy 0453ddca35 Use the row's own model and host type in regenerate-config
processItems() passed proxyHostModel and the literal "proxy_host" to
configure() for every host type. host_type selects both the template and
the output path, and each host type has its own id sequence, so
redirection hosts, 404 hosts and streams were rendered through
proxy_host.conf and written over /data/nginx/proxy_host/<id>.conf. The
proxy host sharing that id lost its config file and had the resulting
nginx error recorded in its own meta.
2026-08-28 12:02:52 +03:00
vzagorovskiy a570c0e503 Keep the failed nginx config as a .conf.err file
When `nginx -t` fails, configure() is meant to move the broken config to
<id>.conf.err so the failure can be inspected. renameConfigAsError()
unlinked the source file before renaming it, so the rename always failed
and the config was simply deleted. The deleteConfig() call after it then
removed any .err file left over from an earlier failure.

- unlink the destination .err file instead of the source
- return the rename promise so the delete does not race it
- pass delete_err_file = false so the new .err file survives
- drop the stale 4th argument in the success path, which silently made
  delete_err_file false and left old .err files behind
2026-08-28 11:47:02 +03:00
jc21 6383017b4c Merge pull request #5780 from NginxProxyManager/dependabot/npm_and_yarn/test/js-yaml-4.3.1
Bump js-yaml from 4.1.1 to 4.3.1 in /test
2026-08-28 16:29:35 +10:00
jc21 3f2cd26913 Merge pull request #5789 from quokkawiki/develop
Fixed Estonian flag bug
2026-08-28 14:16:55 +10:00
jc21 ece5dea0ad Merge branch 'develop' into develop 2026-08-28 07:57:25 +10:00
jc21 a75eb467ca Merge pull request #5807 from vgoer/changes-not-taking-effect
fix: Changes not taking effect
2026-08-28 07:51:55 +10:00
jc21 708ea5cfbb Merge pull request #5809 from tapacko88/feat/ukrainian-locale
Add Ukrainian locale
2026-08-28 07:50:51 +10:00
tapacko d77d5c643d Add Ukrainian locale 2026-08-26 23:39:25 +01:00
jc21 934a3fafe5 Merge pull request #5765 from fatihemre/feat/collapsible-custom-locations
Collapse custom locations into a filterable list
2026-08-27 07:42:43 +10:00
vgoer 08b4bbdbd4 fix: Changes not taking effect 2026-08-26 07:39:31 +00:00
jc21 a7677c18c3 Merge branch 'develop' into feat/collapsible-custom-locations 2026-08-26 13:00:05 +10:00
dependabot[bot] 1d894f8ed1 Bump fast-uri from 3.1.2 to 3.1.6 in /test
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.2 to 3.1.6.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.2...v3.1.6)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-26 02:59:09 +00:00
dependabot[bot] 7cde02c446 Bump js-yaml from 4.1.1 to 4.3.1 in /test
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.3.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/4.3.1/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.3.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-26 02:59:07 +00:00
jc21 190b7a606d Merge pull request #5763 from NginxProxyManager/dependabot/github_actions/actions/stale-11
Bump actions/stale from 10 to 11
2026-08-26 12:57:43 +10:00
jc21 61e74cbb7f Merge pull request #5773 from zergflag/develop
Add LWS DNS provider
2026-08-26 12:57:00 +10:00
Jamie Curnow ba8117fbcf Update test deps 2026-08-26 12:56:32 +10:00
jc21 5e7fea7d56 Merge pull request #5787 from CallMeLeon167/develop
Add German translations for two-factor authentication and some UI elements
2026-08-26 12:54:23 +10:00
jc21 4f1d309a90 Merge pull request #5788 from setrin/fix/update-websupport-certbot-plugin
fix(certbot): update Websupport DNS plugin to 5.0.0
2026-08-26 12:52:52 +10:00
jc21 58ff491dc0 Merge pull request #5795 from xzy103/fix/tencentcloud-dns-apex-wildcard
Fix Tencent Cloud apex and wildcard certificate issuance
2026-08-26 12:52:00 +10:00
jc21 85ee5e2200 Merge pull request #5800 from frenck/fix/update-timeweb-certbot-plugin
fix(certbot): update Timeweb Cloud DNS plugin to 2.0.0
2026-08-26 12:50:39 +10:00
Jamie Curnow 09300941af Add white padding to qrcode, especially in dark mode 2026-08-26 12:48:36 +10:00
jc21 c4fa8daa87 Merge pull request #5801 from vgoer/qr-services-2FA
Switched to local QR code generation to prevent leaking 2FA keys to third-party QR services.
2026-08-26 12:47:56 +10:00
jc21 3d800c7138 Merge branch 'develop' into qr-services-2FA 2026-08-26 08:07:11 +10:00
Jamie Curnow 2ed0c47de8 Fix #5802 404 hosts disable missing async/await paradigm 2026-08-26 08:02:52 +10:00
vgoer 4d30946d2f fix: local QR code 2026-08-25 09:14:44 +00:00
vgoer a5e49a53c0 feat: Add react-qr-code dependency 2026-08-25 09:13:41 +00:00
Franck Nijhof 41539c027f fix(certbot): update Timeweb Cloud DNS plugin to 2.0.0 2026-08-25 10:15:38 +02:00
jc21 046d3e022f Merge pull request #5798 from vgoer/develop
Fix: Insufficient private key permissions led to a leak and  Effective fixing when setting PUID and PGid environment variables
2026-08-25 07:41:27 +10:00
vgoer c98b1685bd Merge pull request #1 from vgoer/private-key-permissions
Insufficient private key permissions led to a leak.
2026-08-24 17:49:40 +08:00
vgoer b7772016ba fix: Ensure JWT key file ownership matches PUID/PGID at startup 2026-08-24 09:30:36 +00:00
vgoer 29687987b2 fix: Restrict JWT key file permissions to 0600 2026-08-24 09:30:22 +00:00
jc21 ab893706da Merge pull request #5796 from timothestoifl24/claude/nifty-neumann-83e87c
Fix missing `new` on PermissionError so denied requests return 403, not 404
2026-08-23 00:11:02 +10:00
Timothé Stoifl 246c54503e Merge branch 'develop' into claude/nifty-neumann-83e87c 2026-08-22 15:40:15 +02:00
Jamie Curnow 4ecb047299 Update frontend packages
- Major update for react-table
- Fix Hosts menu staying around after selecting a menu item
2026-08-22 23:23:20 +10:00
Jamie Curnow 4e2b052b50 Updated backend packages 2026-08-22 23:10:26 +10:00
Timothé Stoifl ce4e35a3d4 Fix missing new on PermissionError in access.can()
The catch block in `access.can()` constructed `errs.PermissionError`
without `new`. The error constructors in `backend/lib/error.js` are plain
constructor functions that assign to `this` and return nothing, so calling
one without `new` evaluates to `undefined`.

The statement therefore did `throw undefined`, the express error handler
in `backend/app.js` received undefined (and could not read `.status` or
`.public` off it), and the request fell through to the catch-all 404
handler in `backend/routes/main.js`.

Net effect: every authorization failure raised by `access.can(...)` was
reported to clients as `404 Not Found` instead of `403 Forbidden`.

Line 45 of the same file already used `new` correctly, which shows this
was an oversight rather than deliberate resource-existence hiding. A grep
over `backend/` confirms this was the only error constructor invoked
without `new`.
2026-08-22 13:07:09 +02:00
Silas 4cbeecaad2 fix: bump certbot-dns-tencentcloud to 2.1.1 2026-08-22 16:19:50 +08:00
Daniel Jacob Butcher fc8b1eb1fb Fixed Estonian flag bug
The Estonian flag was appearing as an Ethiopian flag, due to a mixup of ISO 639 language codes and ISO 3166 country codes
2026-08-20 00:04:47 +03:00
Daniel Slavkovsky d64d9a8ab2 fix(certbot): update Websupport DNS plugin to 5.0.0 2026-08-18 11:13:20 +02:00
Leon Schmidt 70f00d60be Add German translations for two-factor authentication and various UI elements 2026-08-18 10:19:06 +02:00
zergflag f43ef16fb5 Update dns-plugins.json 2026-08-09 14:59:39 +02:00
Dimas R. Wisnu 35ad8227d7 fix: resolve biome lint errors
- Use optional chaining for nullable checks
- Replace template literals with string literals for plain SQL
- Add node: protocol to fs/promises import in setup.js
2026-08-07 17:01:20 +07:00
Dimas R. Wisnu 4a0f212289 feat: per-path access lists, host logs modal, PostgreSQL support
- Per-path access lists: assign different access lists to individual
  locations on the same proxy host
- Host logs modal: view access/error logs from proxy host dropdown
- PostgreSQL JSON containment query (@>) for location regeneration
- Locale keys: action.logs, column.error
2026-08-07 16:40:32 +07:00
Fatih Emre ab96d78f4d Collapse custom locations into a filterable list
The Custom Locations tab rendered every location fully expanded, which
becomes unusable once a host has more than a handful of them. Each row is
now a collapsible card showing its path and forward target, collapsed by
default, with the Add Location button moved to the top so it stays
reachable without scrolling to the end of the list.

A filter appears once there are five or more locations and matches on the
path as well as the forward scheme, host and port.

New locations are still appended to the end of the list, so the generated
nginx config is unchanged.

Also fixes two existing issues in this component: the per-location inputs
shared the same DOM ids across rows, and the advanced-config toggle state
was keyed by array index, so removing a location moved it onto the wrong
row.
2026-08-04 16:11:46 +03:00
dependabot[bot] a42614094f Bump actions/stale from 10 to 11
Bumps [actions/stale](https://github.com/actions/stale) from 10 to 11.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v10...v11)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: '11'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-03 13:06:48 +00:00
dependabot[bot] 2cd346d054 Bump postcss from 8.5.14 to 8.5.25 in /docs
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.14 to 8.5.25.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.14...8.5.25)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.25
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-02 06:13:35 +00:00
dependabot[bot] 20b8068aaa Bump tar from 7.5.15 to 7.5.22 in /test
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.15 to 7.5.22.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.15...v7.5.22)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.22
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-26 16:21:19 +00:00
addielarue 9aa7fd5722 fix(stream): bracket IPv6 forward host for valid nginx upstream
A Stream Host with an IPv6 address as the Forward Host was accepted and
saved but never activated. The generated stream config rendered
`proxy_pass {{ forwarding_host }}:{{ forwarding_port }}` as e.g.
`fe80::528:3c87:e7bb:ab08:25`, which nginx rejects with
"invalid port in upstream" because an IPv6 literal must be wrapped in
square brackets before the port is appended (`[fe80::...]:25`).

Normalize the stream forward host in generateConfig (alongside the existing
per-host-type data massaging) using net.isIPv6(), so IPv6 hosts render as
`[address]:port` while IPv4 addresses and hostnames are emitted unchanged.
The mutation is applied to the deep-copied render object, so persisted and
audit data are unaffected.

Fixes #5740
2026-07-25 22:55:58 +10:00
dependabot[bot] 553f879c06 Bump immutable from 5.1.5 to 5.1.9 in /frontend
Bumps [immutable](https://github.com/immutable-js/immutable-js) from 5.1.5 to 5.1.9.
- [Release notes](https://github.com/immutable-js/immutable-js/releases)
- [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/immutable-js/immutable-js/compare/v5.1.5...v5.1.9)

---
updated-dependencies:
- dependency-name: immutable
  dependency-version: 5.1.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 08:31:47 +00:00
dependabot[bot] cbf2782571 Bump systeminformation from 5.31.6 to 5.31.17 in /test
Bumps [systeminformation](https://github.com/sebhildebrandt/systeminformation) from 5.31.6 to 5.31.17.
- [Release notes](https://github.com/sebhildebrandt/systeminformation/releases)
- [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.31.6...v5.31.17)

---
updated-dependencies:
- dependency-name: systeminformation
  dependency-version: 5.31.17
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-16 00:15:51 +00:00
Jamie Curnow a62c2a6dc3 Adds support for custom admin port via env var #3808
and documentation
2026-06-10 11:24:53 +10:00
jc21 a5db5ed156 Merge pull request #5498 from Yasha-ops/develop
fix: Changed order of escape to prevent RCE
2026-06-08 08:51:46 +10:00
jc21 101595b90b Merge pull request #5616 from NginxProxyManager/dependabot/npm_and_yarn/backend/node-rsa-2.0.0
Bump node-rsa from 1.1.1 to 2.0.0 in /backend
2026-06-08 08:36:58 +10:00
Yassine Damiri 45a3659ffd Change fs import to use fs/promises 2026-06-04 15:08:31 +02:00
Yassine Damiri a457fe4d96 Merge branch 'develop' into develop 2026-06-04 15:06:42 +02:00
jc21 33ef7c859e Merge pull request #5592 from NginxProxyManager/dependabot/npm_and_yarn/test/tmp-0.2.6
Bump tmp from 0.2.5 to 0.2.7 in /test
2026-06-04 14:22:00 +10:00
jc21 bfa2dca1b1 Merge pull request #5626 from NginxProxyManager/dependabot/npm_and_yarn/frontend/prod-minor-updates-d03944a3ba
Bump the prod-minor-updates group across 1 directory with 4 updates
2026-06-04 14:21:43 +10:00
jc21 32fc268edf Merge pull request #5622 from fo5for/update-s6
Update s6 overlay to 3.2.3.0
2026-06-04 07:29:55 +10:00
dependabot[bot] 300d0653e0 Bump the prod-minor-updates group across 1 directory with 4 updates
Bumps the prod-minor-updates group with 4 updates in the /frontend directory: [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query), [date-fns](https://github.com/date-fns/date-fns), [query-string](https://github.com/sindresorhus/query-string) and [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom).


Updates `@tanstack/react-query` from 5.100.14 to 5.101.0
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.101.0/packages/react-query)

Updates `date-fns` from 4.3.0 to 4.4.0
- [Release notes](https://github.com/date-fns/date-fns/releases)
- [Commits](https://github.com/date-fns/date-fns/compare/v4.3.0...v4.4.0)

Updates `query-string` from 9.3.1 to 9.4.0
- [Release notes](https://github.com/sindresorhus/query-string/releases)
- [Commits](https://github.com/sindresorhus/query-string/compare/v9.3.1...v9.4.0)

Updates `react-router-dom` from 7.15.1 to 7.16.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.16.0/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.101.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: date-fns
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: query-string
  dependency-version: 9.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react-router-dom
  dependency-version: 7.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 21:22:00 +00:00
dependabot[bot] 06f060dc0f Bump tmp from 0.2.5 to 0.2.7 in /test
Bumps [tmp](https://github.com/raszi/node-tmp) from 0.2.5 to 0.2.7.
- [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/raszi/node-tmp/compare/v0.2.5...v0.2.7)

---
updated-dependencies:
- dependency-name: tmp
  dependency-version: 0.2.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 21:21:13 +00:00
jc21 7e447acfcc Merge pull request #5617 from NginxProxyManager/dependabot/npm_and_yarn/frontend/prod-patch-updates-509dafd62b
Bump the prod-patch-updates group in /frontend with 3 updates
2026-06-04 07:19:19 +10:00
jc21 acd26d7adb Merge pull request #5612 from NginxProxyManager/dependabot/npm_and_yarn/test/prod-patch-updates-e5e2dd2d94
Bump the prod-patch-updates group in /test with 2 updates
2026-06-04 07:18:45 +10:00
jc21 1f5a56d0bd Merge pull request #5615 from NginxProxyManager/dependabot/npm_and_yarn/frontend/dev-patch-updates-6b7b087425
Bump the dev-patch-updates group in /frontend with 4 updates
2026-06-04 07:18:24 +10:00
jc21 258b522725 Merge branch 'develop' into dependabot/npm_and_yarn/backend/node-rsa-2.0.0 2026-06-04 07:17:53 +10:00
jc21 179c27e382 Merge pull request #5275 from 7heMech/develop
Add reuseport to improve performance and reduce packet drops
2026-06-04 07:16:50 +10:00
Tony Chau 94176d2f9c Update s6 overlay to 3.2.3.0 2026-06-03 20:03:29 +08:00
jc21 76f09db610 Merge pull request #5619 from NginxProxyManager/develop
v2.15.1
2026-06-03 14:14:30 +10:00
jc21 d51602b150 Merge branch 'master' into develop 2026-06-03 08:23:48 +10:00
Jamie Curnow af2de98c41 Bump version 2026-06-03 08:22:27 +10:00
dependabot[bot] 6311329772 Bump the prod-patch-updates group in /frontend with 3 updates
Bumps the prod-patch-updates group in /frontend with 3 updates: [react](https://github.com/facebook/react/tree/HEAD/packages/react), [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) and [react-intl](https://github.com/formatjs/formatjs).


Updates `react` from 19.2.6 to 19.2.7
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react)

Updates `react-dom` from 19.2.6 to 19.2.7
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react-dom)

Updates `react-intl` from 10.1.9 to 10.1.11
- [Release notes](https://github.com/formatjs/formatjs/releases)
- [Commits](https://github.com/formatjs/formatjs/compare/react-intl@10.1.9...react-intl@10.1.11)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: react-dom
  dependency-version: 19.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: react-intl
  dependency-version: 10.1.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-02 14:00:10 +00:00
dependabot[bot] a08f821c54 Bump node-rsa from 1.1.1 to 2.0.0 in /backend
Bumps [node-rsa](https://github.com/rzcoder/node-rsa) from 1.1.1 to 2.0.0.
- [Release notes](https://github.com/rzcoder/node-rsa/releases)
- [Changelog](https://github.com/rzcoder/node-rsa/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rzcoder/node-rsa/compare/1.1.1...v2.0.0)

---
updated-dependencies:
- dependency-name: node-rsa
  dependency-version: 2.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-02 13:59:29 +00:00
dependabot[bot] fc4a409292 Bump the dev-patch-updates group in /frontend with 4 updates
Bumps the dev-patch-updates group in /frontend with 4 updates: [@formatjs/cli](https://github.com/formatjs/formatjs), [tmp](https://github.com/raszi/node-tmp), [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `@formatjs/cli` from 6.16.3 to 6.16.6
- [Release notes](https://github.com/formatjs/formatjs/releases)
- [Commits](https://github.com/formatjs/formatjs/compare/@formatjs/cli@6.16.3...@formatjs/intl-datetimeformat@6.16.6)

Updates `tmp` from 0.2.6 to 0.2.7
- [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/raszi/node-tmp/compare/v0.2.6...v0.2.7)

Updates `vite` from 8.0.14 to 8.0.16
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

Updates `vitest` from 4.1.7 to 4.1.8
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.8/packages/vitest)

---
updated-dependencies:
- dependency-name: "@formatjs/cli"
  dependency-version: 6.16.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: tmp
  dependency-version: 0.2.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: vitest
  dependency-version: 4.1.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-02 13:58:55 +00:00
dependabot[bot] 62ed330053 Bump the prod-patch-updates group in /test with 2 updates
Bumps the prod-patch-updates group in /test with 2 updates: [@cypress/grep](https://github.com/cypress-io/cypress) and [eslint](https://github.com/eslint/eslint).


Updates `@cypress/grep` from 6.0.0 to 6.0.1
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/@cypress/grep-v6.0.0...@cypress/grep-v6.0.1)

Updates `eslint` from 10.4.0 to 10.4.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.4.0...v10.4.1)

---
updated-dependencies:
- dependency-name: "@cypress/grep"
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: eslint
  dependency-version: 10.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-02 13:57:48 +00:00
Jamie Curnow 0d832092a5 Only modify files in startup when they aren't mounted 2026-06-02 22:09:21 +10:00
Yassine Damiri 6629abed32 Use fs module for credential file operations
Refactor (from the review) credential file handling to use fs module for directory creation and file writing
2026-06-01 17:02:19 +02:00
jc21 52a7b7dca1 Merge pull request #5593 from NginxProxyManager/develop
v2.15.0
2026-06-01 08:28:34 +10:00
jc21 8e042ba79f Merge pull request #5535 from DaanSelen/develop
chore(deps): bump EuroDNS plugin and fix endpoint url
2026-06-01 07:42:49 +10:00
jc21 79a4128b78 Merge branch 'develop' into develop 2026-05-31 22:04:14 +10:00
Jamie Curnow fc1adb64a3 Fix regression of null domains in db creates 2026-05-29 07:32:58 +10:00
Jamie Curnow a6b7a7dd9d Only clean domain names on cert save if given 2026-05-28 18:24:13 +10:00
jc21 dc2c05aeb3 Merge pull request #5579 from NginxProxyManager/dependabot/npm_and_yarn/backend/qs-6.15.2
Bump qs from 6.15.0 to 6.15.2 in /backend
2026-05-27 11:32:57 +10:00
Jamie Curnow 0c15125830 Bump version 2026-05-27 11:30:25 +10:00
jc21 e48695401e Merge branch 'develop' into dependabot/npm_and_yarn/backend/qs-6.15.2 2026-05-27 11:08:07 +10:00
jc21 7211bf3500 Merge pull request #5557 from NginxProxyManager/dependabot/npm_and_yarn/backend/prod-minor-updates-033d9d3f2b
Bump the prod-minor-updates group across 1 directory with 2 updates
2026-05-27 11:07:50 +10:00
jc21 f7516304cb Merge pull request #5559 from NginxProxyManager/dependabot/npm_and_yarn/frontend/prod-patch-updates-6c65a517cd
Bump the prod-patch-updates group across 1 directory with 3 updates
2026-05-27 11:07:39 +10:00
jc21 a89410ddd7 Merge pull request #5560 from NginxProxyManager/dependabot/npm_and_yarn/frontend/prod-minor-updates-e3c98d2b5f
Bump date-fns from 4.1.0 to 4.3.0 in /frontend in the prod-minor-updates group across 1 directory
2026-05-27 11:07:24 +10:00
jc21 d08f1498c7 Merge pull request #5590 from NginxProxyManager/dependabot/npm_and_yarn/frontend/dev-patch-updates-0186d58a3a
Bump the dev-patch-updates group across 1 directory with 7 updates
2026-05-27 11:07:12 +10:00
dependabot[bot] e703a9adc4 Bump the prod-patch-updates group across 1 directory with 3 updates
Bumps the prod-patch-updates group with 3 updates in the /frontend directory: [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query), [react-intl](https://github.com/formatjs/formatjs) and [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom).


Updates `@tanstack/react-query` from 5.100.10 to 5.100.14
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.100.14/packages/react-query)

Updates `react-intl` from 10.1.6 to 10.1.9
- [Release notes](https://github.com/formatjs/formatjs/releases)
- [Commits](https://github.com/formatjs/formatjs/compare/react-intl@10.1.6...react-intl@10.1.9)

Updates `react-router-dom` from 7.15.0 to 7.15.1
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.15.1/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.100.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: react-intl
  dependency-version: 10.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: react-router-dom
  dependency-version: 7.15.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-27 00:04:39 +00:00
dependabot[bot] 5fd8a30508 Bump the dev-patch-updates group across 1 directory with 7 updates
Bumps the dev-patch-updates group with 7 updates in the /frontend directory:

| Package | From | To |
| --- | --- | --- |
| [@tanstack/react-query-devtools](https://github.com/TanStack/query/tree/HEAD/packages/react-query-devtools) | `5.100.10` | `5.100.14` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.14` | `19.2.15` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.1` | `6.0.2` |
| [postcss](https://github.com/postcss/postcss) | `8.5.14` | `8.5.15` |
| [tmp](https://github.com/raszi/node-tmp) | `0.2.5` | `0.2.6` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.12` | `8.0.14` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.6` | `4.1.7` |



Updates `@tanstack/react-query-devtools` from 5.100.10 to 5.100.14
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query-devtools/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query-devtools@5.100.14/packages/react-query-devtools)

Updates `@types/react` from 19.2.14 to 19.2.15
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@vitejs/plugin-react` from 6.0.1 to 6.0.2
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.2/packages/plugin-react)

Updates `postcss` from 8.5.14 to 8.5.15
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.14...8.5.15)

Updates `tmp` from 0.2.5 to 0.2.6
- [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/raszi/node-tmp/compare/v0.2.5...v0.2.6)

Updates `vite` from 8.0.12 to 8.0.14
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.14/packages/vite)

Updates `vitest` from 4.1.6 to 4.1.7
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.7/packages/vitest)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query-devtools"
  dependency-version: 5.100.14
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@types/react"
  dependency-version: 19.2.15
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: postcss
  dependency-version: 8.5.15
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: tmp
  dependency-version: 0.2.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: vite
  dependency-version: 8.0.14
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: vitest
  dependency-version: 4.1.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-27 00:04:14 +00:00
jc21 b7ca25a574 Merge pull request #5583 from NginxProxyManager/dependabot/npm_and_yarn/test/prod-patch-updates-9acc5e4454
Bump the prod-patch-updates group across 1 directory with 3 updates
2026-05-27 10:01:13 +10:00
jc21 063950cbb4 Merge pull request #5589 from NginxProxyManager/dependabot/npm_and_yarn/frontend/dev-minor-updates-98f8d9c27e
Bump the dev-minor-updates group across 1 directory with 4 updates
2026-05-27 10:01:03 +10:00
jc21 f7d72e7cc5 Merge pull request #5591 from NginxProxyManager/dependabot/npm_and_yarn/test/prod-minor-updates-5b42709b6f
Bump the prod-minor-updates group across 1 directory with 2 updates
2026-05-27 10:00:54 +10:00
dependabot[bot] f2e8ab5202 Bump the prod-minor-updates group across 1 directory with 2 updates
Bumps the prod-minor-updates group with 2 updates in the /test directory: [cypress](https://github.com/cypress-io/cypress) and [eslint](https://github.com/eslint/eslint).


Updates `cypress` from 15.15.0 to 15.16.0
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v15.15.0...v15.16.0)

Updates `eslint` from 10.3.0 to 10.4.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.3.0...v10.4.0)

---
updated-dependencies:
- dependency-name: cypress
  dependency-version: 15.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: eslint
  dependency-version: 10.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-26 22:54:40 +00:00
dependabot[bot] 6bb9a90103 Bump the prod-minor-updates group across 1 directory with 2 updates
Bumps the prod-minor-updates group with 2 updates in the /backend directory: [liquidjs](https://github.com/harttle/liquidjs) and [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg).


Updates `liquidjs` from 10.25.7 to 10.27.0
- [Release notes](https://github.com/harttle/liquidjs/releases)
- [Changelog](https://github.com/harttle/liquidjs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/harttle/liquidjs/compare/v10.25.7...v10.27.0)

Updates `pg` from 8.20.0 to 8.21.0
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/commits/pg@8.21.0/packages/pg)

---
updated-dependencies:
- dependency-name: liquidjs
  dependency-version: 10.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: pg
  dependency-version: 8.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-26 22:54:39 +00:00
dependabot[bot] 79a0deedf7 Bump date-fns
Bumps the prod-minor-updates group with 1 update in the /frontend directory: [date-fns](https://github.com/date-fns/date-fns).


Updates `date-fns` from 4.1.0 to 4.3.0
- [Release notes](https://github.com/date-fns/date-fns/releases)
- [Commits](https://github.com/date-fns/date-fns/compare/v4.1.0...v4.3.0)

---
updated-dependencies:
- dependency-name: date-fns
  dependency-version: 4.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-26 22:54:37 +00:00
dependabot[bot] a47d401d86 Bump qs from 6.15.0 to 6.15.2 in /backend
Bumps [qs](https://github.com/ljharb/qs) from 6.15.0 to 6.15.2.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.15.0...v6.15.2)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.15.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-26 22:53:22 +00:00
dependabot[bot] 33bf6d412d Bump the dev-minor-updates group across 1 directory with 4 updates
Bumps the dev-minor-updates group with 4 updates in the /frontend directory: [@formatjs/cli](https://github.com/formatjs/formatjs), [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [sass](https://github.com/sass/dart-sass) and [vite-plugin-checker](https://github.com/fi3ework/vite-plugin-checker).


Updates `@formatjs/cli` from 6.15.0 to 6.16.3
- [Release notes](https://github.com/formatjs/formatjs/releases)
- [Commits](https://github.com/formatjs/formatjs/compare/@formatjs/cli@6.15.0...@formatjs/cli@6.16.3)

Updates `@types/node` from 25.7.0 to 25.9.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `sass` from 1.99.0 to 1.100.0
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.99.0...1.100.0)

Updates `vite-plugin-checker` from 0.13.0 to 0.14.1
- [Release notes](https://github.com/fi3ework/vite-plugin-checker/releases)
- [Commits](https://github.com/fi3ework/vite-plugin-checker/compare/vite-plugin-checker@0.13.0...vite-plugin-checker@0.14.1)

---
updated-dependencies:
- dependency-name: "@formatjs/cli"
  dependency-version: 6.16.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: "@types/node"
  dependency-version: 25.9.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: sass
  dependency-version: 1.100.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: vite-plugin-checker
  dependency-version: 0.14.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-26 22:29:03 +00:00
dependabot[bot] c94cbb235b Bump the prod-patch-updates group across 1 directory with 3 updates
Bumps the prod-patch-updates group with 3 updates in the /test directory: [@quobix/vacuum](https://github.com/daveshanley/vacuum), [axios](https://github.com/axios/axios) and [mocha](https://github.com/mochajs/mocha).


Updates `@quobix/vacuum` from 0.26.4 to 0.26.8
- [Release notes](https://github.com/daveshanley/vacuum/releases)
- [Commits](https://github.com/daveshanley/vacuum/compare/v0.26.4...v0.26.8)

Updates `axios` from 1.16.0 to 1.16.1
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.16.0...v1.16.1)

Updates `mocha` from 11.7.5 to 11.7.6
- [Release notes](https://github.com/mochajs/mocha/releases)
- [Changelog](https://github.com/mochajs/mocha/blob/v11.7.6/CHANGELOG.md)
- [Commits](https://github.com/mochajs/mocha/compare/v11.7.5...v11.7.6)

---
updated-dependencies:
- dependency-name: "@quobix/vacuum"
  dependency-version: 0.26.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: axios
  dependency-version: 1.16.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: mocha
  dependency-version: 11.7.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-26 22:28:25 +00:00
Jamie Curnow 553a69a9ef Adds Certbot document to docs site 2026-05-27 07:54:23 +10:00
Jamie Curnow 7e9cc32f97 Fix cypress suite
- split out cypress tests for plugins to be isolated
- use updated testca image from proper location
2026-05-27 07:25:24 +10:00
Jamie Curnow 03c70e3902 Fixes for upgraded debian trixie
- Adds request logging in debug mode for some endpoints
- Moves certbot version determination to the startup scripts and removes
  bash script encapsulation when installing plugins
- Revert loose domain validation, which was there for a specific reason
  addressing CVE's
- Fix Cypress suite for cert generation
- Adds Cypress test that iterates over the entire certbot plugins list
  and installs each one, ensuring at the very least that the install
  works
- Fixed some plugins based on this
- (!) Still some work to do on this, hostinger is still broken at least
- Improved cypress tests for custom certs; they will generate on each
  run instead of being baked in. The baked ones were due to expire soon
2026-05-25 07:37:24 +10:00
Jamie Curnow c354238c35 Testing mkcert in test suite 2026-05-20 08:05:24 +10:00
Jamie Curnow dff978a63c Tests: install mkcert before generating 2026-05-20 07:52:46 +10:00
Jamie Curnow 11955e5dff Cypress improvements, move mkcert back to command line 2026-05-19 08:51:52 +10:00
Jamie Curnow f53bf88f4d Update cypress docker version, generate custom certs each time 2026-05-19 08:04:49 +10:00
Jamie Curnow ee1f7ba551 Fall back to error code 500 when not set in error object 2026-05-18 16:55:41 +10:00
Jamie Curnow 586dfd36a9 Fix openssl3 formatting of subject cn 2026-05-18 16:12:02 +10:00
Jamie Curnow 8b256c33e9 Sorted fr locale 2026-05-18 15:55:54 +10:00
Jamie Curnow 2bf9e9b213 Support different cert info output in new version of debian 2026-05-18 15:52:52 +10:00
Jamie Curnow 7330d0441c Loose validation on certificate domain names 2026-05-18 15:34:29 +10:00
Jamie Curnow 84886383a7 Fix certificates getting null domain names whgen no cn exists 2026-05-18 15:21:55 +10:00
Jamie Curnow 1e22574000 Fix nulls showing in certificate rows 2026-05-18 15:04:27 +10:00
jc21 32a74d9781 Merge pull request #5543 from Matthew-Kilpatrick/fix-deny-all-on-no-access-rules
fix: omit "Access rules" directives if no rules configured
2026-05-18 11:23:55 +10:00
jc21 aa66badcd8 Merge pull request #5546 from D0LBA3B/feature/proxy-host-delete-context
Adds host info to proxy host delete confirmation modal
2026-05-18 07:47:08 +10:00
jc21 840a720d09 Merge pull request #5550 from rfos/develop
Add Hostinger DNS plugin configuration
2026-05-18 07:44:59 +10:00
Reinaldo Ferro ef0ec0b188 Add Hostinger DNS plugin configuration 2026-05-17 22:16:48 +02:00
Daan Selen 387f8d9424 cherry-pick requested changes 2026-05-17 20:23:22 +02:00
DOLBAEB 6008a666dd Adds host info to proxy host delete confirmation modal 2026-05-16 14:18:48 +02:00
Matthew Kilpatrick 52c32b473d fix: omit "Access rules" directives if no rules configured
When an access list was associated with a template which had users (items) but no rules (clients), a `deny all` directive was inserted to the config. This resulted in all requests, including those with valid credentials, being rejected due to the lack of any `allow` directive.

This commit wraps the access rule configuration inside of an if block, so the `deny all;` directive is only present when at least one rule is configured.
2026-05-15 19:03:20 +00:00
jc21 43e0911ce4 Merge pull request #5352 from MarioGervais/patch-1
feat(i18n): complete French (fr) translation
2026-05-14 14:16:56 +10:00
jc21 0242187db0 Add message for available update in French locale 2026-05-14 14:16:00 +10:00
jc21 f6a9c5865b Merge pull request #5382 from roundone/docs/add-selfhostingsh-guide
docs: add selfhosting.sh guide to third-party list
2026-05-14 14:03:50 +10:00
jc21 ba2f714b55 Merge branch 'develop' into docs/add-selfhostingsh-guide 2026-05-14 14:03:27 +10:00
jc21 cd32dbdc4d Merge pull request #5403 from Mark0025/docs/add-npm-auth-gateway
docs: add NPM Auth Gateway to third-party integrations
2026-05-14 13:54:24 +10:00
jc21 ebcd2a209c Merge pull request #5415 from 12LuA/develop
Fix typo in debug mode comment
2026-05-14 11:43:08 +10:00
jc21 2a6d0d8b32 Merge pull request #5417 from fankes/develop
Docs: Fix PostgreSQL volume mapping to prevent data loss
2026-05-14 11:42:46 +10:00
jc21 661b1005ee Merge pull request #5419 from Stephan-P/patch-1
Update nl.json - completion and corrections to Dutch
2026-05-14 11:40:53 +10:00
jc21 d099b00463 Merge pull request #5421 from edklesel/access-list-clients-ordered-insert
Access list clients ordered insert
2026-05-14 11:39:32 +10:00
jc21 2da9e0330b Merge pull request #5427 from Arihant101/patch-1
Fix grammar and clarity issues in README.md
2026-05-14 11:37:09 +10:00
jc21 9875a7650d Merge pull request #5460 from barttran2k/contribai/fix/security/undefined-variable-next-referenced-in-ro
Security: Undefined Variable `next` Referenced in Route Error Handler
2026-05-14 11:11:12 +10:00
jc21 6694df668e Merge pull request #5480 from MichaelJanssenNesai/patch-1
Update dns plugin for deSec to 1.3.2
2026-05-14 10:52:14 +10:00
jc21 6aa629864f Merge pull request #5484 from jcarvajalantigua/patch-1
Add SECURITY.md for security policy and reporting
2026-05-14 10:51:43 +10:00
jc21 d3ba40e066 Merge pull request #5485 from butalex11/add-hosterby-dns-provider
Add hoster.by DNS provider
2026-05-14 10:51:00 +10:00
jc21 868d0b08ad Merge pull request #5489 from tomas-bara/bugfix/stream-enable-translation
Fix translation in streams table ignoring current enable/disable state
2026-05-14 10:33:54 +10:00
Jamie Curnow 31500c225d Update pull request template to include duplicate warning 2026-05-14 10:32:20 +10:00
jc21 13cfa340de Merge pull request #5508 from Zoey2936/fix-5441
Fix bug that allowed any authenticated user to modify their own roles field through the PUT
2026-05-14 10:23:31 +10:00
Jamie Curnow 673082ab5b Update pull request template to include AI 2026-05-14 10:03:03 +10:00
jc21 70538af311 Merge pull request #5520 from D0LBA3B/feature/sortable-tables
Adds column sorting to Nginx tables
2026-05-14 08:56:35 +10:00
jc21 a38fb8d311 Merge pull request #5530 from Ludo-code/new-dns-provider-RcodeZero
Add RcodeZero DNS plugin to dns-plugins.json for #5512
2026-05-14 08:36:37 +10:00
jc21 c8e6010ac3 Merge pull request #5531 from MBulli/patch-1
Added PUT and DELETE method to Access-Control-Allow-Methods field
2026-05-14 08:35:23 +10:00
Jamie Curnow 5fafeff7c0 Update pull request template to include api 2026-05-14 08:35:04 +10:00
Jamie Curnow edb56c285f Add pull request template, too many PR's don't have descriptions 2026-05-14 08:33:00 +10:00
jc21 ce55f8ebcd Merge pull request #5537 from NginxProxyManager/dependabot/npm_and_yarn/test/systeminformation-5.31.6
Bump systeminformation from 5.31.1 to 5.31.6 in /test
2026-05-14 08:26:09 +10:00
jc21 f4e37dff1f Merge pull request #5538 from NginxProxyManager/dependabot/npm_and_yarn/backend/minimatch-10.2.5
Bump minimatch from 10.2.2 to 10.2.5 in /backend
2026-05-14 08:25:54 +10:00
dependabot[bot] 982c16a86d Bump minimatch from 10.2.2 to 10.2.5 in /backend
Bumps [minimatch](https://github.com/isaacs/minimatch) from 10.2.2 to 10.2.5.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v10.2.2...v10.2.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 10.2.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-13 20:13:44 +00:00
dependabot[bot] 5dbfd63477 Bump systeminformation from 5.31.1 to 5.31.6 in /test
Bumps [systeminformation](https://github.com/sebhildebrandt/systeminformation) from 5.31.1 to 5.31.6.
- [Release notes](https://github.com/sebhildebrandt/systeminformation/releases)
- [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.31.1...v5.31.6)

---
updated-dependencies:
- dependency-name: systeminformation
  dependency-version: 5.31.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-13 16:04:32 +00:00
jc21 f2e3a3dd81 Merge pull request #5447 from NginxProxyManager/dependabot/npm_and_yarn/frontend/lodash-es-4.18.1
Bump lodash-es from 4.17.23 to 4.18.1 in /frontend
2026-05-13 20:26:28 +10:00
jc21 5af25c2d9c Merge pull request #5528 from NginxProxyManager/dependabot/npm_and_yarn/backend/fast-uri-3.1.2
Bump fast-uri from 3.1.0 to 3.1.2 in /backend
2026-05-13 20:26:20 +10:00
jc21 f8fca5bd19 Merge pull request #5529 from NginxProxyManager/dependabot/npm_and_yarn/test/fast-uri-3.1.2
Bump fast-uri from 3.1.0 to 3.1.2 in /test
2026-05-13 20:26:10 +10:00
jc21 0703c6589b Merge pull request #5429 from NginxProxyManager/dependabot/npm_and_yarn/frontend/yaml-1.10.3
Bump yaml from 1.10.2 to 1.10.3 in /frontend
2026-05-13 19:14:13 +10:00
jc21 b29bc1a66e Merge pull request #5430 from NginxProxyManager/dependabot/npm_and_yarn/backend/picomatch-2.3.2
Bump picomatch from 2.3.1 to 2.3.2 in /backend
2026-05-13 19:13:59 +10:00
jc21 9bb4476366 Merge pull request #5434 from NginxProxyManager/dependabot/npm_and_yarn/frontend/picomatch-4.0.4
Bump picomatch from 4.0.3 to 4.0.4 in /frontend
2026-05-13 19:13:32 +10:00
jc21 a68f993571 Merge pull request #5438 from NginxProxyManager/dependabot/npm_and_yarn/backend/path-to-regexp-8.4.0
Bump path-to-regexp from 8.3.0 to 8.4.2 in /backend
2026-05-13 19:13:23 +10:00
jc21 ea4604ffa4 Merge pull request #5533 from NginxProxyManager/dependabot/npm_and_yarn/docs/postcss-8.5.14
Bump postcss from 8.4.47 to 8.5.14 in /docs
2026-05-13 19:12:30 +10:00
jc21 9cb9512bfa Merge pull request #5534 from NginxProxyManager/dependabot/npm_and_yarn/test/axios-1.16.0
Bump axios from 1.13.6 to 1.16.0 in /test
2026-05-13 19:12:10 +10:00
dependabot[bot] 7bb34a7273 Bump axios from 1.13.6 to 1.16.0 in /test
Bumps [axios](https://github.com/axios/axios) from 1.13.6 to 1.16.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.6...v1.16.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.16.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-13 07:31:29 +00:00
jc21 6719cfc6a0 Merge pull request #5437 from Flop2691/develop
Updating french translation
2026-05-13 15:03:24 +10:00
jc21 b5a26574ec Merge pull request #5359 from eren-karakus0/fix/irish-locale-flag-and-label
fix(i18n): correct Irish locale flag and language label
2026-05-13 14:58:16 +10:00
jc21 a0fb4510f7 Merge pull request #5395 from kraineff/develop
Update ru.json
2026-05-13 14:57:13 +10:00
jc21 dc5e71f28c Merge pull request #5473 from NginxProxyManager/dependabot/npm_and_yarn/frontend/lodash-4.18.1
Bump lodash from 4.17.23 to 4.18.1 in /frontend
2026-05-13 14:48:50 +10:00
jc21 0173dd6b49 Merge pull request #5522 from NginxProxyManager/dependabot/npm_and_yarn/backend/ip-address-10.2.0
Bump ip-address from 10.1.0 to 10.2.0 in /backend
2026-05-13 14:39:28 +10:00
jc21 980097683f Merge pull request #5420 from NginxProxyManager/dependabot/npm_and_yarn/test/flatted-3.4.2
Bump flatted from 3.3.3 to 3.4.2 in /test
2026-05-13 14:36:15 +10:00
dependabot[bot] e49af3cc87 Bump postcss from 8.4.47 to 8.5.14 in /docs
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.47 to 8.5.14.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.47...8.5.14)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-13 04:12:48 +00:00
dependabot[bot] 2fe460557a Bump flatted from 3.3.3 to 3.4.2 in /test
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-13 03:49:31 +00:00
dependabot[bot] f06c53330a Bump yaml from 1.10.2 to 1.10.3 in /frontend
Bumps [yaml](https://github.com/eemeli/yaml) from 1.10.2 to 1.10.3.
- [Release notes](https://github.com/eemeli/yaml/releases)
- [Commits](https://github.com/eemeli/yaml/compare/v1.10.2...v1.10.3)

---
updated-dependencies:
- dependency-name: yaml
  dependency-version: 1.10.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-13 03:49:25 +00:00
dependabot[bot] 6ed6210a7b Bump picomatch from 2.3.1 to 2.3.2 in /backend
Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-13 03:49:18 +00:00
dependabot[bot] dad08083c6 Bump picomatch from 4.0.3 to 4.0.4 in /frontend
Bumps [picomatch](https://github.com/micromatch/picomatch) from 4.0.3 to 4.0.4.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-13 03:49:09 +00:00
dependabot[bot] 8b24059e3b Bump path-to-regexp from 8.3.0 to 8.4.2 in /backend
Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) from 8.3.0 to 8.4.2.
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v8.3.0...v8.4.2)

---
updated-dependencies:
- dependency-name: path-to-regexp
  dependency-version: 8.4.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-13 03:49:03 +00:00
dependabot[bot] 6537fb248e Bump lodash-es from 4.17.23 to 4.18.1 in /frontend
Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1)

---
updated-dependencies:
- dependency-name: lodash-es
  dependency-version: 4.18.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-13 03:48:35 +00:00
dependabot[bot] 738afde96e Bump lodash from 4.17.23 to 4.18.1 in /frontend
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-13 03:47:36 +00:00
dependabot[bot] dc84b4b94b Bump ip-address from 10.1.0 to 10.2.0 in /backend
Bumps [ip-address](https://github.com/beaugunderson/ip-address) from 10.1.0 to 10.2.0.
- [Commits](https://github.com/beaugunderson/ip-address/commits)

---
updated-dependencies:
- dependency-name: ip-address
  dependency-version: 10.2.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-13 03:47:17 +00:00
dependabot[bot] 41c32ebf0a Bump fast-uri from 3.1.0 to 3.1.2 in /backend
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.0 to 3.1.2.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.0...v3.1.2)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-13 03:47:12 +00:00
dependabot[bot] 04b43d072c Bump fast-uri from 3.1.0 to 3.1.2 in /test
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.0 to 3.1.2.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.0...v3.1.2)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-13 03:47:12 +00:00
Jamie Curnow 7d58c579fa Fix backend linting 2026-05-13 13:40:06 +10:00
Jamie Curnow c17475968f Fix frontend linting 2026-05-13 13:35:19 +10:00
Jamie Curnow 18b3404f46 Update all packages and fix related problems 2026-05-13 13:27:59 +10:00
Markus b66fd9d3ae Added PUT and DELETE method to Access-Control-Allow-Methods field 2026-05-12 09:24:28 +02:00
Ludovic c869d5a42b Add RcodeZero DNS plugin to dns-plugins.json 2026-05-12 01:56:05 +02:00
DOLBA3B ff066d5626 Adds column sorting to Nginx tables 2026-05-05 21:12:04 +02:00
Stephan Paternotte a6f8b7dd1d Update nl.json 2026-05-05 19:53:16 +02:00
Stephan Paternotte f7c2219680 Update nl.json
More improvements re. @TheMazeIsAmazing
2026-05-05 19:13:52 +02:00
Zoey 1ad78af5a0 fix #5441 2026-04-26 14:14:30 +02:00
Yassine DAMIRI 46bf3274d8 fix: Changed order of escape to prevent RCE 2026-04-20 01:00:43 +02:00
Tomáš Bára 26a2bcab42 Fix translation in streams table ignoring current enable/disable state 2026-04-16 09:13:10 +02:00
Alexander Gumi cea69e4ba5 Add hoster.by DNS provider 2026-04-14 11:05:03 +03:00
Jhonatan carvajal antigua 69d94b4a53 Add SECURITY.md for security policy and reporting
Added a security policy document outlining supported versions and vulnerability reporting procedures.
2026-04-14 00:03:10 -04:00
Stephan Paternotte 218e32bf15 Update nl.json
Hyphen inserted for "404-Host"
2026-04-13 17:24:39 +02:00
Stephan Paternotte a03c47932e Update nl.json
More updates after running the PR-version on live server.
Applied consistency with https://nl.wikipedia.org/wiki/Lijst_van_HTTP-statuscodes
2026-04-13 16:56:54 +02:00
Stephan Paternotte 871787ac95 Update nl.json
fixes for CRs by @TheMazeIsAmazing
Thanks. Well spotted!
2026-04-13 16:18:04 +02:00
MichaelJanssenNesai d161b944b1 Update dns plugin for deSec to 1.3.2
There a a couple of bug fixes (e.g. for DNS challenge with CNAME) for deSec.
2026-04-13 15:30:32 +02:00
Stephan Paternotte 82a326ea0b Update nl.json
Consistency in Dutch translation of 'enable' and 'disable'
2026-04-10 15:24:48 +02:00
Trần Bách 986c4cae6c fix(security): undefined variable next referenced in route erro
The GET /schema route handler's function signature is `(req, res)` (only two parameters), but the catch block calls `next(err)`. Since `next` is not defined in this scope, this will throw a `ReferenceError` at runtime when an error occurs, potentially crashing the process or causing an unhandled rejection instead of properly returning an error response.

Affected files: schema.js

Signed-off-by: Trần Bách <45133811+barttran2k@users.noreply.github.com>
2026-04-07 00:09:34 +07:00
Flop 65b905789f Updating french translation 2026-03-27 15:44:54 -04:00
Arihant Jain 7cfac7354d Fix grammar and clarity issues in README.md
Corrected minor grammatical errors and improved clarity in README.md.
2026-03-25 02:33:16 -04:00
Jamie Curnow 34374cbe09 Adds name.com certbot dns support
and sorts and organises the json file
2026-03-23 07:29:12 +10:00
Edward Klesel 1635e512c7 Change access list client insert to synchronous to ensure order preserved 2026-03-21 21:49:28 +00:00
Stephan Paternotte 40f39a7f19 Update nl.json - completion and corrections
Adding Ducth language translation for latest additions
Capitalisation
Infinitive verb form
2026-03-21 11:20:25 +01:00
fankesyooni 1b782997ef fix: postgres data volume path 2026-03-19 11:10:24 +08:00
12LuA cafb614b80 Fix typo in debug mode comment 2026-03-18 13:47:03 +01:00
Terry Git Cracken 9e4c92a066 docs: add NPM Auth Gateway to third-party integrations
Add NPM Auth Gateway — a companion app that adds user-level access
control with auto IP whitelisting on top of NPM's access list system.

NPM remains authoritative. The gateway only reads/writes through
NPM's REST API. If the gateway goes down, all existing IP whitelists
persist in NPM.

Running in production managing 90+ proxy hosts.
2026-03-14 20:08:49 -05:00
Alexey Krainev 10e33b39d7 Fix ru.json 2026-03-13 01:50:55 +05:00
Alexey Krainev d88f0b2618 Update ru.json 2026-03-13 01:19:22 +05:00
Nishant 4a9ad2baf7 docs: add selfhosting.sh guide to third-party list 2026-03-06 19:37:51 +05:30
Jamie Curnow 15896132ff Update all deps 2026-03-05 06:37:10 +10:00
Jamie Curnow 604b32ffbf Fix incorrect html description 2026-03-05 06:25:32 +10:00
Jamie Curnow eb67b3bfb6 Regenerate configs improvements
- Fix certificates
- Adds dry run
- code cleanup
2026-03-03 08:44:42 +10:00
jc21 f1e95c9c52 Merge pull request #5362 from NginxProxyManager/dependabot/npm_and_yarn/test/prod-patch-updates-bc9dbfe8b0
Bump the prod-patch-updates group in /test with 2 updates
2026-03-03 07:34:53 +10:00
jc21 d6e1d15996 Merge pull request #5363 from NginxProxyManager/dependabot/npm_and_yarn/test/prod-minor-updates-00a0b1e025
Bump the prod-minor-updates group in /test with 2 updates
2026-03-03 07:34:36 +10:00
jc21 99886f3111 Merge pull request #5364 from NginxProxyManager/dependabot/npm_and_yarn/backend/prod-minor-updates-55663a20b7
Bump the prod-minor-updates group in /backend with 3 updates
2026-03-03 07:34:26 +10:00
jc21 6b2d9a77a3 Merge pull request #5365 from NginxProxyManager/dependabot/npm_and_yarn/frontend/prod-patch-updates-cb5bb22b3e
Bump the prod-patch-updates group in /frontend with 2 updates
2026-03-03 07:34:16 +10:00
jc21 d37ab95ed8 Merge pull request #5366 from NginxProxyManager/dependabot/npm_and_yarn/frontend/prod-minor-updates-0ce8b7fd9e
Bump @tabler/icons-react from 3.37.1 to 3.38.0 in /frontend in the prod-minor-updates group
2026-03-03 07:34:07 +10:00
7heMech 18fc3fe770 Merge branch 'develop' into develop 2026-03-02 20:45:41 +02:00
dependabot[bot] e869518dd1 Bump @tabler/icons-react in /frontend in the prod-minor-updates group
Bumps the prod-minor-updates group in /frontend with 1 update: [@tabler/icons-react](https://github.com/tabler/tabler-icons/tree/HEAD/packages/icons-react).


Updates `@tabler/icons-react` from 3.37.1 to 3.38.0
- [Release notes](https://github.com/tabler/tabler-icons/releases)
- [Commits](https://github.com/tabler/tabler-icons/commits/v3.38.0/packages/icons-react)

---
updated-dependencies:
- dependency-name: "@tabler/icons-react"
  dependency-version: 3.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 13:45:33 +00:00
dependabot[bot] 37712ba2a4 Bump the prod-patch-updates group in /frontend with 2 updates
Bumps the prod-patch-updates group in /frontend with 2 updates: [country-flag-icons](https://gitlab.com/catamphetamine/country-flag-icons) and [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom).


Updates `country-flag-icons` from 1.6.14 to 1.6.15
- [Changelog](https://gitlab.com/catamphetamine/country-flag-icons/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/catamphetamine/country-flag-icons/compare/v1.6.14...v1.6.15)

Updates `react-router-dom` from 7.13.0 to 7.13.1
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.13.1/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: country-flag-icons
  dependency-version: 1.6.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: react-router-dom
  dependency-version: 7.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 13:45:09 +00:00
dependabot[bot] a714b41fd6 Bump the prod-minor-updates group in /backend with 3 updates
Bumps the prod-minor-updates group in /backend with 3 updates: [@apidevtools/json-schema-ref-parser](https://github.com/APIDevTools/json-schema-ref-parser), [mysql2](https://github.com/sidorares/node-mysql2) and [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg).


Updates `@apidevtools/json-schema-ref-parser` from 15.2.2 to 15.3.1
- [Release notes](https://github.com/APIDevTools/json-schema-ref-parser/releases)
- [Commits](https://github.com/APIDevTools/json-schema-ref-parser/compare/v15.2.2...v15.3.1)

Updates `mysql2` from 3.17.5 to 3.18.2
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.17.5...v3.18.2)

Updates `pg` from 8.18.0 to 8.19.0
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/commits/pg@8.19.0/packages/pg)

---
updated-dependencies:
- dependency-name: "@apidevtools/json-schema-ref-parser"
  dependency-version: 15.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: mysql2
  dependency-version: 3.18.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: pg
  dependency-version: 8.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 13:45:00 +00:00
dependabot[bot] b87bedc347 Bump the prod-minor-updates group in /test with 2 updates
Bumps the prod-minor-updates group in /test with 2 updates: [@quobix/vacuum](https://github.com/daveshanley/vacuum) and [cypress](https://github.com/cypress-io/cypress).


Updates `@quobix/vacuum` from 0.23.8 to 0.24.0
- [Release notes](https://github.com/daveshanley/vacuum/releases)
- [Commits](https://github.com/daveshanley/vacuum/compare/v0.23.8...v0.24.0)

Updates `cypress` from 15.10.0 to 15.11.0
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v15.10.0...v15.11.0)

---
updated-dependencies:
- dependency-name: "@quobix/vacuum"
  dependency-version: 0.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: cypress
  dependency-version: 15.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 13:44:34 +00:00
dependabot[bot] a62068275d Bump the prod-patch-updates group in /test with 2 updates
Bumps the prod-patch-updates group in /test with 2 updates: [axios](https://github.com/axios/axios) and [eslint](https://github.com/eslint/eslint).


Updates `axios` from 1.13.5 to 1.13.6
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.5...v1.13.6)

Updates `eslint` from 10.0.1 to 10.0.2
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.0.1...v10.0.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.13.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: eslint
  dependency-version: 10.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 13:44:15 +00:00
Muhammet Eren Karakuş cbcbd95812 test(i18n): add tests for getFlagCodeForLocale flag mapping
Cover standard locales, all special-case mappings (ja, zh, vi, ko, cs,
ga), and the undefined/fallback path to prevent future flag regressions.
2026-03-02 11:15:03 +03:00
Muhammet Eren Karakuş 278b1c92b3 fix(i18n): correct Irish locale flag and language label
- Fix locale key in lang-list.json from "locale-ie-GA" to "locale-ga-IE"
  to match the actual locale code used in IntlProvider
- Add ga -> ie mapping in getFlagCodeForLocale() so the Ireland flag is
  shown instead of the Gabon flag
- Add missing ["ga", "ga-IE"] entry to check-locales.cjs validation list

Fixes NginxProxyManager/nginx-proxy-manager#5354
2026-03-02 11:05:45 +03:00
jc21 c1d09eaceb Merge pull request #5353 from NginxProxyManager/dependabot/npm_and_yarn/docs/rollup-4.59.0
Bump rollup from 4.24.0 to 4.59.0 in /docs
2026-02-27 10:34:54 +10:00
jc21 9c509f30de Merge pull request #5355 from NginxProxyManager/dependabot/npm_and_yarn/frontend/rollup-4.59.0
Bump rollup from 4.57.1 to 4.59.0 in /frontend
2026-02-27 10:19:55 +10:00
dependabot[bot] c85b11ee33 Bump rollup from 4.57.1 to 4.59.0 in /frontend
Bumps [rollup](https://github.com/rollup/rollup) from 4.57.1 to 4.59.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.57.1...v4.59.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.59.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-26 22:11:06 +00:00
jc21 cd5ef390b9 Merge pull request #5350 from NginxProxyManager/dependabot/npm_and_yarn/backend/basic-ftp-5.2.0
Bump basic-ftp from 5.1.0 to 5.2.0 in /backend
2026-02-27 08:09:57 +10:00
dependabot[bot] d49cab1c0e Bump rollup from 4.24.0 to 4.59.0 in /docs
Bumps [rollup](https://github.com/rollup/rollup) from 4.24.0 to 4.59.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.24.0...v4.59.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.59.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-26 11:05:43 +00:00
MarioGervais dea7007802 feat(i18n): complete French (fr) translation
Add 4 missing certificate key-type strings, remove obsolete update-available key
2026-02-25 21:08:53 -05:00
MarioGervais 7e5f8d824b Update fr.json
feat(i18n): complete French translation - add missing certificate key-type strings
2026-02-25 20:55:27 -05:00
dependabot[bot] 33b1a993ec Bump basic-ftp from 5.1.0 to 5.2.0 in /backend
Bumps [basic-ftp](https://github.com/patrickjuchli/basic-ftp) from 5.1.0 to 5.2.0.
- [Release notes](https://github.com/patrickjuchli/basic-ftp/releases)
- [Changelog](https://github.com/patrickjuchli/basic-ftp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/patrickjuchli/basic-ftp/compare/v5.1.0...v5.2.0)

---
updated-dependencies:
- dependency-name: basic-ftp
  dependency-version: 5.2.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-25 23:29:28 +00:00
Jamie Curnow 67d40e186f Attempt to fix #5335 by allowing resovler generation to be opt-out with a env var 2026-02-26 08:32:02 +10:00
jc21 52be66c43e Merge pull request #5346 from siimaarmaa/develop
Added Estonia langugae support.
2026-02-25 08:38:17 +10:00
jc21 ec46cabcd4 Merge pull request #5334 from bill-mahoney/fix/atomic-ipv6-config-write
Fix silent nginx config corruption in 50-ipv6.sh
2026-02-25 08:35:11 +10:00
jc21 a7a9cc3acb Merge pull request #5337 from NginxProxyManager/dependabot/npm_and_yarn/test/prod-patch-updates-2e2637830d
Bump eslint from 10.0.0 to 10.0.1 in /test in the prod-patch-updates group
2026-02-25 08:31:03 +10:00
jc21 020b3ebb33 Merge pull request #5338 from NginxProxyManager/dependabot/npm_and_yarn/backend/dev-patch-updates-d8f01b0b39
Bump nodemon from 3.1.13 to 3.1.14 in /backend in the dev-patch-updates group
2026-02-25 08:30:51 +10:00
jc21 c1c4baf389 Merge pull request #5339 from NginxProxyManager/dependabot/npm_and_yarn/test/prod-minor-updates-22dc529b9a
Bump eslint-plugin-cypress from 6.0.0 to 6.1.0 in /test in the prod-minor-updates group
2026-02-25 08:30:40 +10:00
jc21 672b5d6dd9 Merge pull request #5341 from NginxProxyManager/dependabot/npm_and_yarn/backend/prod-patch-updates-07cfa309fd
Bump mysql2 from 3.17.3 to 3.17.5 in /backend in the prod-patch-updates group
2026-02-25 08:30:00 +10:00
jc21 cd230b5878 Merge pull request #5342 from NginxProxyManager/dependabot/npm_and_yarn/frontend/dev-minor-updates-a1de0f9639
Bump happy-dom from 20.6.3 to 20.7.0 in /frontend in the dev-minor-updates group
2026-02-25 08:29:49 +10:00
jc21 a8f35062af Merge pull request #5343 from NginxProxyManager/dependabot/npm_and_yarn/frontend/prod-patch-updates-bfb85ae48b
Bump country-flag-icons from 1.6.13 to 1.6.14 in /frontend in the prod-patch-updates group
2026-02-25 08:29:06 +10:00
Jamie Curnow da5955412d Command to regenerate nginx configs 2026-02-25 08:13:38 +10:00
siimaarmaa adb27fe67d Added Estonia langugae support. First Estonia lanuage update is HelpDocs. By Siim Aarmaa 2026-02-23 20:49:06 +02:00
dependabot[bot] d874af8692 Bump country-flag-icons in /frontend in the prod-patch-updates group
Bumps the prod-patch-updates group in /frontend with 1 update: [country-flag-icons](https://gitlab.com/catamphetamine/country-flag-icons).


Updates `country-flag-icons` from 1.6.13 to 1.6.14
- [Changelog](https://gitlab.com/catamphetamine/country-flag-icons/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/catamphetamine/country-flag-icons/compare/v1.6.13...v1.6.14)

---
updated-dependencies:
- dependency-name: country-flag-icons
  dependency-version: 1.6.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 13:51:21 +00:00
dependabot[bot] 0844dade98 Bump happy-dom in /frontend in the dev-minor-updates group
Bumps the dev-minor-updates group in /frontend with 1 update: [happy-dom](https://github.com/capricorn86/happy-dom).


Updates `happy-dom` from 20.6.3 to 20.7.0
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.6.3...v20.7.0)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-version: 20.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 13:50:56 +00:00
dependabot[bot] 71d59516e8 Bump mysql2 in /backend in the prod-patch-updates group
Bumps the prod-patch-updates group in /backend with 1 update: [mysql2](https://github.com/sidorares/node-mysql2).


Updates `mysql2` from 3.17.3 to 3.17.5
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.17.3...v3.17.5)

---
updated-dependencies:
- dependency-name: mysql2
  dependency-version: 3.17.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 13:50:35 +00:00
dependabot[bot] 06e220e184 Bump nodemon in /backend in the dev-patch-updates group
Bumps the dev-patch-updates group in /backend with 1 update: [nodemon](https://github.com/remy/nodemon).


Updates `nodemon` from 3.1.13 to 3.1.14
- [Release notes](https://github.com/remy/nodemon/releases)
- [Commits](https://github.com/remy/nodemon/compare/v3.1.13...v3.1.14)

---
updated-dependencies:
- dependency-name: nodemon
  dependency-version: 3.1.14
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 13:50:13 +00:00
dependabot[bot] dc53647e76 Bump eslint-plugin-cypress in /test in the prod-minor-updates group
Bumps the prod-minor-updates group in /test with 1 update: [eslint-plugin-cypress](https://github.com/cypress-io/eslint-plugin-cypress).


Updates `eslint-plugin-cypress` from 6.0.0 to 6.1.0
- [Release notes](https://github.com/cypress-io/eslint-plugin-cypress/releases)
- [Commits](https://github.com/cypress-io/eslint-plugin-cypress/compare/v6.0.0...v6.1.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-cypress
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 13:50:13 +00:00
dependabot[bot] 4c04e89483 Bump eslint in /test in the prod-patch-updates group
Bumps the prod-patch-updates group in /test with 1 update: [eslint](https://github.com/eslint/eslint).


Updates `eslint` from 10.0.0 to 10.0.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.0.0...v10.0.1)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 13:50:02 +00:00
William Mahoney 7241869a9e Fix silent config corruption in 50-ipv6.sh on NFS volumes
Replace unsafe `echo "$(sed ...)" > $FILE` with atomic temp-file write.

The current pattern reads a file with sed inside a command substitution,
then writes the result back via echo redirection. If sed reads an empty
or momentarily unreadable file (e.g., NFS transient issue during
container recreation by Watchtower or similar tools), it produces no
output. The echo then writes exactly 1 byte (a newline) to the config
file, silently destroying its contents.

The fix writes sed output to a temp file first, checks it's non-empty
with `[ -s ]`, then atomically replaces the original via `mv`. If sed
produces empty output, the original file is preserved and a warning is
logged to stderr.
2026-02-20 21:24:40 -07:00
jc21 94f6191a21 Merge pull request #5332 from NginxProxyManager/update-deps
Update deps
2026-02-20 11:54:46 +10:00
Jamie Curnow cac52dd0ff Update linked deps 2026-02-20 11:20:59 +10:00
dependabot[bot] 906f177960 Bump tar from 7.5.7 to 7.5.9 in /test
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.7 to 7.5.9.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.7...v7.5.9)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-20 11:20:59 +10:00
dependabot[bot] f52afced5d Bump systeminformation from 5.30.6 to 5.31.1 in /test
Bumps [systeminformation](https://github.com/sebhildebrandt/systeminformation) from 5.30.6 to 5.31.1.
- [Release notes](https://github.com/sebhildebrandt/systeminformation/releases)
- [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.30.6...v5.31.1)

---
updated-dependencies:
- dependency-name: systeminformation
  dependency-version: 5.31.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-20 11:20:59 +10:00
Jamie Curnow e8224ff0af Update all dependencies 2026-02-20 11:02:56 +10:00
jc21 a4fa83d0ce Merge pull request #5326 from NginxProxyManager/dependabot/npm_and_yarn/test/tar-7.5.9
Bump tar from 7.5.7 to 7.5.9 in /test
2026-02-20 10:53:03 +10:00
jc21 770716ebf8 Merge pull request #5327 from NginxProxyManager/dependabot/npm_and_yarn/test/systeminformation-5.31.1
Bump systeminformation from 5.30.6 to 5.31.1 in /test
2026-02-20 10:52:51 +10:00
dependabot[bot] f1067d3308 Bump systeminformation from 5.30.6 to 5.31.1 in /test
Bumps [systeminformation](https://github.com/sebhildebrandt/systeminformation) from 5.30.6 to 5.31.1.
- [Release notes](https://github.com/sebhildebrandt/systeminformation/releases)
- [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.30.6...v5.31.1)

---
updated-dependencies:
- dependency-name: systeminformation
  dependency-version: 5.31.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-18 22:37:39 +00:00
dependabot[bot] 85c1a935ea Bump tar from 7.5.7 to 7.5.9 in /test
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.7 to 7.5.9.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.7...v7.5.9)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-18 22:37:36 +00:00
Jamie Curnow 51ef7f3b86 Docs update, use package version instead of latest, refer to better mariadb image 2026-02-19 08:36:41 +10:00
jc21 846b94f7e8 Merge pull request #5324 from biodland/develop
chore: added Norwegian translation
2026-02-19 07:51:23 +10:00
Birger 19e24c7e7e Rename langNO import to langNo for consistency 2026-02-18 12:24:20 +01:00
Birger c1bc471dac chore: added Norwegian translation, added missing references. 2026-02-18 12:23:08 +01:00
Birger 608dc0b6bf chore: added Norwegian translation 2026-02-18 11:31:42 +01:00
Jamie Curnow 0dbf268f37 Fix #5284 for older sqlite3 configurations 2026-02-18 08:32:17 +10:00
jc21 84fb2729af Merge pull request #5318 from NginxProxyManager/develop
v2.14.0
2026-02-17 15:32:19 +10:00
jc21 c7437ddf8f Merge branch 'master' into develop 2026-02-17 15:02:29 +10:00
jc21 627f43c729 Merge pull request #5314 from NginxProxyManager/dependabot/npm_and_yarn/frontend/dev-minor-updates-d71d2fefd7
Bump the dev-minor-updates group in /frontend with 2 updates
2026-02-17 14:57:17 +10:00
jc21 fc4c5aac86 Merge pull request #5315 from NginxProxyManager/dependabot/npm_and_yarn/frontend/prod-patch-updates-95db6732c0
Bump the prod-patch-updates group in /frontend with 2 updates
2026-02-17 14:57:03 +10:00
jc21 aff390f35d Merge pull request #5317 from Tech-no-1/fix-custom-certificates
Fix uploading of custom certificates
2026-02-17 14:50:07 +10:00
Jamie Curnow 5f5a3870e4 Drop support for armv7 builds, bump version, update docs 2026-02-17 12:55:56 +10:00
Tech-no-1 40f363bd4f Fix uploading of custom certificates 2026-02-17 02:58:18 +01:00
dependabot[bot] 678fdd22c6 Bump the dev-minor-updates group in /frontend with 2 updates
Bumps the dev-minor-updates group in /frontend with 2 updates: [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) and [happy-dom](https://github.com/capricorn86/happy-dom).


Updates `@biomejs/biome` from 2.3.14 to 2.4.0
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.4.0/packages/@biomejs/biome)

Updates `happy-dom` from 20.5.3 to 20.6.1
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.5.3...v20.6.1)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: happy-dom
  dependency-version: 20.6.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-17 01:43:56 +00:00
dependabot[bot] 6c3cc83d66 Bump the prod-patch-updates group in /frontend with 2 updates
Bumps the prod-patch-updates group in /frontend with 2 updates: [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) and [country-flag-icons](https://gitlab.com/catamphetamine/country-flag-icons).


Updates `@tanstack/react-query` from 5.90.20 to 5.90.21
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.90.21/packages/react-query)

Updates `country-flag-icons` from 1.6.12 to 1.6.13
- [Changelog](https://gitlab.com/catamphetamine/country-flag-icons/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/catamphetamine/country-flag-icons/compare/v1.6.12...v1.6.13)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.90.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: country-flag-icons
  dependency-version: 1.6.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-17 01:43:37 +00:00
jc21 5916fd5bee Merge pull request #5313 from NginxProxyManager/dependabot/npm_and_yarn/backend/prod-minor-updates-4d12c0f7cc
Bump the prod-minor-updates group in /backend with 3 updates
2026-02-17 11:42:02 +10:00
jc21 f105673904 Merge pull request #5312 from NginxProxyManager/dependabot/npm_and_yarn/frontend/dev-patch-updates-38f6d3601d
Bump the dev-patch-updates group in /frontend with 3 updates
2026-02-17 11:41:44 +10:00
jc21 a37d0b88d6 Merge pull request #5308 from YTKme/ytkme/fix-sqlite-internal-error
Fix SQLite Internal Error
2026-02-17 11:41:31 +10:00
Jamie Curnow 43bc2a743e Add note to docs about retiring armv7 after June 2026 2026-02-17 11:38:17 +10:00
jc21 269545256a Merge pull request #5283 from broker-consulting/feat/add-czech-translation
Add Czech translation and related locale files
2026-02-17 11:06:40 +10:00
jc21 e5df45e9ef Merge pull request #5279 from dodog/develop
Update Slovak translation
2026-02-17 11:05:51 +10:00
dependabot[bot] 5601dd14fc Bump the prod-minor-updates group in /backend with 3 updates
Bumps the prod-minor-updates group in /backend with 3 updates: [ajv](https://github.com/ajv-validator/ajv), [mysql2](https://github.com/sidorares/node-mysql2) and [otplib](https://github.com/yeojz/otplib/tree/HEAD/packages/otplib).


Updates `ajv` from 8.17.1 to 8.18.0
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v8.17.1...v8.18.0)

Updates `mysql2` from 3.16.3 to 3.17.1
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.16.3...v3.17.1)

Updates `otplib` from 13.2.1 to 13.3.0
- [Release notes](https://github.com/yeojz/otplib/releases)
- [Changelog](https://github.com/yeojz/otplib/blob/main/release.config.json)
- [Commits](https://github.com/yeojz/otplib/commits/v13.3.0/packages/otplib)

---
updated-dependencies:
- dependency-name: ajv
  dependency-version: 8.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: mysql2
  dependency-version: 3.17.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: otplib
  dependency-version: 13.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 14:01:58 +00:00
dependabot[bot] 3e5655cfcd Bump the dev-patch-updates group in /frontend with 3 updates
Bumps the dev-patch-updates group in /frontend with 3 updates: [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react), [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) and [vite-tsconfig-paths](https://github.com/aleclarson/vite-tsconfig-paths).


Updates `@types/react` from 19.2.13 to 19.2.14
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@vitejs/plugin-react` from 5.1.3 to 5.1.4
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@5.1.4/packages/plugin-react)

Updates `vite-tsconfig-paths` from 6.1.0 to 6.1.1
- [Release notes](https://github.com/aleclarson/vite-tsconfig-paths/releases)
- [Commits](https://github.com/aleclarson/vite-tsconfig-paths/compare/v6.1.0...v6.1.1)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.2.14
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 5.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: vite-tsconfig-paths
  dependency-version: 6.1.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 14:01:52 +00:00
jc21 a90af83270 Merge pull request #5309 from NginxProxyManager/dependabot/npm_and_yarn/test/prod-patch-updates-d4d031af8e
Bump @quobix/vacuum from 0.23.5 to 0.23.8 in /test in the prod-patch-updates group across 1 directory
2026-02-16 11:57:14 +10:00
jc21 619a8e5acc Merge pull request #5310 from NginxProxyManager/dependabot/npm_and_yarn/test/prod-minor-updates-aef0194d28
Bump eslint-plugin-cypress from 5.2.1 to 5.3.0 in /test in the prod-minor-updates group across 1 directory
2026-02-16 11:57:05 +10:00
jc21 6dcdefb57e Merge pull request #5294 from NginxProxyManager/dependabot/npm_and_yarn/frontend/dev-patch-updates-cb582034f5
Bump the dev-patch-updates group in /frontend with 2 updates
2026-02-16 10:30:46 +10:00
jc21 787616010b Merge pull request #5289 from kiaxseventh/develop
Added ArvanCloud DNS plugin support via certbot-dns-arvan package
2026-02-16 10:30:34 +10:00
dependabot[bot] 5891c291d2 Bump eslint-plugin-cypress
Bumps the prod-minor-updates group with 1 update in the /test directory: [eslint-plugin-cypress](https://github.com/cypress-io/eslint-plugin-cypress).


Updates `eslint-plugin-cypress` from 5.2.1 to 5.3.0
- [Release notes](https://github.com/cypress-io/eslint-plugin-cypress/releases)
- [Commits](https://github.com/cypress-io/eslint-plugin-cypress/compare/v5.2.1...v5.3.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-cypress
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 00:29:59 +00:00
dependabot[bot] 41a2a41e67 Bump @quobix/vacuum
Bumps the prod-patch-updates group with 1 update in the /test directory: [@quobix/vacuum](https://github.com/daveshanley/vacuum).


Updates `@quobix/vacuum` from 0.23.5 to 0.23.8
- [Release notes](https://github.com/daveshanley/vacuum/releases)
- [Commits](https://github.com/daveshanley/vacuum/compare/v0.23.5...v0.23.8)

---
updated-dependencies:
- dependency-name: "@quobix/vacuum"
  dependency-version: 0.23.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 00:29:36 +00:00
jc21 379099d7ed Merge pull request #5292 from NginxProxyManager/dependabot/npm_and_yarn/test/prod-minor-updates-69046ce3b8
Bump cypress from 15.9.0 to 15.10.0 in /test in the prod-minor-updates group
2026-02-16 10:29:02 +10:00
jc21 dbeab93c02 Merge pull request #5293 from NginxProxyManager/dependabot/npm_and_yarn/test/eslint-10.0.0
Bump eslint from 9.39.2 to 10.0.0 in /test
2026-02-16 10:28:52 +10:00
jc21 010cb562a0 Merge pull request #5295 from NginxProxyManager/dependabot/npm_and_yarn/frontend/dev-minor-updates-24fcbaaf54
Bump vite-tsconfig-paths from 6.0.5 to 6.1.0 in /frontend in the dev-minor-updates group
2026-02-16 10:28:39 +10:00
jc21 7ff2fc1900 Merge pull request #5299 from NginxProxyManager/dependabot/npm_and_yarn/test/axios-1.13.5
Bump axios from 1.13.4 to 1.13.5 in /test
2026-02-16 10:28:26 +10:00
jc21 1c189a1888 Merge pull request #5300 from NginxProxyManager/dependabot/npm_and_yarn/test/jsonpath-1.2.1
Bump jsonpath from 1.1.1 to 1.2.1 in /test
2026-02-16 10:28:16 +10:00
jc21 f3c46487f6 Merge pull request #5303 from 7heMech/fix-2fa-logout
Add guardrail to fix disabling 2fa
2026-02-16 10:27:58 +10:00
jc21 fcca481d1b Merge pull request #5305 from NginxProxyManager/dependabot/npm_and_yarn/backend/qs-6.14.2
Bump qs from 6.14.1 to 6.14.2 in /backend
2026-02-16 10:27:28 +10:00
jc21 c59c237000 Merge pull request #5306 from NginxProxyManager/dependabot/npm_and_yarn/test/qs-6.14.2
Bump qs from 6.14.1 to 6.14.2 in /test
2026-02-16 10:27:17 +10:00
Yan Kuang a62b6de9f2 Update SQLite client configuration from sqlite3 to better-sqlite3 2026-02-14 23:53:43 -08:00
dependabot[bot] d92cc953e1 Bump qs from 6.14.1 to 6.14.2 in /test
Bumps [qs](https://github.com/ljharb/qs) from 6.14.1 to 6.14.2.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.1...v6.14.2)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-14 12:44:07 +00:00
dependabot[bot] 1b6412688b Bump qs from 6.14.1 to 6.14.2 in /backend
Bumps [qs](https://github.com/ljharb/qs) from 6.14.1 to 6.14.2.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.1...v6.14.2)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-14 12:44:05 +00:00
7heMech 1d14f72ba5 Add guardrail for disable 2fa 2026-02-14 06:28:59 +00:00
dependabot[bot] 099243aff7 Bump jsonpath from 1.1.1 to 1.2.1 in /test
Bumps [jsonpath](https://github.com/dchester/jsonpath) from 1.1.1 to 1.2.1.
- [Commits](https://github.com/dchester/jsonpath/commits/1.2.1)

---
updated-dependencies:
- dependency-name: jsonpath
  dependency-version: 1.2.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-12 15:31:26 +00:00
dependabot[bot] 5fe12f69ba Bump axios from 1.13.4 to 1.13.5 in /test
Bumps [axios](https://github.com/axios/axios) from 1.13.4 to 1.13.5.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.4...v1.13.5)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.13.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-11 20:22:09 +00:00
jc21 011191f645 Merge pull request #5260 from jerry-yuan/develop
Add trust_forwarded_proto option for SSL redirect handling in r…
2026-02-11 14:54:00 +10:00
jerry-yuan eeab425ea4 fix: unknown "trust_forwarded_proto" variable error when run with already created old virtual hosts 2026-02-10 10:53:17 +00:00
Jamie Curnow 13fbc53591 Fix bug when adding invalid custom certs 2026-02-10 14:54:33 +10:00
dependabot[bot] 3f2aec7b86 Bump vite-tsconfig-paths in /frontend in the dev-minor-updates group
Bumps the dev-minor-updates group in /frontend with 1 update: [vite-tsconfig-paths](https://github.com/aleclarson/vite-tsconfig-paths).


Updates `vite-tsconfig-paths` from 6.0.5 to 6.1.0
- [Release notes](https://github.com/aleclarson/vite-tsconfig-paths/releases)
- [Commits](https://github.com/aleclarson/vite-tsconfig-paths/compare/v6.0.5...v6.1.0)

---
updated-dependencies:
- dependency-name: vite-tsconfig-paths
  dependency-version: 6.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 14:29:32 +00:00
dependabot[bot] 09a3d65aa1 Bump the dev-patch-updates group in /frontend with 2 updates
Bumps the dev-patch-updates group in /frontend with 2 updates: [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) and [happy-dom](https://github.com/capricorn86/happy-dom).


Updates `@types/react` from 19.2.10 to 19.2.13
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `happy-dom` from 20.5.0 to 20.5.3
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.5.0...v20.5.3)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.2.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: happy-dom
  dependency-version: 20.5.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 14:29:16 +00:00
dependabot[bot] c910cf9512 Bump eslint from 9.39.2 to 10.0.0 in /test
Bumps [eslint](https://github.com/eslint/eslint) from 9.39.2 to 10.0.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.2...v10.0.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 14:29:11 +00:00
dependabot[bot] 304c51aae8 Bump cypress in /test in the prod-minor-updates group
Bumps the prod-minor-updates group in /test with 1 update: [cypress](https://github.com/cypress-io/cypress).


Updates `cypress` from 15.9.0 to 15.10.0
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v15.9.0...v15.10.0)

---
updated-dependencies:
- dependency-name: cypress
  dependency-version: 15.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 14:28:59 +00:00
kiaxseventh b552eb90ed Add ArvanCloud DNS support 2026-02-09 13:02:18 +03:30
Tomáš Novák b78ef9bcd3 Add Czech translation and related locale files 2026-02-06 17:02:47 +01:00
Jozef Gaal 7c67fafedf Update Slovak translation
Updated Slovak translations for 2FA and other features
2026-02-06 01:23:59 +01:00
7heMech e72dfc3bfd Add reuseport to improve performance and reduce packet drops 2026-02-04 12:14:10 +00:00
Jerry8块 b7402d47a0 Merge branch 'NginxProxyManager:develop' into develop 2026-02-03 15:10:13 +08:00
jerry-yuan 21f63e3db3 fix: delete advanced options from redir_host/dead_host/streams 2026-02-01 10:38:09 +00:00
Jerry 232b5b759a fix: make variable name meaningful 2026-02-01 00:16:17 +08:00
jerry-yuan 054742539f fix: Supplement Swagger documentation 2026-01-31 14:17:05 +00:00
jerry-yuan 2b6a617599 fix: reformat migration scripts 2026-01-31 13:28:53 +00:00
jerry-yuan 187d21a0d5 feat: add trust_forwarded_proto option for SSL redirect handling in reverse proxy scenarios
When Nginx is behind another proxy server (like CloudFlare or AWS ALB), the force-SSL
feature can cause redirect loops because Nginx sees the connection as plain HTTP
while SSL is already handled upstream. This adds a new boolean option to trust
the X-Forwarded-Proto header from upstream proxies.

Changes:
- Add `trust_forwarded_proto` column to proxy_host table (migration)
- Update model and API schema to support the new boolean field
- Modify force-ssl Nginx template to check X-Forwarded-Proto/X-Forwarded-Scheme
- Add map directives in nginx.conf to validate and sanitize forwarded headers
- Add advanced option toggle in frontend UI with i18n support (EN/ZH)
- Set proxy headers from validated map variables instead of $scheme

This allows administrators to control SSL redirect behavior when Nginx is deployed
behind a TLS-terminating proxy.
2026-01-31 13:11:47 +00:00
264 changed files with 14524 additions and 5973 deletions
+31
View File
@@ -0,0 +1,31 @@
<!-- WARNING: Any PR without a description will be closed. The title is not enough! -->
<!-- ANOTHER WARNING: Don't go creating a duplicate PR! Check that someone hasn't already created something that tackles your fix and if so, help them first -->
## Why
<!-- Provide a brief description of WHY you are making your changes -->
<!-- Consider if you are changing the API, then go in to detail why and/or if
you change will break API for existing users -->
## Type of Change
<!-- Mark the relevant options with an "x" -->
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation update
- [ ] Code refactoring
- [ ] API changes
- [ ] Performance improvement
- [ ] Test addition or update
## AI Usage
<!-- Mark the relevant options with an "x" -->
- [ ] AI was used to write this
- [ ] AI was used to review this
+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
- uses: actions/stale@v11
with:
stale-issue-label: 'stale'
stale-pr-label: 'stale'
+2
View File
@@ -8,3 +8,5 @@ test/node_modules
*/node_modules
docker/dev/dnsrouter-config.json.tmp
docker/dev/resolv.conf
.claude
+1 -1
View File
@@ -1 +1 @@
2.13.7
2.16.0
+19 -16
View File
@@ -1,7 +1,7 @@
<p align="center">
<img src="https://nginxproxymanager.com/github.png">
<br><br>
<img src="https://img.shields.io/badge/version-2.13.7-green.svg?style=for-the-badge">
<img src="https://img.shields.io/badge/version-2.16.0-green.svg?style=for-the-badge">
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager">
<img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge">
</a>
@@ -10,7 +10,7 @@
</a>
</p>
This project comes as a pre-built docker image that enables you to easily forward to your websites
This project comes as a pre-built Docker image that enables you to easily forward to your websites
running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt.
- [Quick Setup](#quick-setup)
@@ -20,9 +20,9 @@ running at home or otherwise, including free SSL, without having to know too muc
## Project Goal
I created this project to fill a personal need to provide users with an easy way to accomplish reverse
proxying hosts with SSL termination and it had to be so easy that a monkey could do it. This goal hasn't changed.
While there might be advanced options they are optional and the project should be as simple as possible
so that the barrier for entry here is low.
proxying hosts with SSL termination, and it had to be so easy that a monkey could do it. This goal hasn't changed.
While there might be advanced options, they are optional, and the project should be as simple as possible
so that the barrier to entry here is low.
<a href="https://www.buymeacoffee.com/jc21" target="_blank"><img src="http://public.jc21.com/github/by-me-a-coffee.png" alt="Buy Me A Coffee" style="height: 51px !important;width: 217px !important;" ></a>
@@ -30,29 +30,32 @@ so that the barrier for entry here is low.
## Features
- Beautiful and Secure Admin Interface based on [Tabler](https://tabler.github.io/)
- Easily create forwarding domains, redirections, streams and 404 hosts without knowing anything about Nginx
- Easily create forwarding domains, redirections, streams, and 404 hosts without knowing anything about Nginx
- Free SSL using Let's Encrypt or provide your own custom SSL certificates
- Access Lists and basic HTTP Authentication for your hosts
- Advanced Nginx configuration available for super users
- User management, permissions and audit log
- User management, permissions, and audit log
::: warning
`armv7` is no longer supported in version 2.14+. This is due to Nodejs dropping support for armhf. Please
use the `2.13.7` image tag if this applies to you.
:::
## Hosting your home network
I won't go in to too much detail here but here are the basics for someone new to this self-hosted world.
I won't go into too much detail here, but here are the basics for someone new to this self-hosted world.
1. Your home router will have a Port Forwarding section somewhere. Log in and find it
2. Add port forwarding for port 80 and 443 to the server hosting this project
3. Configure your domain name details to point to your home, either with a static ip or a service like DuckDNS or [Amazon Route53](https://github.com/jc21/route53-ddns)
4. Use the Nginx Proxy Manager as your gateway to forward to your other web based services
2. Add port forwarding for ports 80 and 443 to the server hosting this project
3. Configure your domain name details to point to your home, either with a static ip or a service like
- DuckDNS
- [Amazon Route53](https://github.com/jc21/route53-ddns)
- [Cloudflare](https://github.com/jc21/cloudflare-ddns)
4. Use the Nginx Proxy Manager as your gateway to forward to your other web-based services
## Quick Setup
1. Install Docker and Docker-Compose
- [Docker Install documentation](https://docs.docker.com/install/)
- [Docker-Compose Install documentation](https://docs.docker.com/compose/install/)
1. [Install Docker](https://docs.docker.com/install/)
2. Create a docker-compose.yml file similar to this:
```yml
+30
View File
@@ -0,0 +1,30 @@
# Security Policy
## Supported Versions
Only the latest stable release receives security updates.
Older versions are not actively maintained.
| Version | Supported |
| ------- | --------- |
| 2.16.x (latest) | :white_check_mark: |
| < 2.16.0 | :x: |
Docker images: `jc21/nginx-proxy-manager:latest`, `jc21/nginx-proxy-manager:2`
See all releases: https://github.com/NginxProxyManager/nginx-proxy-manager/releases
## Reporting a Vulnerability
**Do NOT open a public GitHub Issue to report a security vulnerability.**
Use GitHub's private vulnerability reporting:
https://github.com/NginxProxyManager/nginx-proxy-manager/security/advisories/new
Please include:
- Affected version (Docker image tag or release)
- Description of the vulnerability
- Steps to reproduce
- Potential impact
Once a fix is available, a public GitHub Security Advisory will be published.
+1 -1
View File
@@ -62,7 +62,7 @@ app.use("/", mainRoutes);
app.use((err, req, res, _) => {
const payload = {
error: {
code: err.status,
code: err.status || 500,
message: err.public ? err.message : "Internal Error",
},
};
+7 -24
View File
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
"$schema": "https://biomejs.dev/schemas/2.5.10/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
@@ -7,13 +7,7 @@
},
"files": {
"ignoreUnknown": false,
"includes": [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx",
"!**/dist/**/*"
]
"includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "!**/dist/**/*"]
},
"formatter": {
"enabled": true,
@@ -31,24 +25,13 @@
"groups": [
":BUN:",
":NODE:",
[
"npm:*",
"npm:*/**"
],
["npm:*", "npm:*/**"],
":PACKAGE_WITH_PROTOCOL:",
":URL:",
":PACKAGE:",
[
"/src/*",
"/src/**"
],
[
"/**"
],
[
"#*",
"#*/**"
],
["/src/*", "/src/**"],
["/**"],
["#*", "#*/**"],
":PATH:"
]
}
@@ -59,7 +42,7 @@
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"preset": "recommended",
"correctness": {
"useUniqueElementIds": "off"
},
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -2,7 +2,7 @@
"database": {
"engine": "knex-native",
"knex": {
"client": "sqlite3",
"client": "better-sqlite3",
"connection": {
"filename": "/app/config/mydb.sqlite"
},
+2 -2
View File
@@ -24,7 +24,7 @@ const generateDbConfig = () => {
password: cfg.password,
database: cfg.name,
port: cfg.port,
...(cfg.ssl ? { ssl: cfg.ssl } : {})
...(cfg.ssl ? { ssl: cfg.ssl } : {}),
},
migrations: {
tableName: "migrations",
@@ -37,6 +37,6 @@ const getInstance = () => {
instance = knex(generateDbConfig());
}
return instance;
}
};
export default getInstance;
+4 -5
View File
@@ -163,6 +163,9 @@ const internal2fa = {
const result = await verify({
token: code,
secret: auth.meta.totp_secret,
guardrails: createGuardrails({
MIN_SECRET_BYTES: 10,
}),
});
if (!result.valid) {
@@ -285,11 +288,7 @@ const internal2fa = {
},
getUserPasswordAuth: async (userId) => {
const auth = await authModel
.query()
.where("user_id", userId)
.andWhere("type", "password")
.first();
const auth = await authModel.query().where("user_id", userId).andWhere("type", "password").first();
if (!auth) {
throw new errs.ItemNotFoundError("Auth not found");
+110 -63
View File
@@ -2,7 +2,9 @@ import fs from "node:fs";
import batchflow from "batchflow";
import _ from "lodash";
import errs from "../lib/error.js";
import { isMysql, isPostgres } from "../lib/config.js";
import utils from "../lib/utils.js";
import db from "../db.js";
import { access as logger } from "../logger.js";
import accessListModel from "../models/access_list.js";
import accessListAuthModel from "../models/access_list_auth.js";
@@ -15,6 +17,38 @@ const omissions = () => {
return ["is_deleted"];
};
/**
* Find proxy hosts that reference an access list in their locations JSON.
*
* @param {Integer} accessListId
* @returns {Promise<Array>}
*/
const getProxyHostsUsingAccessListInLocations = async (accessListId) => {
let result;
if (isMysql()) {
const searchObj = JSON.stringify([{ access_list_id: accessListId }]);
result = await db().raw(
"SELECT id FROM proxy_host WHERE is_deleted = 0 AND JSON_CONTAINS(locations, ?, ?)",
[searchObj, "$"],
);
} else if (isPostgres()) {
result = await db().raw(
"SELECT id FROM proxy_host WHERE is_deleted = 0 AND locations::jsonb @> ?::jsonb",
[JSON.stringify([{ access_list_id: accessListId }])],
);
} else {
result = await db().raw(
"SELECT id FROM proxy_host WHERE is_deleted = 0 AND locations LIKE ?",
[`%"access_list_id":${accessListId}%`],
);
}
// knex raw() returns [rows, metadata] for MySQL, { rows } for Postgres and rows for SQLite
if (!Array.isArray(result)) {
return result?.rows || [];
}
return (Array.isArray(result[0]) ? result[0] : result) || [];
};
const internalAccessList = {
/**
* @param {Access} access
@@ -48,19 +82,16 @@ const internalAccessList = {
return true;
});
await Promise.all(promises);
// Clients
data.clients?.map((client) => {
promises.push(
accessListClientModel.query().insert({
for (const client of data.clients ?? []) {
await accessListClientModel.query().insert({
access_list_id: row.id,
address: client.address,
directive: client.directive,
}),
);
return true;
});
await Promise.all(promises);
}
// re-fetch with expansions
const freshRow = await internalAccessList.get(
@@ -69,7 +100,7 @@ const internalAccessList = {
id: data.id,
expand: ["owner", "items", "clients", "proxy_hosts.access_list.[clients,items]"],
},
true // skip masking
true, // skip masking
);
// Audit log
@@ -154,25 +185,17 @@ const internalAccessList = {
// Check for clients and add/update/remove them
if (typeof data.clients !== "undefined" && data.clients) {
const clientPromises = [];
data.clients.map((client) => {
const query = accessListClientModel.query().delete().where("access_list_id", data.id);
await query;
for (const client of data.clients) {
if (client.address) {
clientPromises.push(
accessListClientModel.query().insert({
await accessListClientModel.query().insert({
access_list_id: data.id,
address: client.address,
directive: client.directive,
}),
);
}
return true;
});
const query = accessListClientModel.query().delete().where("access_list_id", data.id);
await query;
// Add new clitens
if (clientPromises.length) {
await Promise.all(clientPromises);
}
}
}
@@ -191,13 +214,31 @@ const internalAccessList = {
id: data.id,
expand: ["owner", "items", "clients", "proxy_hosts.[certificate,access_list.[clients,items]]"],
},
true // skip masking
true, // skip masking
);
await internalAccessList.build(freshRow)
await internalAccessList.build(freshRow);
if (Number.parseInt(freshRow.proxy_host_count, 10)) {
await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts);
}
// Also regenerate configs for proxy hosts that reference this access list in their locations
const locationHostRows = await getProxyHostsUsingAccessListInLocations(data.id);
if (locationHostRows?.length) {
const locationHostIds = locationHostRows.map((r) => r.id).filter((id) => {
// Exclude hosts already regenerated above
return !freshRow.proxy_hosts?.find((h) => h.id === id);
});
if (locationHostIds.length) {
const locationHosts = await proxyHostModel.query()
.where("is_deleted", 0)
.whereIn("id", locationHostIds)
.allowGraph(proxyHostModel.defaultAllowGraph)
.withGraphFetched("[owner, certificate, access_list.[clients,items]]");
await internalNginx.bulkGenerateConfigs("proxy_host", locationHosts);
}
}
await internalNginx.reload();
return internalAccessList.maskItems(freshRow);
},
@@ -213,17 +254,13 @@ const internalAccessList = {
*/
get: async (access, data, skipMasking) => {
const thisData = data || {};
const accessData = await access.can("access_lists:get", thisData.id)
const accessData = await access.can("access_lists:get", thisData.id);
const query = accessListModel
.query()
.select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count"))
.leftJoin("proxy_host", function () {
this.on("proxy_host.access_list_id", "=", "access_list.id").andOn(
"proxy_host.is_deleted",
"=",
0,
);
this.on("proxy_host.access_list_id", "=", "access_list.id").andOn("proxy_host.is_deleted", "=", 0);
})
.where("access_list.is_deleted", 0)
.andWhere("access_list.id", thisData.id)
@@ -241,7 +278,7 @@ const internalAccessList = {
let row = await query.then(utils.omitRow(omissions()));
if (!row || !row.id) {
if (!row?.id) {
throw new errs.ItemNotFoundError(thisData.id);
}
if (!skipMasking && typeof row.items !== "undefined" && row.items) {
@@ -268,7 +305,7 @@ const internalAccessList = {
expand: ["proxy_hosts", "items", "clients"],
});
if (!row || !row.id) {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
@@ -278,28 +315,41 @@ const internalAccessList = {
// 4. audit log
// 1. update row to be deleted
await accessListModel
.query()
.where("id", row.id)
.patch({
await accessListModel.query().where("id", row.id).patch({
is_deleted: 1,
});
// 2. update any proxy hosts that were using it (ignoring permissions)
if (row.proxy_hosts) {
await proxyHostModel
.query()
.where("access_list_id", "=", row.id)
.patch({ access_list_id: 0 });
const affectedHostIds = new Set((row.proxy_hosts || []).map((h) => h.id));
if (affectedHostIds.size) {
await proxyHostModel.query().where("access_list_id", "=", row.id).patch({ access_list_id: 0 });
}
// 3. reconfigure those hosts, then reload nginx
// set the access_list_id to zero for these items
row.proxy_hosts.map((_val, idx) => {
row.proxy_hosts[idx].access_list_id = 0;
return true;
// Also clear it from any proxy host locations using it, these will then inherit the host's access list
const locationHostRows = await getProxyHostsUsingAccessListInLocations(row.id);
for (const { id: hostId } of locationHostRows) {
const host = await proxyHostModel.query().where("id", hostId).first();
if (host?.locations?.some((loc) => loc.access_list_id === row.id)) {
const updatedLocations = host.locations.map((loc) => {
if (loc.access_list_id === row.id) {
return { ...loc, access_list_id: 0 };
}
return loc;
});
await proxyHostModel.query().where("id", hostId).patch({ locations: updatedLocations });
affectedHostIds.add(hostId);
}
}
await internalNginx.bulkGenerateConfigs("proxy_host", row.proxy_hosts);
// 3. reconfigure those hosts from fresh rows, then reload nginx
if (affectedHostIds.size) {
const affectedHosts = await proxyHostModel
.query()
.where("is_deleted", 0)
.whereIn("id", [...affectedHostIds])
.allowGraph(proxyHostModel.defaultAllowGraph)
.withGraphFetched("[owner, certificate, access_list.[clients,items]]");
await internalNginx.bulkGenerateConfigs("proxy_host", affectedHosts);
}
await internalNginx.reload();
@@ -336,11 +386,7 @@ const internalAccessList = {
.query()
.select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count"))
.leftJoin("proxy_host", function () {
this.on("proxy_host.access_list_id", "=", "access_list.id").andOn(
"proxy_host.is_deleted",
"=",
0,
);
this.on("proxy_host.access_list_id", "=", "access_list.id").andOn("proxy_host.is_deleted", "=", 0);
})
.where("access_list.is_deleted", 0)
.groupBy("access_list.id")
@@ -382,10 +428,7 @@ const internalAccessList = {
* @returns {Promise}
*/
getCount: async (userId, visibility) => {
const query = accessListModel
.query()
.count("id as count")
.where("is_deleted", 0);
const query = accessListModel.query().count("id as count").where("is_deleted", 0);
if (visibility !== "all") {
query.andWhere("owner_user_id", userId);
@@ -447,20 +490,24 @@ const internalAccessList = {
}
// 2. create empty access file
fs.writeFileSync(htpasswdFile, '', {encoding: 'utf8'});
fs.writeFileSync(htpasswdFile, "", { encoding: "utf8" });
// 3. generate password for each user
if (list.items.length) {
await new Promise((resolve, reject) => {
batchflow(list.items).sequential()
batchflow(list.items)
.sequential()
.each((_i, item, next) => {
if (item.password?.length) {
logger.info(`Adding: ${item.username}`);
utils.execFile('openssl', ['passwd', '-apr1', item.password])
utils
.execFile("openssl", ["passwd", "-apr1", item.password])
.then((res) => {
try {
fs.appendFileSync(htpasswdFile, `${item.username}:${res}\n`, {encoding: 'utf8'});
fs.appendFileSync(htpasswdFile, `${item.username}:${res}\n`, {
encoding: "utf8",
});
} catch (err) {
reject(err);
}
@@ -482,7 +529,7 @@ const internalAccessList = {
});
});
}
}
}
},
};
export default internalAccessList;
+1 -6
View File
@@ -3,7 +3,6 @@ import { castJsonIfNeed } from "../lib/helpers.js";
import auditLogModel from "../models/audit-log.js";
const internalAuditLog = {
/**
* All logs
*
@@ -46,11 +45,7 @@ const internalAuditLog = {
get: async (access, data) => {
await access.can("auditlog:list");
const query = auditLogModel
.query()
.andWhere("id", data.id)
.allowGraph("[user]")
.first();
const query = auditLogModel.query().andWhere("id", data.id).allowGraph("[user]").first();
if (typeof data.expand !== "undefined" && data.expand !== null) {
query.withGraphFetched(`[${data.expand.join(", ")}]`);
+75 -14
View File
@@ -1,7 +1,7 @@
import fs from "node:fs";
import https from "node:https";
import path from "path";
import archiver from "archiver";
import { ZipArchive } from "archiver";
import _ from "lodash";
import moment from "moment";
import { ProxyAgent } from "proxy-agent";
@@ -66,7 +66,7 @@ const internalCertificate = {
.andWhere("provider", "letsencrypt")
.andWhere("expires_on", "<", expirationThreshold)
.then((certificates) => {
if (!certificates || !certificates.length) {
if (!certificates?.length) {
return null;
}
@@ -143,7 +143,7 @@ const internalCertificate = {
await internalCertificate.disableInUseHosts(inUseResult);
const user = await userModel.query().where("is_deleted", 0).andWhere("id", data.owner_user_id).first();
if (!user || !user.email) {
if (!user?.email) {
throw new error.ValidationError(
"A valid email address must be set on your user account to use Let's Encrypt",
);
@@ -305,7 +305,7 @@ const internalCertificate = {
}
const row = await query.then(utils.omitRow(omissions()));
if (!row || !row.id) {
if (!row?.id) {
throw new error.ItemNotFoundError(data.id);
}
// Custom omissions
@@ -370,7 +370,7 @@ const internalCertificate = {
* @returns {Promise}
*/
zipFiles: async (source, out) => {
const archive = archiver("zip", { zlib: { level: 9 } });
const archive = new ZipArchive({ zlib: { level: 9 } });
const stream = fs.createWriteStream(out);
return new Promise((resolve, reject) => {
@@ -397,7 +397,7 @@ const internalCertificate = {
await access.can("certificates:delete", data.id);
const row = await internalCertificate.get(access, { id: data.id });
if (!row || !row.id) {
if (!row?.id) {
throw new error.ItemNotFoundError(data.id);
}
@@ -614,7 +614,7 @@ const internalCertificate = {
const certificate = await internalCertificate.update(access, {
id: data.id,
expires_on: moment(validations.certificate.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"),
domain_names: [validations.certificate.cn],
domain_names: validations.certificate.cn ? [validations.certificate.cn] : [],
meta: _.clone(row.meta), // Prevent the update method from changing this value that we'll use later
});
@@ -630,7 +630,7 @@ const internalCertificate = {
* @param {String} privateKey This is the entire key contents as a string
*/
checkPrivateKey: async (privateKey) => {
const filepath = await tempWrite(privateKey, "/tmp");
const filepath = await tempWrite(privateKey);
const failTimeout = setTimeout(() => {
throw new error.ValidationError(
"Result Validation Error: Validation timed out. This could be due to the key being passphrase-protected.",
@@ -660,8 +660,8 @@ const internalCertificate = {
* @param {Boolean} [throwExpired] Throw when the certificate is out of date
*/
getCertificateInfo: async (certificate, throwExpired) => {
const filepath = await tempWrite(certificate);
try {
const filepath = await tempWrite(certificate, "/tmp");
const certData = await internalCertificate.getCertificateInfoFromFile(filepath, throwExpired);
fs.unlinkSync(filepath);
return certData;
@@ -683,13 +683,15 @@ const internalCertificate = {
try {
const result = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-subject", "-noout"]);
// Examples:
// subject=CN = *.jc21.com
// subject=CN = something.example.com
const regex = /(?:subject=)?[^=]+=\s+(\S+)/gim;
// subject=CN=*.jc21.com
const regex = /(?:subject=)?[^=]+=\s*(\S+)/gim;
const match = regex.exec(result);
if (match && typeof match[1] !== "undefined") {
certData.cn = match[1];
certData.cn = match[1].trim();
}
const result2 = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-issuer", "-noout"]);
@@ -779,6 +781,7 @@ const internalCertificate = {
const args = [
"certonly",
"-n", // non-interactive
"--config",
letsencryptConfig,
"--work-dir",
@@ -834,6 +837,7 @@ const internalCertificate = {
const args = [
"certonly",
"-n", // non-interactive
"--config",
letsencryptConfig,
"--work-dir",
@@ -877,10 +881,20 @@ const internalCertificate = {
const result = await utils.execFile(certbotCommand, args, adds.opts);
logger.info(result);
return result;
} catch (err) {
// Don't fail if file does not exist, so no need for action in the callback
} finally {
// Remove the credentials file whether certbot succeeded or failed.
//
// This cleanup used to sit in a catch block, so it only ran when issuance FAILED.
// A certificate that issued successfully left its DNS provider API credentials in
// /etc/letsencrypt/credentials for the entire life of that certificate. Nothing
// reads the file between certbot runs, so there is no reason to keep it:
// renewLetsEncryptSslWithDnsChallenge() writes it again immediately before each
// renewal.
//
// unlink is fire-and-forget with an empty callback. If the file is already gone
// that is the end state we wanted anyway, and a missing file must never turn a
// successful issuance into a failure.
fs.unlink(credentialsLocation, () => {});
throw err;
}
},
@@ -977,6 +991,43 @@ const internalCertificate = {
`Renewing LetsEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(", ")}`,
);
// certbot reads the DNS credentials back from the path recorded in the renewal config
// it wrote at issuance time, for example:
//
// authenticator = dns-cloudflare
// dns_cloudflare_credentials = /etc/letsencrypt/credentials/credentials-27
//
// so the file has to be present for the duration of this run. Write it here and remove
// it again below rather than leaving it on disk between renewals.
//
// Leaving it is an avoidable exposure. Anything running as root - a compromised
// process, a script, malware - can read the token and use it to issue valid Let's
// Encrypt certificates for the domain. Those certificates are genuinely trusted, so
// traffic presented with them passes TLS inspection, IDS/IPS and DLP that would
// otherwise flag it, and an exfiltration path built on them looks like ordinary
// HTTPS. The exposure window should be one certbot run, not the life of the
// certificate.
//
// The value is not on the certificate object we were handed: renew() sources that from
// internalCertificate.get(), which pipes the row through utils.omitRow(omissions()) so
// meta.dns_provider_credentials can never travel out over the API. Read the row from
// the model directly to get at it.
const row = await certificateModel.query().where("id", certificate.id).first();
const credentials = row?.meta?.dns_provider_credentials;
const credentialsLocation = `/etc/letsencrypt/credentials/credentials-${certificate.id}`;
if (credentials) {
fs.mkdirSync("/etc/letsencrypt/credentials", { recursive: true });
fs.writeFileSync(credentialsLocation, credentials, { mode: 0o600 });
} else {
// Nothing stored to write. A certificate issued under the previous behaviour may
// still have its file on disk; leave it be and let certbot decide. Throwing here
// would break a renewal that would otherwise have succeeded.
logger.warn(
`No stored DNS credentials for Cert #${certificate.id}; relying on any existing ${credentialsLocation}`,
);
}
const args = [
"renew",
"--force-renewal",
@@ -1004,9 +1055,19 @@ const internalCertificate = {
logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
try {
const result = await utils.execFile(certbotCommand, args, adds.opts);
logger.info(result);
return result;
} finally {
// Only clean up a file we put there ourselves. If `credentials` came back empty we
// wrote nothing, and an older file left on disk by the previous behaviour is the
// only thing keeping that certificate renewable - deleting it would break the next
// run for no gain.
if (credentials) {
fs.unlink(credentialsLocation, () => {});
}
}
},
/**
+22 -34
View File
@@ -54,9 +54,7 @@ const internalDeadHost = {
thisData.advanced_config = "";
}
const row = await deadHostModel.query()
.insertAndFetch(thisData)
.then(utils.omitRow(omissions()));
const row = await deadHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
// Add to audit log
await internalAuditLog.add(access, {
@@ -153,12 +151,8 @@ const internalDeadHost = {
thisData = internalHost.cleanSslHstsData(thisData, row);
// do the row update
await deadHostModel
.query()
.where({id: data.id})
.patch(data);
await deadHostModel.query().where({ id: data.id }).patch(data);
// Add to audit log
await internalAuditLog.add(access, {
@@ -168,15 +162,18 @@ const internalDeadHost = {
meta: thisData,
});
const thisRow = await internalDeadHost
.get(access, {
const thisRow = await internalDeadHost.get(access, {
id: thisData.id,
expand: ["owner", "certificate"],
});
// Configure nginx
const newMeta = await internalNginx.configure(deadHostModel, "dead_host", row);
row.meta = newMeta;
if (!thisRow.enabled) {
// No need to add nginx config if host is disabled
return _.omit(internalHost.cleanRowCertificateMeta(thisRow), omissions());
}
const newMeta = await internalNginx.configure(deadHostModel, "dead_host", thisRow);
thisRow.meta = newMeta;
return _.omit(internalHost.cleanRowCertificateMeta(thisRow), omissions());
},
@@ -194,7 +191,7 @@ const internalDeadHost = {
.query()
.where("is_deleted", 0)
.andWhere("id", data.id)
.allowGraph("[owner,certificate]")
.allowGraph(deadHostModel.defaultAllowGraph)
.first();
if (accessData.permission_visibility !== "all") {
@@ -206,7 +203,7 @@ const internalDeadHost = {
}
const row = await query.then(utils.omitRow(omissions()));
if (!row || !row.id) {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
// Custom omissions
@@ -224,16 +221,13 @@ const internalDeadHost = {
* @returns {Promise}
*/
delete: async (access, data) => {
await access.can("dead_hosts:delete", data.id)
await access.can("dead_hosts:delete", data.id);
const row = await internalDeadHost.get(access, { id: data.id });
if (!row || !row.id) {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
await deadHostModel
.query()
.where("id", row.id)
.patch({
await deadHostModel.query().where("id", row.id).patch({
is_deleted: 1,
});
@@ -259,12 +253,12 @@ const internalDeadHost = {
* @returns {Promise}
*/
enable: async (access, data) => {
await access.can("dead_hosts:update", data.id)
await access.can("dead_hosts:update", data.id);
const row = await internalDeadHost.get(access, {
id: data.id,
expand: ["certificate", "owner"],
});
if (!row || !row.id) {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
@@ -273,10 +267,7 @@ const internalDeadHost = {
row.enabled = 1;
await deadHostModel
.query()
.where("id", row.id)
.patch({
await deadHostModel.query().where("id", row.id).patch({
enabled: 1,
});
@@ -301,9 +292,9 @@ const internalDeadHost = {
* @returns {Promise}
*/
disable: async (access, data) => {
await access.can("dead_hosts:update", data.id)
await access.can("dead_hosts:update", data.id);
const row = await internalDeadHost.get(access, { id: data.id });
if (!row || !row.id) {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (!row.enabled) {
@@ -312,10 +303,7 @@ const internalDeadHost = {
row.enabled = 0;
await deadHostModel
.query()
.where("id", row.id)
.patch({
await deadHostModel.query().where("id", row.id).patch({
enabled: 0,
});
@@ -342,12 +330,12 @@ const internalDeadHost = {
* @returns {Promise}
*/
getAll: async (access, expand, searchQuery) => {
const accessData = await access.can("dead_hosts:list")
const accessData = await access.can("dead_hosts:list");
const query = deadHostModel
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph("[owner,certificate]")
.allowGraph(deadHostModel.defaultAllowGraph)
.orderBy(castJsonIfNeed("domain_names"), "ASC");
if (accessData.permission_visibility !== "all") {
+229
View File
@@ -0,0 +1,229 @@
import fs from "node:fs";
import errs from "../lib/error.js";
import internalDeadHost from "./dead-host.js";
import internalProxyHost from "./proxy-host.js";
import internalRedirectionHost from "./redirection-host.js";
import internalStream from "./stream.js";
const SYSTEM_LOG_FILE = "/data/logs/backend.log";
const LETSENCRYPT_LOG_FILE = "/data/logs/letsencrypt.log";
// Matches the access_log/error_log paths written by the nginx templates
// (see backend/templates/{proxy_host,redirection_host,dead_host,stream}.conf).
// This is a fixed, server-side lookup table - a host log path is always derived
// from a validated `host_type` enum + numeric `host_id`, never from a client-supplied
// path or filename, so there is no path-traversal surface here.
const HOST_FILE_PREFIX = {
proxy: "proxy-host",
redirection: "redirection-host",
dead: "dead-host",
stream: "stream",
};
const DEFAULT_LINES = 200;
const MAX_LINES = 1000;
const CHUNK_SIZE = 64 * 1024;
// Never scan further back than this, regardless of how many lines were requested,
// so a huge or pathological log file can't turn a single request into unbounded I/O.
const MAX_SCAN_BYTES = 5 * 1024 * 1024;
/**
* Reads at most `maxLines` lines from the end of a file, without loading the
* whole file into memory. Reads backwards in fixed-size chunks until enough
* newlines have been seen, the start of the file is reached, or the hard
* MAX_SCAN_BYTES ceiling is hit.
*
* @param {String} filePath
* @param {Number} maxLines
* @returns {Promise<{lines: String[], size: Number, truncated: Boolean}>}
*/
const readLastLines = async (filePath, maxLines) => {
let handle;
try {
handle = await fs.promises.open(filePath, "r");
const stat = await handle.stat();
const { size } = stat;
if (size === 0) {
return { lines: [], size: 0, truncated: false };
}
let position = size;
let scanned = 0;
let newlineCount = 0;
const chunks = [];
while (position > 0 && newlineCount <= maxLines && scanned < MAX_SCAN_BYTES) {
const readSize = Math.min(CHUNK_SIZE, position);
position -= readSize;
const buffer = Buffer.alloc(readSize);
await handle.read(buffer, 0, readSize, position);
scanned += readSize;
for (let i = buffer.length - 1; i >= 0; i--) {
if (buffer[i] === 0x0a) newlineCount++;
}
chunks.unshift(buffer);
}
const truncated = position > 0 && scanned >= MAX_SCAN_BYTES;
// If we didn't start reading from byte 0, the first line in our buffer is only
// partial *unless* it happens that `position` landed exactly on a line boundary
// (the byte right before it is a newline) - check that one byte to avoid
// silently dropping a perfectly valid line.
let startsOnLineBoundary = position === 0;
if (position > 0) {
const boundaryByte = Buffer.alloc(1);
await handle.read(boundaryByte, 0, 1, position - 1);
startsOnLineBoundary = boundaryByte[0] === 0x0a;
}
const text = Buffer.concat(chunks).toString("utf8");
const allLines = text.split("\n");
if (!startsOnLineBoundary && allLines.length > 0) {
allLines.shift();
}
// Drop the trailing empty element caused by a final trailing newline.
if (allLines.length > 0 && allLines[allLines.length - 1] === "") {
allLines.pop();
}
return { lines: allLines.slice(-maxLines), size, truncated };
} finally {
if (handle) {
await handle.close();
}
}
};
/**
* Resolves a validated {type, host_type, host_id, channel} selection to the
* fixed, absolute path of the log file on disk. Throws if the combination
* isn't a recognised source.
*
* `channel` ("access" | "error") picks which of the two log files nginx writes
* per host - it's deliberately not named "stream" to avoid confusion with the
* "stream" host_type (TCP/UDP stream hosts).
*
* @param {Object} data
* @returns {String}
*/
const resolveFilePath = (data) => {
switch (data.type) {
case "system":
return SYSTEM_LOG_FILE;
case "letsencrypt":
return LETSENCRYPT_LOG_FILE;
case "host": {
const prefix = HOST_FILE_PREFIX[data.host_type];
if (!prefix || !data.host_id || !["access", "error"].includes(data.channel)) {
throw new errs.ValidationError("Invalid host log source");
}
return `/data/logs/${prefix}-${data.host_id}_${data.channel}.log`;
}
default:
throw new errs.ItemNotFoundError(data.type);
}
};
/**
* @param {Array} rows
* @returns {Array}
*/
const toHostOptions = (rows) =>
rows.map((row) => ({
id: row.id,
label: Array.isArray(row.domain_names) ? row.domain_names.join(", ") : `Host #${row.id}`,
}));
const internalLogViewer = {
/**
* Lists the log sources available for the log viewer: the system (backend)
* log, the Let's Encrypt (certbot) log, and one entry per host the caller
* can see, for each host type.
*
* @param {Access} access
* @returns {Promise}
*/
listSources: async (access) => {
await access.can("logs:list");
const [proxyHosts, redirectionHosts, deadHosts, streams] = await Promise.all([
internalProxyHost.getAll(access),
internalRedirectionHost.getAll(access),
internalDeadHost.getAll(access),
internalStream.getAll(access),
]);
return {
system: { label: "System" },
letsencrypt: { label: "Let's Encrypt" },
hosts: {
proxy: toHostOptions(proxyHosts),
redirection: toHostOptions(redirectionHosts),
dead: toHostOptions(deadHosts),
stream: streams.map((row) => ({
id: row.id,
label: `Port ${row.incoming_port} → ${row.forwarding_host}:${row.forwarding_port}`,
})),
},
};
},
/**
* Returns the last N lines of the requested log source, optionally
* filtered by level and/or a plain-text search term.
*
* @param {Access} access
* @param {Object} data
* @param {String} data.type "system" | "letsencrypt" | "host"
* @param {String} [data.host_type] "proxy" | "redirection" | "dead" | "stream"
* @param {Number} [data.host_id]
* @param {String} [data.channel] "access" | "error"
* @param {Number} [data.lines]
* @param {String} [data.level]
* @param {String} [data.search]
* @returns {Promise}
*/
tail: async (access, data) => {
await access.can("logs:list");
const filePath = resolveFilePath(data);
const lines = Math.min(Math.max(data.lines || DEFAULT_LINES, 1), MAX_LINES);
let result;
try {
result = await readLastLines(filePath, lines);
} catch (err) {
if (err.code === "ENOENT") {
return { lines: [], size: 0, truncated: false, exists: false };
}
throw err;
}
let outputLines = result.lines;
// Only the system log is written in our own "LEVEL [scope]" format - level
// filtering on nginx/certbot lines would just match nothing and look like an
// empty log, so the filter is a no-op for any other source.
if (data.type === "system" && data.level) {
const needle = ` ${data.level.toUpperCase().padEnd(7)} `;
outputLines = outputLines.filter((line) => line.includes(needle));
}
if (data.search) {
const needle = data.search.toLowerCase();
outputLines = outputLines.filter((line) => line.toLowerCase().includes(needle));
}
return {
lines: outputLines,
size: result.size,
truncated: result.truncated,
exists: true,
};
},
};
export default internalLogViewer;
+45 -7
View File
@@ -1,10 +1,12 @@
import fs from "node:fs";
import net from "node:net";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import _ from "lodash";
import errs from "../lib/error.js";
import utils from "../lib/utils.js";
import { debug, nginx as logger } from "../logger.js";
import accessListModel from "../models/access_list.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -34,7 +36,7 @@ const internalNginx = {
// We're deleting this config regardless.
// Don't throw errors, as the file may not exist at all
// Delete the .err file too
return internalNginx.deleteConfig(host_type, host, false, true);
return internalNginx.deleteConfig(host_type, host, true);
})
.then(() => {
return internalNginx.generateConfig(host_type, host);
@@ -83,10 +85,12 @@ const internalNginx = {
meta: combined_meta,
})
.then(() => {
internalNginx.renameConfigAsError(host_type, host);
// Keep the failed config as a .err file for inspection
return internalNginx.renameConfigAsError(host_type, host);
})
.then(() => {
return internalNginx.deleteConfig(host_type, host, true);
// The rename removed the live config already, don't touch the .err file
return internalNginx.deleteConfig(host_type, host, false);
});
});
})
@@ -165,6 +169,24 @@ const internalNginx = {
host.locations[i],
);
// A location with its own access list overrides the host's,
// otherwise it inherits the host's access list
let locationAccessList = null;
if (locationCopy.access_list_id > 0 && locationCopy.access_list_id !== host.access_list?.id) {
locationAccessList = await accessListModel
.query()
.where("is_deleted", 0)
.andWhere("id", locationCopy.access_list_id)
.withGraphFetched("[clients,items]")
.first();
}
if (locationAccessList) {
locationCopy.access_list = locationAccessList;
} else {
locationCopy.access_list_id = host.access_list_id;
locationCopy.access_list = host.access_list;
}
if (locationCopy.forward_host.indexOf("/") > -1) {
const splitted = locationCopy.forward_host.split("/");
@@ -176,7 +198,9 @@ const internalNginx = {
}
};
locationRendering().then(() => resolve(renderedLocations));
locationRendering()
.then(() => resolve(renderedLocations))
.catch(reject);
});
},
@@ -217,10 +241,21 @@ const internalNginx = {
}
// For redirection hosts, if the scheme is not http or https, set it to $scheme
if (nice_host_type === "redirection_host" && ['http', 'https'].indexOf(host.forward_scheme.toLowerCase()) === -1) {
if (
nice_host_type === "redirection_host" &&
["http", "https"].indexOf(host.forward_scheme.toLowerCase()) === -1
) {
host.forward_scheme = "$scheme";
}
// A stream forwarding to an IPv6 literal must have the address wrapped in
// square brackets before nginx appends ":<port>". Without the brackets nginx
// reads the trailing ":<port>" as part of the address and rejects the upstream
// ("invalid port in upstream"), so the stream saves but never activates (#5740).
if (nice_host_type === "stream" && net.isIPv6(host.forwarding_host)) {
host.forwarding_host = `[${host.forwarding_host}]`;
}
if (host.locations) {
//logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2));
origLocations = [].concat(host.locations);
@@ -257,6 +292,9 @@ const internalNginx = {
debug(logger, `Could not write ${filename}:`, err.message);
reject(new errs.ConfigurationError(err.message));
});
}).catch((err) => {
debug(logger, `Could not render locations for ${filename}:`, err.message);
reject(new errs.ConfigurationError(err.message));
});
});
},
@@ -375,8 +413,8 @@ const internalNginx = {
const config_file_err = `${config_file}.err`;
return new Promise((resolve /*, reject*/) => {
fs.unlink(config_file, () => {
// ignore result, continue
fs.unlink(config_file_err, () => {
// ignore result, a previous .err file may not exist
fs.rename(config_file, config_file_err, () => {
// also ignore result, as this is a debugging informative file anyway
resolve();
+20 -23
View File
@@ -83,7 +83,7 @@ const internalProxyHost = {
expand: ["certificate", "owner", "access_list.[clients,items]"],
});
})
.then((row) => {
.then(async (row) => {
// Configure nginx
return internalNginx.configure(proxyHostModel, "proxy_host", row).then(() => {
return row;
@@ -115,9 +115,9 @@ const internalProxyHost = {
*/
update: (access, data) => {
let thisData = data;
const create_certificate = thisData.certificate_id === "new";
const createCertificate = thisData.certificate_id === "new";
if (create_certificate) {
if (createCertificate) {
delete thisData.certificate_id;
}
@@ -155,7 +155,7 @@ const internalProxyHost = {
);
}
if (create_certificate) {
if (createCertificate) {
return internalCertificate
.createQuickCertificate(access, {
domain_names: thisData.domain_names || row.domain_names,
@@ -208,7 +208,7 @@ const internalProxyHost = {
id: thisData.id,
expand: ["owner", "certificate", "access_list.[clients,items]"],
})
.then((row) => {
.then(async (row) => {
if (!row.enabled) {
// No need to add nginx config if host is disabled
return row;
@@ -232,7 +232,6 @@ const internalProxyHost = {
*/
get: (access, data) => {
const thisData = data || {};
return access
.can("proxy_hosts:get", thisData.id)
.then((access_data) => {
@@ -240,7 +239,7 @@ const internalProxyHost = {
.query()
.where("is_deleted", 0)
.andWhere("id", thisData.id)
.allowGraph("[owner,access_list.[clients,items],certificate]")
.allowGraph(proxyHostModel.defaultAllowGraph)
.first();
if (access_data.permission_visibility !== "all") {
@@ -254,7 +253,7 @@ const internalProxyHost = {
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
if (!row || !row.id) {
if (!row?.id) {
throw new errs.ItemNotFoundError(thisData.id);
}
const thisRow = internalHost.cleanRowCertificateMeta(row);
@@ -280,7 +279,7 @@ const internalProxyHost = {
return internalProxyHost.get(access, { id: data.id });
})
.then((row) => {
if (!row || !row.id) {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
@@ -327,8 +326,8 @@ const internalProxyHost = {
expand: ["certificate", "owner", "access_list"],
});
})
.then((row) => {
if (!row || !row.id) {
.then(async (row) => {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
@@ -337,27 +336,24 @@ const internalProxyHost = {
row.enabled = 1;
return proxyHostModel
await proxyHostModel
.query()
.where("id", row.id)
.patch({
enabled: 1,
})
.then(() => {
});
// Configure nginx
return internalNginx.configure(proxyHostModel, "proxy_host", row);
})
.then(() => {
await internalNginx.configure(proxyHostModel, "proxy_host", row);
// Add to audit log
return internalAuditLog.add(access, {
await internalAuditLog.add(access, {
action: "enabled",
object_type: "proxy-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
.then(() => {
return true;
});
},
@@ -376,7 +372,7 @@ const internalProxyHost = {
return internalProxyHost.get(access, { id: data.id });
})
.then((row) => {
if (!row || !row.id) {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (!row.enabled) {
@@ -422,11 +418,12 @@ const internalProxyHost = {
*/
getAll: async (access, expand, searchQuery) => {
const accessData = await access.can("proxy_hosts:list");
const query = proxyHostModel
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph("[owner,access_list,certificate]")
.allowGraph(proxyHostModel.defaultAllowGraph)
.orderBy(castJsonIfNeed("domain_names"), "ASC");
if (accessData.permission_visibility !== "all") {
+6 -7
View File
@@ -229,7 +229,6 @@ const internalRedirectionHost = {
*/
get: (access, data) => {
const thisData = data || {};
return access
.can("redirection_hosts:get", thisData.id)
.then((access_data) => {
@@ -237,7 +236,7 @@ const internalRedirectionHost = {
.query()
.where("is_deleted", 0)
.andWhere("id", thisData.id)
.allowGraph("[owner,certificate]")
.allowGraph(redirectionHostModel.defaultAllowGraph)
.first();
if (access_data.permission_visibility !== "all") {
@@ -252,7 +251,7 @@ const internalRedirectionHost = {
})
.then((row) => {
let thisRow = row;
if (!thisRow || !thisRow.id) {
if (!thisRow?.id) {
throw new errs.ItemNotFoundError(thisData.id);
}
thisRow = internalHost.cleanRowCertificateMeta(thisRow);
@@ -278,7 +277,7 @@ const internalRedirectionHost = {
return internalRedirectionHost.get(access, { id: data.id });
})
.then((row) => {
if (!row || !row.id) {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
@@ -326,7 +325,7 @@ const internalRedirectionHost = {
});
})
.then((row) => {
if (!row || !row.id) {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
@@ -374,7 +373,7 @@ const internalRedirectionHost = {
return internalRedirectionHost.get(access, { id: data.id });
})
.then((row) => {
if (!row || !row.id) {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (!row.enabled) {
@@ -426,7 +425,7 @@ const internalRedirectionHost = {
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph("[owner,certificate]")
.allowGraph(redirectionHostModel.defaultAllowGraph)
.orderBy(castJsonIfNeed("domain_names"), "ASC");
if (access_data.permission_visibility !== "all") {
+6 -7
View File
@@ -178,7 +178,6 @@ const internalStream = {
*/
get: (access, data) => {
const thisData = data || {};
return access
.can("streams:get", thisData.id)
.then((access_data) => {
@@ -186,7 +185,7 @@ const internalStream = {
.query()
.where("is_deleted", 0)
.andWhere("id", thisData.id)
.allowGraph("[owner,certificate]")
.allowGraph(streamModel.defaultAllowGraph)
.first();
if (access_data.permission_visibility !== "all") {
@@ -201,7 +200,7 @@ const internalStream = {
})
.then((row) => {
let thisRow = row;
if (!thisRow || !thisRow.id) {
if (!thisRow?.id) {
throw new errs.ItemNotFoundError(thisData.id);
}
thisRow = internalHost.cleanRowCertificateMeta(thisRow);
@@ -227,7 +226,7 @@ const internalStream = {
return internalStream.get(access, { id: data.id });
})
.then((row) => {
if (!row || !row.id) {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
@@ -275,7 +274,7 @@ const internalStream = {
});
})
.then((row) => {
if (!row || !row.id) {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
@@ -323,7 +322,7 @@ const internalStream = {
return internalStream.get(access, { id: data.id });
})
.then((row) => {
if (!row || !row.id) {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (!row.enabled) {
@@ -375,7 +374,7 @@ const internalStream = {
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph("[owner,certificate]")
.allowGraph(streamModel.defaultAllowGraph)
.orderBy("incoming_port", "ASC");
if (access_data.permission_visibility !== "all") {
+4 -14
View File
@@ -38,11 +38,7 @@ export default {
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
}
const auth = await authModel
.query()
.where("user_id", "=", user.id)
.where("type", "=", "password")
.first();
const auth = await authModel.query().where("user_id", "=", user.id).where("type", "=", "password").first();
if (!auth) {
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
@@ -50,10 +46,7 @@ export default {
const valid = await auth.verifyPassword(data.secret);
if (!valid) {
throw new errs.AuthError(
ERROR_MESSAGE_INVALID_AUTH,
ERROR_MESSAGE_INVALID_AUTH_I18N,
);
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH, ERROR_MESSAGE_INVALID_AUTH_I18N);
}
if (data.scope !== "user" && _.indexOf(user.roles, data.scope) === -1) {
@@ -171,7 +164,7 @@ export default {
}
// Check scope
if (!tokenData.scope || tokenData.scope[0] !== "2fa-challenge") {
if (tokenData.scope?.[0] !== "2fa-challenge") {
throw new errs.AuthError("Invalid challenge token");
}
@@ -183,10 +176,7 @@ export default {
// Verify 2FA code
const valid = await twoFactor.verifyForLogin(userId, code);
if (!valid) {
throw new errs.AuthError(
ERROR_MESSAGE_INVALID_2FA,
ERROR_MESSAGE_INVALID_2FA_I18N,
);
throw new errs.AuthError(ERROR_MESSAGE_INVALID_2FA, ERROR_MESSAGE_INVALID_2FA_I18N);
}
// Create full token
+20 -6
View File
@@ -87,7 +87,13 @@ const internalUser = {
}
return access
.can("users:update", data.id)
.can("users:permissions", data.id)
.catch(() => {
delete data.roles;
})
.then(() => {
return access.can("users:update", data.id);
})
.then(() => {
// Make sure that the user being updated doesn't change their email to another user that is already using it
// 1. get user we want to update
@@ -171,7 +177,7 @@ const internalUser = {
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
if (!row || !row.id) {
if (!row?.id) {
throw new errs.ItemNotFoundError(thisData.id);
}
// Custom omissions
@@ -251,9 +257,7 @@ const internalUser = {
},
deleteAll: async () => {
await userModel
.query()
.patch({
await userModel.query().patch({
is_deleted: 1,
});
},
@@ -384,11 +388,21 @@ const internalUser = {
.andWhere("type", data.type)
.first()
.then((existing_auth) => {
// Stamped here rather than read off modified_on, because it is compared against a
// token's `iat` and the two only line up when the same clock writes both. The
// database clock is a different one: with the app on one timezone and the database
// on another, its timestamps come back hours away from where Node thinks it is.
const password_changed_at = Math.floor(Date.now() / 1000);
if (existing_auth) {
// patch
const meta = existing_auth.meta || {};
meta.password_changed_at = password_changed_at;
return authModel.query().where("user_id", user.id).andWhere("type", data.type).patch({
type: data.type, // This is required for the model to encrypt on save
secret: data.secret,
meta,
});
}
// insert
@@ -396,7 +410,7 @@ const internalUser = {
user_id: user.id,
type: data.type,
secret: data.secret,
meta: {},
meta: { password_changed_at },
});
})
.then(() => {
+11 -11
View File
@@ -1,19 +1,19 @@
module.exports = {
development: {
client: 'mysql2',
client: "mysql2",
migrations: {
tableName: 'migrations',
stub: 'lib/migrate_template.js',
directory: 'migrations'
}
tableName: "migrations",
stub: "lib/migrate_template.js",
directory: "migrations",
},
},
production: {
client: 'mysql2',
client: "mysql2",
migrations: {
tableName: 'migrations',
stub: 'lib/migrate_template.js',
directory: 'migrations'
}
}
tableName: "migrations",
stub: "lib/migrate_template.js",
directory: "migrations",
},
},
};
+24 -5
View File
@@ -12,6 +12,7 @@ import { fileURLToPath } from "node:url";
import Ajv from "ajv/dist/2020.js";
import _ from "lodash";
import { access as logger } from "../logger.js";
import authModel from "../models/auth.js";
import proxyHostModel from "../models/proxy_host.js";
import TokenModel from "../models/token.js";
import userModel from "../models/user.js";
@@ -80,6 +81,22 @@ export default function (tokenString) {
if (!ok) {
throw new errs.AuthError("Invalid token scope for User");
}
// A token issued before the password was last changed is no longer valid: taking an account
// back from whoever has the old password has to end the sessions that password opened.
const auth = await authModel
.query()
.where("user_id", "=", user.id)
.where("type", "=", "password")
.first();
// Both sides come from the same clock and in the same unit, whole seconds since
// the epoch: `setPassword` stamps the marker and `jsonwebtoken` stamps `iat`.
const changedAt = auth?.meta?.password_changed_at;
if (changedAt && typeof tokenData.iat === "number" && tokenData.iat < changedAt) {
throw new errs.TokenRevokedError("Token was issued before the password was changed");
}
initialised = true;
userRoles = user.roles;
permissions = user.permissions;
@@ -119,10 +136,7 @@ export default function (tokenString) {
// Proxy Hosts
case "proxy_hosts": {
const query = proxyHostModel
.query()
.select("id")
.andWhere("is_deleted", 0);
const query = proxyHostModel.query().select("id").andWhere("is_deleted", 0);
if (permissions.visibility === "user") {
query.andWhere("owner_user_id", tokenUserId);
@@ -271,7 +285,12 @@ export default function (tokenString) {
err.permission = permission;
err.permission_data = data;
logger.error(permission, data, err.message);
throw errs.PermissionError("Permission Denied", err);
// A revoked token is not a permission problem, and the client can tell: the frontend
// clears the session on a 401 and on nothing else.
if (err instanceof errs.TokenRevokedError) {
throw err;
}
throw new errs.PermissionError("Permission Denied", err);
}
},
};
+7
View File
@@ -0,0 +1,7 @@
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}
+17 -13
View File
@@ -4,8 +4,6 @@ import { certbot as logger } from "../logger.js";
import errs from "./error.js";
import utils from "./utils.js";
const CERTBOT_VERSION_REPLACEMENT = "$(certbot --version | grep -Eo '[0-9](\\.[0-9]+)+')";
/**
* Installs a cerbot plugin given the key for the object from
* ../certbot/dns-plugins.json
@@ -15,24 +13,32 @@ const CERTBOT_VERSION_REPLACEMENT = "$(certbot --version | grep -Eo '[0-9](\\.[0
*/
const installPlugin = async (pluginKey) => {
if (typeof dnsPlugins[pluginKey] === "undefined") {
// throw Error(`Certbot plugin ${pluginKey} not found`);
throw new errs.ItemNotFoundError(pluginKey);
}
const plugin = dnsPlugins[pluginKey];
logger.start(`Installing ${pluginKey}...`);
plugin.version = plugin.version.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
plugin.dependencies = plugin.dependencies.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
plugin.version = plugin.version.replace(/{{certbot-version}}/g, process.env.CERTBOT_VERSION);
plugin.dependencies = plugin.dependencies.replace(/{{certbot-version}}/g, process.env.CERTBOT_VERSION);
// SETUPTOOLS_USE_DISTUTILS is required for certbot plugins to install correctly
// in new versions of Python
let env = Object.assign({}, process.env, { SETUPTOOLS_USE_DISTUTILS: "stdlib" });
// SETUPTOOLS_USE_DISTUTILS=local uses setuptools' own bundled distutils.
// "stdlib" breaks Python 3.13+ where distutils was removed from the standard library.
let env = Object.assign({}, process.env, { SETUPTOOLS_USE_DISTUTILS: "local" });
if (typeof plugin.env === "object") {
env = Object.assign(env, plugin.env);
}
const cmd = `. /opt/certbot/bin/activate && pip install --no-cache-dir ${plugin.dependencies} ${plugin.package_name}${plugin.version} && deactivate`;
const quotedDeps = plugin.dependencies.trim()
? plugin.dependencies
.trim()
.split(/\s+/)
.filter(Boolean)
.map((d) => `'${d}'`)
.join(" ")
: "";
const cmd = `. /opt/certbot/bin/activate && pip install --no-cache-dir ${quotedDeps} '${plugin.package_name}${plugin.version}' && deactivate`;
return utils
.exec(cmd, { env })
.then((result) => {
@@ -73,9 +79,7 @@ const installPlugins = async (pluginKeys) => {
})
.end(() => {
if (hasErrors) {
reject(
new errs.CommandError("Some plugins failed to install. Please check the logs above", 1),
);
reject(new errs.CommandError("Some plugins failed to install. Please check the logs above", 1));
} else {
resolve();
}
@@ -83,4 +87,4 @@ const installPlugins = async (pluginKeys) => {
});
};
export { installPlugins, installPlugin };
export { installPlugin, installPlugins };
+36 -13
View File
@@ -2,10 +2,13 @@ import fs from "node:fs";
import NodeRSA from "node-rsa";
import { global as logger } from "../logger.js";
const keysFile = '/data/keys.json';
const mysqlEngine = 'mysql2';
const postgresEngine = 'pg';
const sqliteClientName = 'better-sqlite3';
const keysFile = "/data/keys.json";
const mysqlEngine = "mysql2";
const postgresEngine = "pg";
const sqliteClientName = "better-sqlite3";
// Not used for new setups anymore but may exist in legacy setups
const legacySqliteClientName = "sqlite3";
let instance = null;
@@ -37,14 +40,20 @@ const configure = () => {
}
}
const toBool = (v) => /^(1|true|yes|on)$/i.test((v || '').trim());
const toBool = (v) => /^(1|true|yes|on)$/i.test((v || "").trim());
const envMysqlHost = process.env.DB_MYSQL_HOST || null;
const envMysqlUser = process.env.DB_MYSQL_USER || null;
const envMysqlName = process.env.DB_MYSQL_NAME || null;
const envMysqlSSL = toBool(process.env.DB_MYSQL_SSL);
const envMysqlSSLRejectUnauthorized = process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED === undefined ? true : toBool(process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED);
const envMysqlSSLVerifyIdentity = process.env.DB_MYSQL_SSL_VERIFY_IDENTITY === undefined ? true : toBool(process.env.DB_MYSQL_SSL_VERIFY_IDENTITY);
const envMysqlSSLRejectUnauthorized =
process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED === undefined
? true
: toBool(process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED);
const envMysqlSSLVerifyIdentity =
process.env.DB_MYSQL_SSL_VERIFY_IDENTITY === undefined
? true
: toBool(process.env.DB_MYSQL_SSL_VERIFY_IDENTITY);
if (envMysqlHost && envMysqlUser && envMysqlName) {
// we have enough mysql creds to go with mysql
logger.info("Using MySQL configuration");
@@ -56,7 +65,9 @@ const configure = () => {
user: envMysqlUser,
password: process.env.DB_MYSQL_PASSWORD,
name: envMysqlName,
ssl: envMysqlSSL ? { rejectUnauthorized: envMysqlSSLRejectUnauthorized, verifyIdentity: envMysqlSSLVerifyIdentity } : false,
ssl: envMysqlSSL
? { rejectUnauthorized: envMysqlSSLRejectUnauthorized, verifyIdentity: envMysqlSSLVerifyIdentity }
: false,
},
keys: getKeys(),
};
@@ -134,7 +145,7 @@ const generateKeys = () => {
// Write keys config
try {
fs.writeFileSync(keysFile, JSON.stringify(keys, null, 2));
fs.writeFileSync(keysFile, JSON.stringify(keys, null, 2), { mode: 0o600 });
} catch (err) {
logger.error(`Could not write JWT key pair to config file: ${keysFile}: ${err.message}`);
process.exit(1);
@@ -184,7 +195,7 @@ const configGet = (key) => {
*/
const isSqlite = () => {
instance === null && configure();
return instance.database.knex && instance.database.knex.client === sqliteClientName;
return instance.database.knex && [sqliteClientName, legacySqliteClientName].includes(instance.database.knex.client);
};
/**
@@ -208,7 +219,7 @@ const isPostgres = () => {
};
/**
* Are we running in debug mdoe?
* Are we running in debug mode?
*
* @returns {boolean}
*/
@@ -219,7 +230,7 @@ const isDebugMode = () => !!process.env.DEBUG;
*
* @returns {boolean}
*/
const isCI = () => process.env.CI === 'true' && process.env.DEBUG === 'true';
const isCI = () => process.env.CI === "true" && process.env.DEBUG === "true";
/**
* Returns a public key
@@ -256,4 +267,16 @@ const useLetsencryptServer = () => {
return null;
};
export { isCI, configHas, configGet, isSqlite, isMysql, isPostgres, isDebugMode, getPrivateKey, getPublicKey, useLetsencryptStaging, useLetsencryptServer };
export {
isCI,
configHas,
configGet,
isSqlite,
isMysql,
isPostgres,
isDebugMode,
getPrivateKey,
getPublicKey,
useLetsencryptStaging,
useLetsencryptServer,
};
+9
View File
@@ -22,6 +22,15 @@ const errs = {
this.status = 404;
},
TokenRevokedError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.public = true;
this.status = 401;
},
AuthError: function (message, messageI18n, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
+1 -1
View File
@@ -3,7 +3,7 @@ export default (req, res, next) => {
res.set({
"Access-Control-Allow-Origin": req.headers.origin,
"Access-Control-Allow-Credentials": true,
"Access-Control-Allow-Methods": "OPTIONS, GET, POST",
"Access-Control-Allow-Methods": "OPTIONS, GET, POST, PUT, DELETE",
"Access-Control-Allow-Headers":
"Content-Type, Cache-Control, Pragma, Expires, Authorization, X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit",
"Access-Control-Max-Age": 5 * 60,
+7
View File
@@ -0,0 +1,7 @@
import chalk from "chalk";
import { debug, express as logger } from "../../logger.js";
export default (req, _res, next) => {
debug(logger, `[${chalk.yellow(req.method.toUpperCase())}] ${chalk.green(req.path)}`);
next();
};
+2 -2
View File
@@ -1,6 +1,6 @@
export default (req, res, next) => {
if (req.params.user_id === 'me' && res.locals.access) {
req.params.user_id = res.locals.access.token.get('attrs').id;
if (req.params.user_id === "me" && res.locals.access) {
req.params.user_id = res.locals.access.token.get("attrs").id;
} else {
req.params.user_id = Number.parseInt(req.params.user_id, 10);
}
-3
View File
@@ -3,14 +3,12 @@ import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { Liquid } from "liquidjs";
import _ from "lodash";
import { debug, global as logger } from "../logger.js";
import errs from "./error.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const exec = async (cmd, options = {}) => {
debug(logger, "CMD:", cmd);
const { stdout, stderr } = await new Promise((resolve, reject) => {
const child = nodeExec(cmd, options, (isError, stdout, stderr) => {
if (isError) {
@@ -34,7 +32,6 @@ const exec = async (cmd, options = {}) => {
* @returns {Promise}
*/
const execFile = (cmd, args, options) => {
debug(logger, `CMD: ${cmd} ${args ? args.join(" ") : ""}`);
const opts = options || {};
return new Promise((resolve, reject) => {
+3 -4
View File
@@ -1,3 +1,4 @@
import net from "node:net";
import Ajv from "ajv/dist/2020.js";
import errs from "../error.js";
@@ -9,6 +10,8 @@ const ajv = new Ajv({
coerceTypes: true,
});
ajv.addFormat("ipv6", { type: "string", validate: (value) => net.isIPv6(value) });
/**
* @param {Object} schema
* @param {Object} payload
@@ -24,18 +27,14 @@ const apiValidator = async (schema, payload /*, description*/) => {
throw new errs.ValidationError("Payload is undefined");
}
const validate = ajv.compile(schema);
const valid = validate(payload);
if (valid && !validate.errors) {
return payload;
}
const message = ajv.errorsText(validate.errors);
const err = new errs.ValidationError(message);
err.debug = { validationErrors: validate.errors, payload };
+4 -1
View File
@@ -1,4 +1,5 @@
import Ajv from 'ajv/dist/2020.js';
import net from "node:net";
import Ajv from "ajv/dist/2020.js";
import _ from "lodash";
import commonDefinitions from "../../schema/common.json" with { type: "json" };
import errs from "../error.js";
@@ -14,6 +15,8 @@ const ajv = new Ajv({
schemas: [commonDefinitions],
});
ajv.addFormat("ipv6", { type: "string", validate: (value) => net.isIPv6(value) });
/**
*
* @param {Object} schema
+111 -11
View File
@@ -1,3 +1,5 @@
import fs from "node:fs";
import path from "node:path";
import signale from "signale";
import { isDebugMode } from "./lib/config.js";
@@ -5,17 +7,115 @@ const opts = {
logLevel: "info",
};
const global = new signale.Signale({ scope: "Global ", ...opts });
const migrate = new signale.Signale({ scope: "Migrate ", ...opts });
const express = new signale.Signale({ scope: "Express ", ...opts });
const access = new signale.Signale({ scope: "Access ", ...opts });
const nginx = new signale.Signale({ scope: "Nginx ", ...opts });
const ssl = new signale.Signale({ scope: "SSL ", ...opts });
const certbot = new signale.Signale({ scope: "Certbot ", ...opts });
const importer = new signale.Signale({ scope: "Importer ", ...opts });
const setup = new signale.Signale({ scope: "Setup ", ...opts });
const ipRanges = new signale.Signale({ scope: "IP Ranges", ...opts });
const remoteVersion = new signale.Signale({ scope: "Remote Version", ...opts });
// Methods that are actually used across the codebase (see grep of `.info(`, `.warn(`, etc).
// Only these are mirrored to the log file - decorative signale methods (star, note, watch, ...)
// are left console-only since they carry no diagnostic value worth persisting.
const PERSISTED_METHODS = ["info", "warn", "error", "debug", "success", "fatal", "complete"];
const LOG_FILE = "/data/logs/backend.log";
// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping ANSI colour codes before writing to disk
const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
let fileStream = null;
let lastOpenAttempt = 0;
// If the file can't be opened (eg. running outside the standard Docker image, or in
// CI without /data), don't retry on every single log call - but do retry periodically
// so a transient issue (disk full, permissions fixed later) recovers without a restart.
const REOPEN_COOLDOWN_MS = 30 * 1000;
/**
* Lazily opens the backend log file for appending. If the directory isn't writable,
* file logging is silently disabled and console logging continues unaffected.
*
* @returns {import('node:fs').WriteStream|null}
*/
const getFileStream = () => {
if (fileStream) {
return fileStream;
}
const now = Date.now();
if (now - lastOpenAttempt < REOPEN_COOLDOWN_MS) {
return null;
}
lastOpenAttempt = now;
try {
fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
const stream = fs.createWriteStream(LOG_FILE, { flags: "a" });
stream.on("error", () => {
fileStream = null;
});
fileStream = stream;
} catch (_err) {
fileStream = null;
}
return fileStream;
};
/**
* Formats and appends a single log line to the backend log file.
* Never throws - a failure here must never take down the application.
*
* @param {String} level
* @param {String} scope
* @param {Array} args
*/
const writeToFile = (level, scope, args) => {
const stream = getFileStream();
if (!stream) {
return;
}
const message = args
.map((arg) => {
if (typeof arg === "string") return arg;
if (arg instanceof Error) return arg.stack || arg.message;
try {
return JSON.stringify(arg);
} catch (_err) {
return String(arg);
}
})
.join(" ")
.replace(ANSI_PATTERN, "");
const line = `${new Date().toISOString()} ${level.toUpperCase().padEnd(7)} [${scope.trim()}] ${message}\n`;
stream.write(line);
};
/**
* Wraps a Signale instance so that every call to one of PERSISTED_METHODS is also
* appended to the backend log file, in addition to its normal console output.
*
* @param {Signale} instance
* @param {String} scope
* @returns {Signale}
*/
const withFileSink = (instance, scope) => {
for (const method of PERSISTED_METHODS) {
const original = instance[method].bind(instance);
instance[method] = (...args) => {
writeToFile(method, scope, args);
return original(...args);
};
}
return instance;
};
const createLogger = (scope) => withFileSink(new signale.Signale({ scope, ...opts }), scope);
const global = createLogger("Global ");
const migrate = createLogger("Migrate ");
const express = createLogger("Express ");
const access = createLogger("Access ");
const nginx = createLogger("Nginx ");
const ssl = createLogger("SSL ");
const certbot = createLogger("Certbot ");
const importer = createLogger("Importer ");
const setup = createLogger("Setup ");
const ipRanges = createLogger("IP Ranges");
const remoteVersion = createLogger("Remote Version");
const debug = (logger, ...args) => {
if (isDebugMode()) {
@@ -13,12 +13,13 @@ const migrateName = "settings";
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema.createTable('setting', (table) => {
table.string('id').notNull().primary();
table.string('name', 100).notNull();
table.string('description', 255).notNull();
table.string('value', 255).notNull();
table.json('meta').notNull();
return knex.schema
.createTable("setting", (table) => {
table.string("id").notNull().primary();
table.string("name", 100).notNull();
table.string("description", 255).notNull();
table.string("value", 255).notNull();
table.json("meta").notNull();
})
.then(() => {
logger.info(`[${migrateName}] setting Table created`);
@@ -17,9 +17,7 @@ const up = (knex) => {
.table("redirection_host", async (table) => {
// change the column default from $scheme to auto
await table.string("forward_scheme").notNull().defaultTo("auto").alter();
await knex('redirection_host')
.where('forward_scheme', '$scheme')
.update({ forward_scheme: 'auto' });
await knex("redirection_host").where("forward_scheme", "$scheme").update({ forward_scheme: "auto" });
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
@@ -38,9 +36,7 @@ const down = (knex) => {
return knex.schema
.table("redirection_host", async (table) => {
await table.string("forward_scheme").notNull().defaultTo("$scheme").alter();
await knex('redirection_host')
.where('forward_scheme', 'auto')
.update({ forward_scheme: '$scheme' });
await knex("redirection_host").where("forward_scheme", "auto").update({ forward_scheme: "$scheme" });
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
@@ -0,0 +1,43 @@
import { migrate as logger } from "../logger.js";
const migrateName = "trust_forwarded_proto";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.alterTable("proxy_host", (table) => {
table.tinyint("trust_forwarded_proto").notNullable().defaultTo(0);
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema
.alterTable("proxy_host", (table) => {
table.dropColumn("trust_forwarded_proto");
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
});
};
export { up, down };
+4
View File
@@ -31,6 +31,10 @@ class AccessList extends Model {
$parseDatabaseJson(json) {
const thisJson = super.$parseDatabaseJson(json);
// Postgres returns COUNT() as a string
if (typeof thisJson.proxy_host_count === "string") {
thisJson.proxy_host_count = Number.parseInt(thisJson.proxy_host_count, 10);
}
return convertIntFieldsToBool(thisJson, boolFields);
}
+14 -6
View File
@@ -15,6 +15,16 @@ Model.knex(db());
const boolFields = ["is_deleted"];
const cleanDomainNames = (domainNames) => {
// Sort domain_names
if (typeof domainNames !== "undefined") {
const newDomainNames = domainNames.filter((name) => name != null);
newDomainNames.sort();
return newDomainNames;
}
return [];
};
class Certificate extends Model {
$beforeInsert() {
this.created_on = now();
@@ -26,7 +36,9 @@ class Certificate extends Model {
}
// Default for domain_names
if (typeof this.domain_names === "undefined") {
if (typeof this.domain_names !== "undefined") {
this.domain_names = cleanDomainNames(this.domain_names);
} else {
this.domain_names = [];
}
@@ -34,16 +46,12 @@ class Certificate extends Model {
if (typeof this.meta === "undefined") {
this.meta = {};
}
this.domain_names.sort();
}
$beforeUpdate() {
this.modified_on = now();
// Sort domain_names
if (typeof this.domain_names !== "undefined") {
this.domain_names.sort();
this.domain_names = cleanDomainNames(this.domain_names);
}
}
+13 -1
View File
@@ -3,7 +3,7 @@
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import Certificate from "./certificate.js";
import now from "./now_helper.js";
import User from "./user.js";
@@ -61,6 +61,18 @@ class DeadHost extends Model {
return ["domain_names", "meta"];
}
static get defaultAllowGraph() {
return "[owner,certificate]";
}
static get defaultExpand() {
return ["certificate", "owner"];
}
static get defaultOrder() {
return [castJsonIfNeed("domain_names"), "ASC"];
}
static get relationMappings() {
return {
owner: {
+14 -1
View File
@@ -3,7 +3,7 @@
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import AccessList from "./access_list.js";
import Certificate from "./certificate.js";
import now from "./now_helper.js";
@@ -21,6 +21,7 @@ const boolFields = [
"enabled",
"hsts_enabled",
"hsts_subdomains",
"trust_forwarded_proto",
];
class ProxyHost extends Model {
@@ -72,6 +73,18 @@ class ProxyHost extends Model {
return ["domain_names", "meta", "locations"];
}
static get defaultAllowGraph() {
return "[owner,access_list.[clients,items],certificate]";
}
static get defaultExpand() {
return ["owner", "certificate", "access_list.[clients,items]"];
}
static get defaultOrder() {
return [castJsonIfNeed("domain_names"), "ASC"];
}
static get relationMappings() {
return {
owner: {
+13 -1
View File
@@ -3,7 +3,7 @@
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import Certificate from "./certificate.js";
import now from "./now_helper.js";
import User from "./user.js";
@@ -70,6 +70,18 @@ class RedirectionHost extends Model {
return ["domain_names", "meta"];
}
static get defaultAllowGraph() {
return "[owner,certificate]";
}
static get defaultExpand() {
return ["certificate", "owner"];
}
static get defaultOrder() {
return [castJsonIfNeed("domain_names"), "ASC"];
}
static get relationMappings() {
return {
owner: {
+4 -4
View File
@@ -9,21 +9,21 @@ Model.knex(db());
class Setting extends Model {
$beforeInsert() {
// Default for meta
if (typeof this.meta === 'undefined') {
if (typeof this.meta === "undefined") {
this.meta = {};
}
}
static get name() {
return 'Setting';
return "Setting";
}
static get tableName() {
return 'setting';
return "setting";
}
static get jsonAttributes() {
return ['meta'];
return ["meta"];
}
}
+13 -1
View File
@@ -1,6 +1,6 @@
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import Certificate from "./certificate.js";
import now from "./now_helper.js";
import User from "./user.js";
@@ -46,6 +46,18 @@ class Stream extends Model {
return ["meta"];
}
static get defaultAllowGraph() {
return "[owner,certificate]";
}
static get defaultExpand() {
return ["certificate", "owner"];
}
static get defaultOrder() {
return [castJsonIfNeed("incoming_port"), "ASC"];
}
static get relationMappings() {
return {
owner: {
+2 -2
View File
@@ -18,11 +18,11 @@ class UserPermission extends Model {
}
static get name() {
return 'UserPermission';
return "UserPermission";
}
static get tableName() {
return 'user_permission';
return "user_permission";
}
}
+23 -22
View File
@@ -9,41 +9,42 @@
"scripts": {
"lint": "biome lint",
"prettier": "biome format --write .",
"validate-schema": "node validate-schema.js"
"validate-schema": "node validate-schema.js",
"regenerate-config": "node scripts/regenerate-config"
},
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^14.1.1",
"ajv": "^8.17.1",
"archiver": "^7.0.1",
"@apidevtools/json-schema-ref-parser": "^16.0.2",
"ajv": "^8.20.0",
"archiver": "^8.0.0",
"batchflow": "^0.4.0",
"bcrypt": "^6.0.0",
"better-sqlite3": "^12.6.2",
"body-parser": "^2.2.2",
"compression": "^1.7.4",
"better-sqlite3": "^13.0.3",
"body-parser": "^2.3.0",
"chalk": "5.6.2",
"compression": "^1.8.2",
"express": "^5.2.1",
"express-fileupload": "^1.5.2",
"gravatar": "^1.8.2",
"jsonwebtoken": "^9.0.3",
"knex": "3.1.0",
"liquidjs": "10.24.0",
"lodash": "^4.17.23",
"moment": "^2.30.1",
"mysql2": "^3.16.3",
"node-rsa": "^1.1.1",
"knex": "3.3.0",
"liquidjs": "10.29.0",
"lodash": "^4.18.1",
"moment": "^2.31.0",
"mysql2": "^3.24.4",
"node-rsa": "^2.0.0",
"objection": "3.1.5",
"otplib": "^13.2.1",
"otplib": "^13.5.0",
"path": "^0.12.7",
"pg": "^8.18.0",
"proxy-agent": "^6.5.0",
"pg": "^8.23.0",
"proxy-agent": "^8.0.2",
"signale": "1.4.0",
"sqlite3": "^5.1.7",
"temp-write": "^4.0.0"
"sqlite3": "^6.0.1",
"temp-write": "^6.0.1"
},
"devDependencies": {
"@apidevtools/swagger-parser": "^12.1.0",
"@biomejs/biome": "^2.3.14",
"chalk": "5.6.2",
"nodemon": "^3.1.11"
"@apidevtools/swagger-parser": "^13.0.0",
"@biomejs/biome": "^2.5.10",
"nodemon": "^3.1.14"
},
"signale": {
"displayDate": true,
+1 -4
View File
@@ -86,10 +86,7 @@ router
},
{
event_id: req.params.event_id,
expand:
typeof req.query.expand === "string"
? req.query.expand.split(",")
: null,
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
},
);
+59
View File
@@ -0,0 +1,59 @@
import express from "express";
import dnsPlugins from "../certbot/dns-plugins.json" with { type: "json" };
import { installPlugin } from "../lib/certbot.js";
import { debug, express as logger } from "../logger.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* ONLY AVAILABLE IN CI ENVIRONMENT!
*/
/**
* /api/ci/certbot-plugins
*/
router
.route("/certbot-plugins")
.options((_, res) => {
res.sendStatus(204);
})
// Return all certbot plugins
.get(async (_req, res, _next) => {
res.status(200).send(dnsPlugins);
});
/**
* /api/ci/certbot-plugins/{plugin}
*/
router
.route("/certbot-plugins/:plugin")
.options((_, res) => {
res.sendStatus(204);
})
// Install a certbot plugin
.post(async (req, res, next) => {
try {
const pluginName = req.params.plugin;
// check if plugin exists
if (!dnsPlugins[pluginName]) {
return res.status(404).send({
error: "Plugin not found",
});
}
await installPlugin(pluginName);
res.status(200).send(true);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
return;
});
export default router;
+106
View File
@@ -0,0 +1,106 @@
import express from "express";
import internalLogViewer from "../internal/log-viewer.js";
import jwtdecode from "../lib/express/jwt-decode.js";
import validator from "../lib/validator/index.js";
import { debug, express as logger } from "../logger.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/logs/sources
*/
router
.route("/sources")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/logs/sources
*
* Lists the log sources available for the log viewer
*/
.get(async (req, res, next) => {
try {
const data = await internalLogViewer.listSources(res.locals.access);
res.status(200).send(data);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* /api/logs/tail
*/
router
.route("/tail")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/logs/tail
*
* Retrieve the last N lines of a log source
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["type"],
additionalProperties: false,
properties: {
type: {
type: "string",
enum: ["system", "letsencrypt", "host"],
},
host_type: {
anyOf: [{ type: "null" }, { type: "string", enum: ["proxy", "redirection", "dead", "stream"] }],
},
host_id: {
anyOf: [{ type: "null" }, { type: "integer", minimum: 1 }],
},
channel: {
anyOf: [{ type: "null" }, { type: "string", enum: ["access", "error"] }],
},
lines: {
anyOf: [{ type: "null" }, { type: "integer", minimum: 1, maximum: 1000 }],
},
level: {
anyOf: [
{ type: "null" },
{ type: "string", enum: ["INFO", "WARN", "ERROR", "DEBUG", "SUCCESS", "FATAL", "COMPLETE"] },
],
},
search: {
anyOf: [{ type: "null" }, { type: "string", minLength: 1, maxLength: 200 }],
},
},
},
{
type: req.query.type,
host_type: typeof req.query.host_type === "string" ? req.query.host_type : null,
host_id: typeof req.query.host_id !== "undefined" ? req.query.host_id : null,
channel: typeof req.query.channel === "string" ? req.query.channel : null,
lines: typeof req.query.lines !== "undefined" ? req.query.lines : null,
level: typeof req.query.level === "string" ? req.query.level : null,
search: typeof req.query.search === "string" ? req.query.search : null,
},
);
const result = await internalLogViewer.tail(res.locals.access, data);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;
+12
View File
@@ -1,8 +1,12 @@
import express from "express";
import { isCI } from "../lib/config.js";
import errs from "../lib/error.js";
import logRequest from "../lib/express/log-request.js";
import pjson from "../package.json" with { type: "json" };
import { isSetup } from "../setup.js";
import auditLogRoutes from "./audit-log.js";
import ciRoutes from "./ci.js";
import logsRoutes from "./logs.js";
import accessListsRoutes from "./nginx/access_lists.js";
import certificatesHostsRoutes from "./nginx/certificates.js";
import deadHostsRoutes from "./nginx/dead_hosts.js";
@@ -22,6 +26,8 @@ const router = express.Router({
mergeParams: true,
});
router.use(logRequest);
/**
* Health Check
* GET /api
@@ -45,6 +51,7 @@ router.use("/schema", schemaRoutes);
router.use("/tokens", tokensRoutes);
router.use("/users", usersRoutes);
router.use("/audit-log", auditLogRoutes);
router.use("/logs", logsRoutes);
router.use("/reports", reportsRoutes);
router.use("/settings", settingsRoutes);
router.use("/version", versionRoutes);
@@ -55,6 +62,11 @@ router.use("/nginx/streams", streamsRoutes);
router.use("/nginx/access-lists", accessListsRoutes);
router.use("/nginx/certificates", certificatesHostsRoutes);
// Only include CI routes if we're in a CI environment
if (isCI()) {
router.use("/ci", ciRoutes);
}
/**
* API 404 for all other routes
*
+7 -29
View File
@@ -44,18 +44,11 @@ router
},
},
{
expand:
typeof req.query.expand === "string"
? req.query.expand.split(",")
: null,
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
query: typeof req.query.query === "string" ? req.query.query : null,
},
);
const rows = await internalCertificate.getAll(
res.locals.access,
data.expand,
data.query,
);
const rows = await internalCertificate.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
@@ -70,15 +63,9 @@ router
*/
.post(async (req, res, next) => {
try {
const payload = await apiValidator(
getValidationSchema("/nginx/certificates", "post"),
req.body,
);
const payload = await apiValidator(getValidationSchema("/nginx/certificates", "post"), req.body);
req.setTimeout(900000); // 15 minutes timeout
const result = await internalCertificate.create(
res.locals.access,
payload,
);
const result = await internalCertificate.create(res.locals.access, payload);
res.status(201).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
@@ -139,16 +126,10 @@ router
*/
.post(async (req, res, next) => {
try {
const payload = await apiValidator(
getValidationSchema("/nginx/certificates/test-http", "post"),
req.body,
);
const payload = await apiValidator(getValidationSchema("/nginx/certificates/test-http", "post"), req.body);
req.setTimeout(60000); // 1 minute timeout
const result = await internalCertificate.testHttpsChallenge(
res.locals.access,
payload,
);
const result = await internalCertificate.testHttpsChallenge(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
@@ -224,10 +205,7 @@ router
},
{
certificate_id: req.params.certificate_id,
expand:
typeof req.query.expand === "string"
? req.query.expand.split(",")
: null,
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
},
);
const row = await internalCertificate.get(res.locals.access, {
+4 -2
View File
@@ -194,9 +194,11 @@ router
/**
* POST /api/nginx/dead-hosts/123/disable
*/
.post((req, res, next) => {
.post(async (req, res, next) => {
try {
const result = internalDeadHost.disable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10) });
const result = await internalDeadHost.disable(res.locals.access, {
id: Number.parseInt(req.params.host_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
+67
View File
@@ -1,4 +1,5 @@
import express from "express";
import fs from "node:fs";
import internalProxyHost from "../../internal/proxy-host.js";
import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js";
@@ -206,4 +207,70 @@ router
}
});
/**
* Proxy-host logs
*
* /api/nginx/proxy-hosts/123/logs
*/
router
.route("/:host_id/logs")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/proxy-hosts/123/logs
*
* Retrieve logs for a specific proxy-host
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["host_id"],
additionalProperties: false,
properties: {
host_id: {
$ref: "common#/properties/id",
},
type: {
type: "string",
enum: ["access", "error"],
},
},
},
{
host_id: req.params.host_id,
type: req.query.type || "access",
},
);
const hostId = Number.parseInt(data.host_id, 10);
const logType = data.type === "error" ? "error" : "access";
const logFile = `/data/logs/proxy-host-${hostId}_${logType}.log`;
// Check access permission
await res.locals.access.can("proxy_hosts:get", hostId);
let logs = "";
if (fs.existsSync(logFile)) {
const content = fs.readFileSync(logFile, { encoding: "utf8" });
const lines = content.split("\n");
// Return last 1000 lines to avoid huge payloads
const maxLines = 1000;
if (lines.length > maxLines) {
logs = lines.slice(-maxLines).join("\n");
} else {
logs = content;
}
}
res.status(200).send({ logs });
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;
+1 -1
View File
@@ -18,7 +18,7 @@ router
/**
* GET /schema
*/
.get(async (req, res) => {
.get(async (req, res, next) => {
try {
const swaggerJSON = await getCompiledSchema();
+10 -41
View File
@@ -48,18 +48,11 @@ router
},
},
{
expand:
typeof req.query.expand === "string"
? req.query.expand.split(",")
: null,
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.expand, data.query);
res.status(200).send(users);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
@@ -95,10 +88,7 @@ router
}
}
const payload = await apiValidator(
getValidationSchema("/users", "post"),
body,
);
const payload = await apiValidator(getValidationSchema("/users", "post"), body);
const user = await internalUser.create(res.locals.access, payload);
res.status(201).send(user);
} catch (err) {
@@ -169,20 +159,14 @@ router
},
{
user_id: req.params.user_id,
expand:
typeof req.query.expand === "string"
? req.query.expand.split(",")
: null,
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
},
);
const user = await internalUser.get(res.locals.access, {
id: data.user_id,
expand: data.expand,
omit: internalUser.getUserOmisionsByAccess(
res.locals.access,
data.user_id,
),
omit: internalUser.getUserOmisionsByAccess(res.locals.access, data.user_id),
});
res.status(200).send(user);
} catch (err) {
@@ -198,10 +182,7 @@ router
*/
.put(async (req, res, next) => {
try {
const payload = await apiValidator(
getValidationSchema("/users/{userID}", "put"),
req.body,
);
const payload = await apiValidator(getValidationSchema("/users/{userID}", "put"), req.body);
payload.id = req.params.user_id;
const result = await internalUser.update(res.locals.access, payload);
res.status(200).send(result);
@@ -248,10 +229,7 @@ router
*/
.put(async (req, res, next) => {
try {
const payload = await apiValidator(
getValidationSchema("/users/{userID}/auth", "put"),
req.body,
);
const payload = await apiValidator(getValidationSchema("/users/{userID}/auth", "put"), req.body);
payload.id = req.params.user_id;
const result = await internalUser.setPassword(res.locals.access, payload);
res.status(200).send(result);
@@ -281,15 +259,9 @@ router
*/
.put(async (req, res, next) => {
try {
const payload = await apiValidator(
getValidationSchema("/users/{userID}/permissions", "put"),
req.body,
);
const payload = await apiValidator(getValidationSchema("/users/{userID}/permissions", "put"), req.body);
payload.id = req.params.user_id;
const result = await internalUser.setPermissions(
res.locals.access,
payload,
);
const result = await internalUser.setPermissions(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
@@ -408,10 +380,7 @@ router
*/
.post(async (req, res, next) => {
try {
const { code } = await apiValidator(
getValidationSchema("/users/{userID}/2fa/enable", "post"),
req.body,
);
const { code } = await apiValidator(getValidationSchema("/users/{userID}/2fa/enable", "post"), req.body);
const result = await internal2FA.enable(res.locals.access, req.params.user_id, code);
res.status(200).send(result);
} catch (err) {
@@ -0,0 +1,69 @@
{
"type": "object",
"description": "Available log sources for the log viewer",
"required": ["system", "letsencrypt", "hosts"],
"additionalProperties": false,
"properties": {
"system": {
"type": "object",
"properties": {
"label": {
"type": "string",
"example": "System"
}
}
},
"letsencrypt": {
"type": "object",
"properties": {
"label": {
"type": "string",
"example": "Let's Encrypt"
}
}
},
"hosts": {
"type": "object",
"required": ["proxy", "redirection", "dead", "stream"],
"additionalProperties": false,
"properties": {
"proxy": {
"$ref": "#/$defs/host-option-list"
},
"redirection": {
"$ref": "#/$defs/host-option-list"
},
"dead": {
"$ref": "#/$defs/host-option-list"
},
"stream": {
"$ref": "#/$defs/host-option-list"
}
},
"example": {
"proxy": [{ "id": 1, "label": "example.com" }],
"redirection": [],
"dead": [],
"stream": [{ "id": 1, "label": "Port 5432 → 10.0.0.5:5432" }]
}
}
},
"$defs": {
"host-option-list": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "label"],
"properties": {
"id": {
"$ref": "../common.json#/properties/id"
},
"label": {
"type": "string",
"example": "example.com"
}
}
}
}
}
}
@@ -0,0 +1,30 @@
{
"type": "object",
"description": "The last N lines of a log source",
"required": ["lines", "size", "truncated", "exists"],
"additionalProperties": false,
"properties": {
"lines": {
"type": "array",
"items": {
"type": "string"
},
"example": ["2026-01-01T00:00:00.000Z INFO [Global ] Backend PID 1 listening on port 3000 ..."]
},
"size": {
"type": "integer",
"description": "Size in bytes of the underlying log file",
"example": 4096
},
"truncated": {
"type": "boolean",
"description": "True when the file is larger than the maximum amount of data scanned per request",
"example": false
},
"exists": {
"type": "boolean",
"description": "False when the underlying log file does not exist yet",
"example": true
}
}
}
@@ -22,7 +22,8 @@
"enabled",
"locations",
"hsts_enabled",
"hsts_subdomains"
"hsts_subdomains",
"trust_forwarded_proto"
],
"properties": {
"id": {
@@ -123,6 +124,9 @@
},
"advanced_config": {
"type": "string"
},
"access_list_id": {
"$ref": "../common.json#/properties/access_list_id"
}
}
},
@@ -131,7 +135,8 @@
"path": "/app",
"forward_scheme": "http",
"forward_host": "example.com",
"forward_port": 80
"forward_port": 80,
"access_list_id": 0
}
]
},
@@ -141,6 +146,11 @@
"hsts_subdomains": {
"$ref": "../common.json#/properties/hsts_subdomains"
},
"trust_forwarded_proto":{
"type": "boolean",
"description": "Trust the forwarded headers",
"example": false
},
"certificate": {
"oneOf": [
{
+1 -1
View File
@@ -44,7 +44,7 @@
},
{
"type": "string",
"format": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$"
"pattern": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$"
},
{
"type": "string",
@@ -0,0 +1,36 @@
{
"operationId": "getLogSources",
"summary": "Get available log sources",
"tags": ["logs"],
"security": [
{
"bearerAuth": ["admin"]
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"system": { "label": "System" },
"letsencrypt": { "label": "Let's Encrypt" },
"hosts": {
"proxy": [{ "id": 1, "label": "example.com" }],
"redirection": [],
"dead": [],
"stream": [{ "id": 1, "label": "Port 5432 → 10.0.0.5:5432" }]
}
}
}
},
"schema": {
"$ref": "../../../components/log-sources-object.json"
}
}
}
}
}
}
+100
View File
@@ -0,0 +1,100 @@
{
"operationId": "getLogTail",
"summary": "Get the last N lines of a log source",
"tags": ["logs"],
"security": [
{
"bearerAuth": ["admin"]
}
],
"parameters": [
{
"in": "query",
"name": "type",
"required": true,
"description": "Which log source to read",
"schema": {
"type": "string",
"enum": ["system", "letsencrypt", "host"]
}
},
{
"in": "query",
"name": "host_type",
"description": "Required when type=host",
"schema": {
"type": "string",
"enum": ["proxy", "redirection", "dead", "stream"]
}
},
{
"in": "query",
"name": "host_id",
"description": "Required when type=host",
"schema": {
"type": "integer",
"minimum": 1
}
},
{
"in": "query",
"name": "channel",
"description": "Required when type=host - which log file to read (not to be confused with host_type=stream)",
"schema": {
"type": "string",
"enum": ["access", "error"]
}
},
{
"in": "query",
"name": "lines",
"description": "Number of lines to return from the end of the file",
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 1000,
"default": 200
}
},
{
"in": "query",
"name": "level",
"description": "Only applicable to type=system",
"schema": {
"type": "string",
"enum": ["INFO", "WARN", "ERROR", "DEBUG", "SUCCESS", "FATAL", "COMPLETE"]
}
},
{
"in": "query",
"name": "search",
"description": "Case-insensitive plain-text search",
"schema": {
"type": "string",
"maxLength": 200
}
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"lines": ["2026-01-01T00:00:00.000Z INFO [Global ] Backend PID 1 listening on port 3000 ..."],
"size": 4096,
"truncated": false,
"exists": true
}
}
},
"schema": {
"$ref": "../../../components/log-tail-object.json"
}
}
}
}
}
}
@@ -58,7 +58,8 @@
"enabled": true,
"locations": [],
"hsts_enabled": false,
"hsts_subdomains": false
"hsts_subdomains": false,
"trust_forwarded_proto": false
}
]
}
@@ -56,6 +56,7 @@
"locations": [],
"hsts_enabled": false,
"hsts_subdomains": false,
"trust_forwarded_proto": false,
"owner": {
"id": 1,
"created_on": "2025-10-28T00:50:24.000Z",
@@ -56,6 +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"
},
@@ -122,6 +125,7 @@
"locations": [],
"hsts_enabled": false,
"hsts_subdomains": false,
"trust_forwarded_proto": false,
"owner": {
"id": 1,
"created_on": "2025-10-28T00:50:24.000Z",
@@ -48,6 +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"
},
@@ -119,6 +122,7 @@
"locations": [],
"hsts_enabled": false,
"hsts_subdomains": false,
"trust_forwarded_proto": false,
"certificate": null,
"owner": {
"id": 1,
+14
View File
@@ -24,6 +24,10 @@
"name": "audit-log",
"description": "Endpoints related to Audit Logs"
},
{
"name": "logs",
"description": "Endpoints for viewing system, Let's Encrypt and per-host logs"
},
{
"name": "access-lists",
"description": "Endpoints related to Access Lists"
@@ -81,6 +85,16 @@
"$ref": "./paths/audit-log/id/get.json"
}
},
"/logs/sources": {
"get": {
"$ref": "./paths/logs/sources/get.json"
}
},
"/logs/tail": {
"get": {
"$ref": "./paths/logs/tail/get.json"
}
},
"/nginx/access-lists": {
"get": {
"$ref": "./paths/nginx/access-lists/get.json"
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env node
import * as process from "node:process"; // Use the node: protocol for built-ins
import internalNginx from "../internal/nginx.js";
import { global as logger } from "../logger.js";
import deadHostModel from "../models/dead_host.js";
import proxyHostModel from "../models/proxy_host.js";
import redirectionHostModel from "../models/redirection_host.js";
import streamModel from "../models/stream.js";
const args = process.argv.slice(2);
const UNATTENDED = args.includes("-y") || args.includes("--yes");
const DRY_RUN = args.includes("--dry-run");
if (args.includes("--help") || args.includes("-h")) {
console.log("\nThis will iterate over all Hosts and regnerate their Nginx configs.\n")
console.log("Usage: ./regenerate-config [-h|--help] [-y|--yes] [--dry-run]\n");
process.exit(0);
}
// ask for the user to confirm the action if not in unattended mode
if (!UNATTENDED && !DRY_RUN) {
const readline = await import("node:readline");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const question = (query) =>
new Promise((resolve) => rl.question(query, resolve));
const answer = await question(
"This will iterate over all Hosts and regnerate their Nginx configs.\n\nAre you sure you want to proceed? (y/N) ",
);
rl.close();
if (answer.toLowerCase() !== "y") {
console.log("Aborting.");
process.exit(0);
}
}
const logIt = (msg, type = "info") => logger[type](
`${DRY_RUN ? '[DRY RUN] ' : ''}${msg}`,
);
// Let's do it.
const processItems = async (model, type, hostType) => {
const rows = await model
.query()
.where("is_deleted", 0)
.andWhere("enabled", 1)
.groupBy("id")
.allowGraph(model.defaultAllowGraph)
.withGraphFetched(`[${model.defaultExpand.join(", ")}]`)
.orderBy(...model.defaultOrder);
logIt(`[${type}] Found ${rows.length} rows to process...`);
for (const row of rows) {
if (!DRY_RUN) {
logIt(`[${type}] Regenerating config #${row.id}: ${row.domain_names ? row.domain_names.join(", ") : 'port ' + row.incoming_port}`);
await internalNginx.configure(model, hostType, row);
} else {
logIt(`[${type}] Skipping generation of config #${row.id}: ${row.domain_names ? row.domain_names.join(", ") : 'port ' + row.incoming_port}`);
}
}
};
await processItems(proxyHostModel, "Proxy Host", "proxy_host");
await processItems(redirectionHostModel, "Redirection Host", "redirection_host");
await processItems(deadHostModel, "404 Host", "dead_host");
await processItems(streamModel, "Stream", "stream");
logIt("Completed", "success");
process.exit(0);
+18 -32
View File
@@ -6,11 +6,12 @@ import certificateModel from "./models/certificate.js";
import settingModel from "./models/setting.js";
import userModel from "./models/user.js";
import userPermissionModel from "./models/user_permission.js";
import fs from "node:fs/promises";
export const isSetup = async () => {
const row = await userModel.query().select("id").where("is_deleted", 0).first();
return row?.id > 0;
}
};
/**
* Creates a default admin users if one doesn't already exist in the database
@@ -44,13 +45,9 @@ const setupDefaultUser = async () => {
roles: ["admin"],
};
const user = await userModel
.query()
.insertAndFetch(data);
const user = await userModel.query().insertAndFetch(data);
await authModel
.query()
.insert({
await authModel.query().insert({
user_id: user.id,
type: "password",
secret: initialAdminPassword,
@@ -77,16 +74,10 @@ const setupDefaultUser = async () => {
* @returns {Promise}
*/
const setupDefaultSettings = async () => {
const row = await settingModel
.query()
.select("id")
.where({ id: "default-site" })
.first();
const row = await settingModel.query().select("id").where({ id: "default-site" }).first();
if (!row?.id) {
await settingModel
.query()
.insert({
await settingModel.query().insert({
id: "default-site",
name: "Default Site",
description: "What to show when Nginx is hit with an unknown Host",
@@ -103,14 +94,10 @@ const setupDefaultSettings = async () => {
* @returns {Promise}
*/
const setupCertbotPlugins = async () => {
const certificates = await certificateModel
.query()
.where("is_deleted", 0)
.andWhere("provider", "letsencrypt");
const certificates = await certificateModel.query().where("is_deleted", 0).andWhere("provider", "letsencrypt");
if (certificates?.length) {
const plugins = [];
const promises = [];
certificates.map((certificate) => {
if (certificate.meta && certificate.meta.dns_challenge === true) {
@@ -118,24 +105,23 @@ const setupCertbotPlugins = async () => {
plugins.push(certificate.meta.dns_provider);
}
// Make sure credentials file exists
const credentials_loc = `/etc/letsencrypt/credentials/credentials-${certificate.id}`;
// Escape single quotes and backslashes
if (typeof certificate.meta.dns_provider_credentials === "string") {
const escapedCredentials = certificate.meta.dns_provider_credentials
.replaceAll("'", "\\'")
.replaceAll("\\", "\\\\");
const credentials_cmd = `[ -f '${credentials_loc}' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo '${escapedCredentials}' > '${credentials_loc}' && chmod 600 '${credentials_loc}'; }`;
promises.push(utils.exec(credentials_cmd));
}
// Deliberately does NOT write the DNS credentials file here any more.
//
// It used to, so that a later `certbot renew` would find the path recorded in its
// renewal config. The effect was that every backend restart rewrote a plaintext
// DNS provider API token for every DNS-01 certificate, and left it there.
//
// internalCertificate now writes that file immediately before it runs certbot and
// removes it again afterwards, so there is exactly one writer and the credential
// is on disk only for the length of a certbot run. Recreating the files at boot
// would put every one of them straight back.
}
return true;
});
await installPlugins(plugins);
if (promises.length) {
await Promise.all(promises);
if (plugins.length) {
logger.info(`Added Certbot plugins ${plugins.join(", ")}`);
}
}
+2
View File
@@ -10,11 +10,13 @@
{% endif %}
{% if access_list.clients.length > 0 %}
# Access Rules: {{ access_list.clients | size }} total
{% for client in access_list.clients %}
{{client | nginxAccessRule}}
{% endfor %}
deny all;
{% endif %}
# Access checks must...
{% if access_list.satisfy_any == 1 or access_list.satisfy_any == true %}
+5
View File
@@ -1,6 +1,11 @@
{% if certificate and certificate_id > 0 -%}
{% if ssl_forced == 1 or ssl_forced == true %}
# Force SSL
{% if trust_forwarded_proto == true %}
set $trust_forwarded_proto "T";
{% else %}
set $trust_forwarded_proto "F";
{% endif %}
include conf.d/include/force-ssl.conf;
{% endif %}
{% endif %}
+4 -4
View File
@@ -5,8 +5,8 @@
{% if enabled %}
{% if tcp_forwarding == 1 or tcp_forwarding == true -%}
server {
listen {{ incoming_port }} {%- if certificate %} ssl {%- endif %};
{% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} {%- if certificate %} ssl {%- endif %};
listen {{ incoming_port }} reuseport {%- if certificate %} ssl {%- endif %};
{% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} reuseport {%- if certificate %} ssl {%- endif %};
{%- include "_certificates_stream.conf" %}
@@ -23,8 +23,8 @@ server {
{% if udp_forwarding == 1 or udp_forwarding == true -%}
server {
listen {{ incoming_port }} udp;
{% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} udp;
listen {{ incoming_port }} udp reuseport;
{% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} udp reuseport;
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
+663 -1196
View File
File diff suppressed because it is too large Load Diff
+8 -5
View File
@@ -3,8 +3,9 @@
# This file assumes that the frontend has been built using ./scripts/frontend-build
ARG BASE_IMAGE=nginxproxymanager/nginx-full:certbot-node
FROM nginxproxymanager/testca AS testca
FROM nginxproxymanager/nginx-full:certbot-node
FROM $BASE_IMAGE
ARG TARGETPLATFORM
ARG BUILD_VERSION
@@ -38,15 +39,17 @@ EXPOSE 80 81 443
COPY backend /app
COPY frontend/dist /app/frontend
COPY docker/rootfs /
# Copy test CA cert for use in container and add to trusted certs from a self-signed CA
COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
WORKDIR /etc/ssl/certs
RUN ln -s NginxProxyManager.crt 1d0e3f10.0 && update-ca-certificates
WORKDIR /app
RUN yarn install \
&& yarn cache clean
# add late to limit cache-busting by modifications
COPY docker/rootfs /
COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
# Remove frontend service not required for prod, dev nginx config as well
RUN rm -rf /etc/s6-overlay/s6-rc.d/user/contents.d/frontend /etc/nginx/conf.d/dev.conf \
&& chmod 644 /etc/logrotate.d/nginx-proxy-manager
+8 -4
View File
@@ -20,19 +20,23 @@ RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \
# Task
WORKDIR /usr
RUN curl -sL https://taskfile.dev/install.sh | sh
WORKDIR /root
RUN curl -sL 'https://taskfile.dev/install.sh' | sh
COPY rootfs /
COPY scripts/install-s6 /tmp/install-s6
RUN rm -f /etc/nginx/conf.d/production.conf \
RUN rm -f /etc/nginx/conf.d/production.conf.template \
&& chmod 644 /etc/logrotate.d/nginx-proxy-manager \
&& /tmp/install-s6 "${TARGETPLATFORM}" \
&& rm -f /tmp/install-s6 \
&& chmod 644 -R /root/.cache
# Certs for testing purposes
# Copy test CA cert for use in container and add to trusted certs from a self-signed CA
COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
WORKDIR /etc/ssl/certs
RUN ln -s NginxProxyManager.crt 1d0e3f10.0 && update-ca-certificates
WORKDIR /root
EXPOSE 80 81 443
ENTRYPOINT [ "/init" ]
+1
View File
@@ -28,6 +28,7 @@ acl SSL_ports port 443
acl Safe_ports port 80 # http
acl Safe_ports port 81
acl Safe_ports port 443 # https
acl Safe_ports port 8000 # for CI testing with custom admin port
#
# Recommended minimum Access Permission configuration:
+13
View File
@@ -0,0 +1,13 @@
# WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production.
services:
cypress:
environment:
CYPRESS_stack: "sqlite"
command: cypress run --browser chrome --config-file=cypress/config/ci.mjs --expose grepTags="@isolated"
fullstack:
environment:
DB_SQLITE_FILE: "/data/mydb.sqlite"
PUID: 1000
PGID: 1000
DISABLE_IPV6: "true"
+4
View File
@@ -3,6 +3,7 @@ services:
cypress:
environment:
CYPRESS_stack: "postgres"
NPM_ADMIN_PORT: 8000
fullstack:
environment:
@@ -11,11 +12,14 @@ services:
DB_POSTGRES_USER: "npm"
DB_POSTGRES_PASSWORD: "npmpass"
DB_POSTGRES_NAME: "npm"
NPM_ADMIN_PORT: 8000
depends_on:
- db-postgres
- authentik
- authentik-worker
- authentik-ldap
expose:
- "8000/tcp"
db-postgres:
image: postgres:17
+5 -2
View File
@@ -1,9 +1,12 @@
# WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production.
services:
cypress:
environment:
CYPRESS_stack: "sqlite"
fullstack:
environment:
DB_SQLITE_FILE: '/data/mydb.sqlite'
DB_SQLITE_FILE: "/data/mydb.sqlite"
PUID: 1000
PGID: 1000
DISABLE_IPV6: 'true'
DISABLE_IPV6: "true"
+13 -2
View File
@@ -38,8 +38,18 @@ services:
- website2.example.com
- website3.example.com
examplesite:
image: "${IMAGE}-examplesite:ci-${BUILD_NUMBER}"
build:
context: ../test/docker
dockerfile: Dockerfile.website
expose:
- "80/tcp"
networks:
- fulltest
stepca:
image: jc21/testca
image: nginxproxymanager/testca
volumes:
- "./dev/resolv.conf:/etc/resolv.conf:ro"
- "/etc/localtime:/etc/localtime:ro"
@@ -105,11 +115,12 @@ services:
environment:
HTTP_PROXY: "squid:3128"
HTTPS_PROXY: "squid:3128"
NPM_ADMIN_PORT: 81
volumes:
- "cypress_logs:/test/results"
- "./dev/resolv.conf:/etc/resolv.conf:ro"
- "/etc/localtime:/etc/localtime:ro"
command: cypress run --browser chrome --config-file=cypress/config/ci.mjs
command: cypress run --browser chrome --config-file=cypress/config/ci.mjs --expose grepTags="-@isolated"
networks:
- fulltest
+1 -1
View File
@@ -94,7 +94,7 @@ services:
- pgdb.internal
stepca:
image: jc21/testca
image: nginxproxymanager/testca
container_name: npm2dev.stepca
volumes:
- "./dev/resolv.conf:/etc/resolv.conf:ro"
@@ -25,3 +25,13 @@
kill -USR1 `cat /run/nginx/nginx.pid 2>/dev/null` 2>/dev/null || true
endscript
}
/data/logs/backend.log {
su npm npm
size 10M
rotate 5
missingok
notifempty
compress
copytruncate
}
@@ -5,9 +5,28 @@ if ($scheme = "http") {
if ($request_uri = /.well-known/acme-challenge/test-challenge) {
set $test "${test}T";
}
# Check if the ssl staff has been handled
set $test_ssl_handled "";
if ($trust_forwarded_proto = "") {
set $trust_forwarded_proto "F";
}
if ($trust_forwarded_proto = "T") {
set $test_ssl_handled "${test_ssl_handled}T";
}
if ($http_x_forwarded_proto = "https") {
set $test_ssl_handled "${test_ssl_handled}S";
}
if ($http_x_forwarded_scheme = "https") {
set $test_ssl_handled "${test_ssl_handled}S";
}
if ($test_ssl_handled = "TSS") {
set $test_ssl_handled "TS";
}
if ($test_ssl_handled = "TS") {
set $test "${test}S";
}
if ($test = H) {
return 301 https://$host$request_uri;
}
@@ -1,7 +1,7 @@
add_header X-Served-By $host;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Scheme $x_forwarded_scheme;
proxy_set_header X-Forwarded-Proto $x_forwarded_proto;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass $forward_scheme://$server:$port$request_uri;
@@ -1,7 +1,7 @@
# Admin Interface
server {
listen 81 default;
listen [::]:81 default;
listen {{NPM_ADMIN_PORT}} default;
listen [::]:{{NPM_ADMIN_PORT}} default;
server_name nginxproxymanager;
root /app/frontend;
+16 -4
View File
@@ -47,16 +47,28 @@ http {
proxy_cache_path /var/lib/nginx/cache/private levels=1:2 keys_zone=private-cache:5m max_size=1024m;
# Log format and fallback log file
include /etc/nginx/conf.d/include/log-proxy.conf;
include /etc/nginx/conf.d/include/log-proxy[.]conf;
# Dynamically generated resolvers file
include /etc/nginx/conf.d/include/resolvers.conf;
include /etc/nginx/conf.d/include/resolvers[.]conf;
# Default upstream scheme
map $host $forward_scheme {
default http;
}
# Handle upstream X-Forwarded-Proto and X-Forwarded-Scheme header
map $http_x_forwarded_proto $x_forwarded_proto {
"http" "http";
"https" "https";
default $scheme;
}
map $http_x_forwarded_scheme $x_forwarded_scheme {
"http" "http";
"https" "https";
default $scheme;
}
# Real IP Determination
# Local subnets:
@@ -64,7 +76,7 @@ http {
set_real_ip_from 172.16.0.0/12; # Includes Docker subnet
set_real_ip_from 192.168.0.0/16;
# NPM generated CDN ip ranges:
include conf.d/include/ip_ranges.conf;
include conf.d/include/ip_ranges[.]conf;
# always put the following 2 lines after ip subnets:
real_ip_header X-Real-IP;
real_ip_recursive on;
@@ -86,7 +98,7 @@ http {
stream {
# Log format and fallback log file
include /etc/nginx/conf.d/include/log-stream.conf;
include /etc/nginx/conf.d/include/log-stream[.]conf;
# Files generated by NPM
include /data/nginx/stream/*.conf;
@@ -11,11 +11,11 @@ log_info 'Starting backend ...'
if [ "${DEVELOPMENT:-}" = 'true' ]; then
s6-setuidgid "$PUID:$PGID" yarn install
exec s6-setuidgid "$PUID:$PGID" bash -c "export HOME=$NPMHOME;node --max_old_space_size=250 --abort_on_uncaught_exception node_modules/nodemon/bin/nodemon.js"
exec s6-setuidgid "$PUID:$PGID" bash -c "export HOME=$NPMHOME;export CERTBOT_VERSION=$CERTBOT_VERSION;node --max_old_space_size=250 --abort_on_uncaught_exception node_modules/nodemon/bin/nodemon.js"
else
while :
do
s6-setuidgid "$PUID:$PGID" bash -c "export HOME=$NPMHOME;node --abort_on_uncaught_exception --max_old_space_size=250 index.js"
s6-setuidgid "$PUID:$PGID" bash -c "export HOME=$NPMHOME;export CERTBOT_VERSION=$CERTBOT_VERSION;node --abort_on_uncaught_exception --max_old_space_size=250 index.js"
sleep 1
done
fi
@@ -17,6 +17,7 @@ fi
. /etc/s6-overlay/s6-rc.d/prepare/20-paths.sh
. /etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh
. /etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh
. /etc/s6-overlay/s6-rc.d/prepare/45-admin-port.sh
. /etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh
. /etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh
. /etc/s6-overlay/s6-rc.d/prepare/90-banner.sh
@@ -46,6 +46,12 @@ for loc in "${locations[@]}"; do
chownit "$loc"
done
# Ensure the JWT key file is owned by the runtime user, even when the /data
# directory ownership already matches PUID:PGID (chownit skips recursion then)
if [ -f /data/keys.json ]; then
chown "$PUID:$PGID" /data/keys.json
fi
if [ "$(is_true "${SKIP_CERTBOT_OWNERSHIP:-}")" = '1' ]; then
log_info 'Skipping ownership change of certbot directories'
else
@@ -7,8 +7,10 @@ log_info 'Dynamic resolvers ...'
# Dynamically generate resolvers file, if resolver is IPv6, enclose in `[]`
# thanks @tfmm
if [ "$(is_true "$DISABLE_IPV6")" = '1' ]; then
if [ "$(is_true "${DISABLE_RESOLVER:-}")" = '0' ]; then
if [ "$(is_true "${DISABLE_IPV6:-}")" = '1' ]; then
echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) ipv6=off valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf
else
echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf
fi
fi
@@ -0,0 +1,30 @@
#!/command/with-contenv bash
# shellcheck shell=bash
# This command reads the `NPM_ADMIN_PORT` env var and will fall
# back to 81 if this is not set or is not a number.
set -e
log_info 'Admin Port ...'
NPM_ADMIN_PORT="${NPM_ADMIN_PORT:-81}"
# ensure admin port is a number
if ! [[ "$NPM_ADMIN_PORT" =~ ^[0-9]+$ ]]; then
echo "WARNING: NPM_ADMIN_PORT must be a number. Defaulting to 81" >&2
NPM_ADMIN_PORT=81
fi
PRODFILE="/etc/nginx/conf.d/production.conf"
SED_REGEX="s/\{\{NPM_ADMIN_PORT\}\}/${NPM_ADMIN_PORT}/g"
if is_mounted "$PRODFILE"; then
echo "WARNING: skipping ${PRODFILE} — mounted file" >&2
elif [ -f "$PRODFILE.template" ]; then
if sed -E "$SED_REGEX" "$PRODFILE.template" > "$PRODFILE" && [ -s "$PRODFILE" ]; then
# success
log_info "Generated ${PRODFILE} from template"
else
log_fatal "Failed to generate ${PRODFILE} from template"
fi
fi
@@ -12,7 +12,7 @@ process_folder () {
FILES=$(find "$1" -type f -name "*.conf")
SED_REGEX=
if [ "$(is_true "$DISABLE_IPV6")" = '1' ]; then
if [ "$(is_true "${DISABLE_IPV6:-}")" = '1' ]; then
# IPV6 is disabled
echo "Disabling IPV6 in hosts in: $1"
SED_REGEX='s/^([^#]*)listen \[::\]/\1#listen [::]/g'
@@ -25,7 +25,16 @@ process_folder () {
for FILE in $FILES
do
echo "- ${FILE}"
echo "$(sed -E "$SED_REGEX" "$FILE")" > $FILE
TMPFILE="${FILE}.tmp"
if is_mounted "${FILE}"; then
echo "WARNING: skipping ${FILE} — mounted file" >&2
elif sed -E "$SED_REGEX" "$FILE" > "$TMPFILE" && [ -s "$TMPFILE" ]; then
mv "$TMPFILE" "$FILE"
else
echo "WARNING: skipping ${FILE} — sed produced empty output" >&2
rm -f "$TMPFILE"
fi
done
# ensure the files are still owned by the npm user
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash
OK=$(curl --silent http://127.0.0.1:81/api/ | jq --raw-output '.status')
OK=$(curl --silent "http://127.0.0.1:${NPM_ADMIN_PORT:-81}/api/" | jq --raw-output '.status')
if [ "$OK" == "OK" ]; then
echo "OK"
+8
View File
@@ -21,6 +21,10 @@ NPMGROUP=npm
NPMHOME=/tmp/npmuserhome
export NPMUSER NPMGROUP NPMHOME
# Query the certbot version just once and use it elsewhere
CERTBOT_VERSION="$(certbot --version | grep -Eo '[0-9](\.[0-9]+)+')"
export CERTBOT_VERSION
if [[ "$PUID" -ne '0' ]] && [ "$PGID" = '0' ]; then
# set group id to same as user id,
# the user probably forgot to specify the group id and
@@ -66,3 +70,7 @@ is_true () {
echo '0'
fi
}
is_mounted() {
awk -v p="$1" '$5 == p { found=1 } END { exit !found }' /proc/self/mountinfo
}
+1 -5
View File
@@ -8,7 +8,7 @@ BLUE='\E[1;34m'
GREEN='\E[1;32m'
RESET='\E[0m'
S6_OVERLAY_VERSION=3.2.1.0
S6_OVERLAY_VERSION=3.2.3.0
TARGETPLATFORM=${1:-linux/amd64}
# Determine the correct binary file for the architecture given
@@ -17,10 +17,6 @@ case $TARGETPLATFORM in
S6_ARCH=aarch64
;;
linux/arm/v7)
S6_ARCH=armhf
;;
*)
S6_ARCH=x86_64
;;
+75 -30
View File
@@ -1,4 +1,4 @@
import { defineConfig, type DefaultTheme } from 'vitepress';
import { defineConfig } from "vitepress";
// https://vitepress.dev/reference/site-config
export default defineConfig({
@@ -6,56 +6,101 @@ export default defineConfig({
description: "Expose your services easily and securely",
head: [
["link", { rel: "icon", href: "/icon.png" }],
["meta", { name: "description", content: "Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt" }],
[
"meta",
{
name: "description",
content:
"Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt",
},
],
["meta", { property: "og:title", content: "Nginx Proxy Manager" }],
["meta", { property: "og:description", content: "Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt"}],
[
"meta",
{
property: "og:description",
content:
"Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt",
},
],
["meta", { property: "og:type", content: "website" }],
["meta", { property: "og:url", content: "https://nginxproxymanager.com/" }],
["meta", { property: "og:image", content: "https://nginxproxymanager.com/icon.png" }],
[
"meta",
{
property: "og:image",
content: "https://nginxproxymanager.com/icon.png",
},
],
["meta", { name: "twitter:card", content: "summary" }],
["meta", { name: "twitter:title", content: "Nginx Proxy Manager" }],
["meta", { name: "twitter:description", content: "Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt"}],
["meta", { name: "twitter:image", content: "https://nginxproxymanager.com/icon.png"}],
[
"meta",
{
name: "twitter:description",
content:
"Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt",
},
],
[
"meta",
{
name: "twitter:image",
content: "https://nginxproxymanager.com/icon.png",
},
],
["meta", { name: "twitter:alt", content: "Nginx Proxy Manager" }],
// GA
['script', { async: 'true', src: 'https://www.googletagmanager.com/gtag/js?id=G-TXT8F5WY5B'}],
['script', {}, "window.dataLayer = window.dataLayer || [];\nfunction gtag(){dataLayer.push(arguments);}\ngtag('js', new Date());\ngtag('config', 'G-TXT8F5WY5B');"],
[
"script",
{
async: "true",
src: "https://www.googletagmanager.com/gtag/js?id=G-TXT8F5WY5B",
},
],
[
"script",
{},
"window.dataLayer = window.dataLayer || [];\nfunction gtag(){dataLayer.push(arguments);}\ngtag('js', new Date());\ngtag('config', 'G-TXT8F5WY5B');",
],
],
sitemap: {
hostname: 'https://nginxproxymanager.com'
hostname: "https://nginxproxymanager.com",
},
metaChunk: true,
srcDir: './src',
outDir: './dist',
srcDir: "./src",
outDir: "./dist",
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
logo: { src: '/logo.svg', width: 24, height: 24 },
nav: [
{ text: 'Setup', link: '/setup/' },
],
logo: { src: "/logo.svg", width: 24, height: 24 },
nav: [{ text: "Setup", link: "/setup/" }],
sidebar: [
{
items: [
// { text: 'Home', link: '/' },
{ text: 'Guide', link: '/guide/' },
{ text: 'Screenshots', link: '/screenshots/' },
{ text: 'Setup Instructions', link: '/setup/' },
{ text: 'Advanced Configuration', link: '/advanced-config/' },
{ text: 'Upgrading', link: '/upgrading/' },
{ text: 'Frequently Asked Questions', link: '/faq/' },
{ text: 'Third Party', link: '/third-party/' },
]
}
{ text: "Guide", link: "/guide/" },
{ text: "Screenshots", link: "/screenshots/" },
{ text: "Setup Instructions", link: "/setup/" },
{ text: "Advanced Configuration", link: "/advanced-config/" },
{ text: "Upgrading", link: "/upgrading/" },
{ text: "Frequently Asked Questions", link: "/faq/" },
{ text: "Certbot", link: "/certbot/" },
{ text: "Third Party", link: "/third-party/" },
],
},
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/NginxProxyManager/nginx-proxy-manager' }
{
icon: "github",
link: "https://github.com/NginxProxyManager/nginx-proxy-manager",
},
],
search: {
provider: 'local'
provider: "local",
},
footer: {
message: 'Released under the MIT License.',
copyright: 'Copyright © 2016-present jc21.com'
}
}
message: "Released under the MIT License.",
copyright: "Copyright © 2016-present jc21.com",
},
},
});
+2 -1
View File
@@ -2,7 +2,8 @@
"scripts": {
"dev": "vitepress dev --host",
"build": "vitepress build",
"preview": "vitepress preview"
"preview": "vitepress preview",
"set-version": "./scripts/set-version.sh"
},
"devDependencies": {
"vitepress": "^1.6.4"
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
set -euf
# this script accepts a version number as an argument
# and replaces {{VERSION}} in src/*.md with the provided version number.
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <version>"
exit 1
fi
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$DIR/.." || exit 1
VERSION="$1"
# find all .md files in src/ and replace {{VERSION}} with the provided version number
find src/ -type f -name "*.md" -exec sed -i "s/{{VERSION}}/$VERSION/g" {} \;
+38 -10
View File
@@ -14,7 +14,7 @@ on the `data` and `letsencrypt` folders at startup.
```yml
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
image: 'jc21/nginx-proxy-manager:{{VERSION}}'
environment:
PUID: 1000
PGID: 1000
@@ -101,7 +101,7 @@ secrets:
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
image: 'jc21/nginx-proxy-manager:{{VERSION}}'
restart: unless-stopped
ports:
# Public HTTP Port:
@@ -130,18 +130,16 @@ services:
- db
db:
image: jc21/mariadb-aria
image: 'linuxserver/mariadb'
restart: unless-stopped
environment:
# MYSQL_ROOT_PASSWORD: "npm" # use secret instead
MYSQL_ROOT_PASSWORD__FILE: /run/secrets/DB_ROOT_PWD
MYSQL_DATABASE: "npm"
MYSQL_USER: "npm"
# MYSQL_PASSWORD: "npm" # use secret instead
MYSQL_DATABASE: 'npm'
MYSQL_USER: 'npm'
MYSQL_PASSWORD__FILE: /run/secrets/MYSQL_PWD
MARIADB_AUTO_UPGRADE: '1'
TZ: 'Australia/Brisbane'
volumes:
- ./mysql:/var/lib/mysql
- ./mariadb:/config
secrets:
- DB_ROOT_PWD
- MYSQL_PWD
@@ -233,8 +231,38 @@ load_module /usr/lib/nginx/modules/ngx_stream_geoip2_module.so;
Setting these environment variables will create the default user on startup, skipping the UI first user setup screen:
```
```yml
environment:
INITIAL_ADMIN_EMAIL: my@example.com
INITIAL_ADMIN_PASSWORD: mypassword1
```
## Disable Nginx Resolver
On startup, we generate a resolvers directive for Nginx unless this is defined:
```yml
environment:
DISABLE_RESOLVER: true
```
In this configuration, all DNS queries performed by Nginx will fall to the `/etc/hosts` file
and then the `/etc/resolv.conf`.
## Changing the Admin UI port from 81 to something else
First, add an env var to your docker compose file:
```yml
environment:
NPM_ADMIN_PORT: '8000'
```
And you'll probably want to expose that port as well
```yml
ports:
- '8000:8000'
```
Then you'll be able to access admin UI at `http://localhost:8000`

Some files were not shown because too many files have changed in this diff Show More