Compare commits

...
889 Commits
Author SHA1 Message Date
Jamie Curnow 66ab4b24d1 Stop non-admins injecting nginx directives through host fields
advanced_config is admin-only, but other fields are written into nginx
config as-is and the schema barely limited them. A non-admin could add
any directive through them:

- location path, forward_host and forward_path (any non-empty string)
- proxy host forward_host (a quote breaks out of set $server "...")
- redirection forward_domain_name and stream forwarding_host (their
  pattern allowed ";", e.g. "example.com; return 200; #")
- access list IPv6 addresses ("%.+" zone id allowed "::1%x; allow all")
- domain names (a newline got out of the header comment)

Validation
- New lib/nginx-values.js holds the rules. Values can't contain
  characters that end or change an nginx token: whitespace, control
  characters, ; { } " ' # \, and no $ in a host. Location paths allow a
  modifier and an unquoted or double-quoted URI, so =, ~, ~* and ^~
  locations and quoted regexes keep working.
- The API schemas use the same patterns, including the certificate
  schema's own copy of the domain rule. A test fails if they drift.
- generateConfig checks stored values again, so rows saved before these
  rules can't inject either. The check is about nginx structure only:
  a stored value the API would now reject, but which can't add
  directives, still renders. If a value is unsafe, nothing is written
  and the host is marked offline with the reason in nginx_err. The same
  check covers per-location access lists and the Let's Encrypt request
  config.
- htpasswd user names can't contain ":" or control characters, which
  corrupted the file.

Fixes along the way
- IPv6 client addresses: drop zone ids, which nginx's allow/deny
  rejects anyway, and fix broken escapes (literal "d" for digits, an
  unescaped ".").
- Rewrite the domain rule used by redirection and stream hosts so it
  can't backtrack exponentially. It accepts the same values.
- Pattern errors still say "must match pattern", but no longer quote
  the regex.

Tests
- backend/test/nginx-values.test.js, run by `yarn test` and in
  scripts/ci/test-and-build. Real request schemas reject an injection
  set and accept valid values on every endpoint. Accepted values,
  rendered through the real templates and parsed using nginx's
  tokenizer rules, keep the same statement structure. The check
  refuses unsafe stored rows.
- NginxValues.cy.js: injection attempts from a non-admin get 400, and
  hosts using valid edge-case values come up with nginx_online true.
2026-11-10 08:20:02 +10:00
Jamie Curnow 5b7e34eb67 Fix second round of review findings from PR #6029
Security
- Check advanced_config permissions before creating a certificate on
  host update (proxy, redirection and 404 hosts). A refused request
  previously requested a Let's Encrypt certificate first, leaving an
  orphan certificate and using up rate limit.
- Only trust X-NPM-Peer-Addr when it comes with a secret generated at
  container start. The admin nginx sends both headers. Any proxy host or
  stream forwarding to 127.0.0.1:3000 was also a loopback peer, so it
  could forge the header and get a fresh rate-limit bucket on every
  request. Without the secret (dev, or a mounted production.conf), the
  header is ignored.
- Add a login limit per IP across all accounts (30 failed attempts a
  minute) alongside the per-account one. Before this, one address could
  spray passwords over any number of accounts. Behind a shared proxy,
  everyone using that proxy shares this limit.
- Stop trusting X-Real-IP from loopback. It was only there for an older
  version of the rate limiter, and it let a stream host forwarding to a
  local proxy host forge the client IP and get past IP access lists.
  Loopback now behaves as it does on master.

Correctness
- Refuse to delete a certificate that hosts still use (400, with counts
  per host type). Their configs include its files, and a successful
  revoke deleted them anyway, which broke nginx -t. Otherwise the
  certificate is revoked and its files are removed even if the revoke
  fails.
- Let non-admins clear a location's advanced config. Deleting the
  location was already allowed and has the same effect, and a location
  deleted and re-added at the same path arrives with an empty config,
  which was refused.
- Clear proxy host grants (meta.proxy_host_ids) when visibility isn't
  "user" or proxy hosts are hidden, and drop ids of deleted hosts.
  Before, hidden grants came back when proxy hosts were shown again.
  The backend enforces this; the permissions modal also stops sending
  them.
- 404 host and redirection host modals now wait for the current user
  to load, so an admin's advanced config change isn't dropped if they
  submit before it loads.

Tests
- NonAdminHosts.cy.js: refused change doesn't create a certificate,
  location rename and clear rules, grant clearing.
- Certificates.cy.js: deleting a certificate that is in use.
2026-11-09 23:39:26 +10:00
Jamie Curnow 04c1f5ae76 Fix review findings from PR #6029
Security
- Redact certbot output before it reaches the API: strip the
  certificate's DNS credential values, URL query strings and userinfo,
  secret-looking key=value pairs and Bearer/Basic tokens. DNS plugins
  can echo request URLs carrying API keys. The raw CommandError is no
  longer attached as `previous`, since the error handler returns
  `previous` to the client on certificate routes.
- Rate limit login/2FA on the admin nginx's real TCP peer, passed as
  X-NPM-Peer-Addr ($realip_remote_addr), instead of X-Forwarded-For.
  nginx trusts X-Real-IP from private ranges, so a client on the LAN or
  Docker network could get a fresh bucket on every request. Behind
  another proxy, limits now apply per account through that proxy.
- Bound rate limiter memory: hash keys over 128 chars and cap each store
  at 50k entries, removing expired entries first and then the oldest.

Correctness
- Proxy host modal: send location advanced configs unchanged for
  non-admins instead of looking them up by path, which dropped the
  config from a renamed location.
- Location advanced config: locations that omit advanced_config now get
  a config only after explicit ones are matched, so a new location at
  an old path can't take the config from the renamed location.
- Certificate upload: check that the certificate matches the private
  key (uploaded or stored) before writing, so a mismatched pair can't
  break nginx -t.
- Certificate delete: keep the Let's Encrypt files when revoke fails and
  hosts still use the certificate, so their configs still load.

Cleanup
- Make the rate-limit certbot hint first in the list, as its comment
  says.
- Proxy host getCount uses the permissions access has already loaded
  instead of querying user_permission again.
- Rate limiter: register the success refund listener in one place and
  call it a fixed window, not a sliding one.
2026-11-09 22:53:53 +10:00
Jamie Curnow 0792a5e24f Linting fixes 2026-11-09 22:33:10 +10:00
Jamie Curnow 779927c995 Fix getHostsWithDomains eager loading unnecessarily 2026-11-09 22:28:50 +10:00
Jamie Curnow 87b77d9461 Optimise isAdmin lookups 2026-11-09 22:25:07 +10:00
Jamie Curnow 8342713d45 Fix a non-admin can now rename a location that has admin-written
advanced config, and still can't change or add advanced config
2026-11-09 22:21:40 +10:00
Jamie Curnow 4175cf66c9 Fix: deleting a certificate now returns success even when some of its files
can't be removed
2026-11-09 22:17:07 +10:00
Jamie Curnow af54d2e346 Fix nginx reloading too much when cert renewal for multiple certs 2026-11-09 22:15:17 +10:00
Jamie Curnow 9b1566aa6b Fix for the broad Cloudflare hint regex (bare numbers
6003/6111/9103/9109/10000, plus 'Authentication error' and 'Invalid
access token') is checked before the rate-limit, DNS and HTTP hints, so
non-Cloudflare failures get a misleading Cloudflare message.
2026-11-09 22:14:07 +10:00
Jamie Curnow fff2b0d097 Fix for changing the upstream Host from $host to $forward_host
($http_host) forwards the raw client Host header, including the port and
any absolute-form URI mismatch, to every upstream and into the asset
cache key
2026-11-09 22:09:34 +10:00
Jamie Curnow 99678969b3 Fix a proxy host whose access list can't be loaded is now denied to
everyone instead of being served with no restrictions
2026-11-09 22:04:32 +10:00
Jamie Curnow 0cde59a8ad Fix for rate limiter only trusts X-Forwarded-For when the socket is
loopback, and the admin nginx sets that header to its own $remote_addr
2026-11-09 22:00:26 +10:00
Jamie Curnow 36b3434b9c Fix for adding meta.certificate_key to omissions()
means upload() now reads a row with no private key, so an upload that
leaves out certificate_key wipes the stored key and writeCustomCert
writes undefined to privkey.pem.
2026-11-09 21:57:00 +10:00
jc21 71be654418 Merge branch 'master' into develop 2026-11-09 14:01:11 +10:00
Jamie Curnow 47c9bfba4f Update dependencies 2026-11-09 11:55:20 +10:00
jc21 7c767f43b0 Merge pull request #6028 from occultsound/zh-tw-locale
Add Traditional Chinese (Taiwan) translation
2026-11-09 07:42:44 +10:00
occultsound 15beff767d Add Traditional Chinese (Taiwan) translation 2026-11-09 02:16:59 +08:00
jc21 53ee436e35 Merge pull request #6026 from Stephan-P/patch-1
Update nl.json
2026-11-08 18:53:40 +10:00
Stephan Paternotteandgreptile-apps[bot] e824d353f0 Update frontend/src/locale/src/nl.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-11-07 15:55:09 +01:00
Stephan Paternotte 3f2c186771 Update nl.json
Lang: Dutch translation file updated + some minor corrections
2026-11-07 15:47:16 +01:00
jc21 ea7e7468df Merge pull request #6023 from Alexis-Loskoutoff/fix-authentication-oracle
Fix authentication oracle
2026-11-06 08:58:10 +10:00
Alexis Loskoutoff e93b771ff5 Check a dummy hash when a login names no account
Without a password to check, a login for an unknown account skipped bcrypt
and answered far sooner than one with a wrong password, which still told
which emails have an account.
2026-11-05 18:29:52 +01:00
Alexis Loskoutoff c08f2e65a8 Add test for the same error on every failed login 2026-11-05 17:29:48 +01:00
Alexis Loskoutoff 0d90542119 Give the same error for every failed login
An unknown or passwordless account was rejected without the i18n key that
a wrong password gets, so the response told which emails have an account.
2026-11-05 17:29:48 +01:00
jc21 2f5342b427 Merge pull request #5908 from auzcoder/fix/auth-rate-limit-and-permissions
feat: rate limit auth endpoints and scope user proxy host permissions
2026-11-05 23:05:08 +10:00
Abdulhafiz Davlatov bc8bbf3562 feat: rate limit auth endpoints and scope user proxy host permissions
- Add in-memory sliding window rate limiter for auth token endpoints with CI/test bypass and successful request exclusion
- Add user_permission meta column migration and Objection model configuration
- Support scoping allowed proxy hosts per user when in 'Created Items Only' visibility mode
- Restrict advanced nginx configuration editing on hosts to admin users only
- Fix user permissions route ID parsing and omit id from permission update payloads
- Add allowed proxy hosts selector in permissions modal and display permissions summary badges in users table
2026-11-05 12:16:36 +05:00
Jamie Curnow cc46ce5ab6 Biome schema match to package 2026-11-05 16:43:28 +10:00
Jamie Curnow b5ee4f7b1b Update pull request template 2026-11-05 10:34:33 +10:00
jc21 9be1cb7818 Merge pull request #5917 from nglinh28/feat/vietnamese-locale-update
i18n(vi): complete Vietnamese locale and add vi tests
2026-11-05 10:30:02 +10:00
jc21 fe58b4eba0 Merge pull request #5889 from StopDragon/i18n/ko-missing-keys
Update Korean translation
2026-11-05 10:25:43 +10:00
jc21 18fc758b4f Merge pull request #5934 from lennondotw/fix/preserve-host-port
fix(proxy): preserve client ports in forwarded Host headers
2026-11-05 10:15:05 +10:00
jc21 5795699091 Merge pull request #5939 from hafizahmedzia/fix/http-certificate-result-keys
fix: preserve hostname keys in HTTP certificate test results
2026-11-05 10:05:11 +10:00
jc21 b88ca8e2e5 Merge pull request #5948 from Waterman1997/patch-1
Fix nginx reload after certificate renewal
2026-11-05 10:04:02 +10:00
jc21 d504c2bf25 Merge pull request #5951 from Alexis-Loskoutoff/fix/update-infomaniak-certbot-plugin
Update infomaniak certbot plugin to 0.3.3
2026-11-05 10:03:35 +10:00
jc21 49662bdc91 Merge pull request #5952 from lsr0/certbot-hetzner-fix-4.0.0
Update Hetzner certbot plugin to v4.0.0
2026-11-05 10:02:57 +10:00
jc21 f23dd9bff0 Merge pull request #5969 from capitain-schos/feature/add-dns-provider-ipp
added ip-projects certbot definitions
2026-11-05 09:59:23 +10:00
jc21 2a8b39bdef Merge pull request #5971 from Juice-de-Orange/fix/hsts-subdomains-per-host
fix: apply the HSTS sub-domains setting per host
2026-11-05 09:58:02 +10:00
jc21 23e56e9581 Merge pull request #5972 from junkerderprovinz/fix/route53-propagation
Skip propagation seconds option for Route 53 DNS challenge
2026-11-05 09:56:43 +10:00
jc21 3d09072ea7 Merge pull request #5976 from l1n-x/actualize-beget-package
Changed target plugin for Beget
2026-11-05 09:36:45 +10:00
jc21 4daa54c12e Merge pull request #5977 from mirfalltnixein/fix/unsaved-modal-changes
Prevent data loss when dismissing unsaved forms
2026-11-05 08:58:53 +10:00
jc21 5457ca0965 Merge pull request #5979 from seaeagle1/develop
Add certbot plugin for Scaleway DNS
2026-11-05 08:57:44 +10:00
jc21 1dd0b8fbe8 Merge pull request #5989 from Rip-07/fix/5964-stale-nginx-pid
fix: remove stale nginx pid file before starting nginx
2026-11-05 08:45:23 +10:00
jc21 5db7516f40 Merge pull request #5992 from chenghaopeng/patch-1
Update package name and version for Baidu DNS plugin
2026-11-05 08:44:51 +10:00
jc21 bd34c94222 Merge pull request #5993 from SebZar/feat/certificate-details-tooltip
feat: show certificate details in host tables
2026-11-05 08:42:57 +10:00
Jamie Curnow 9754c4c639 Bump mid version, as Tabler has changed the dark UI it's worth more than a revision change 2026-11-05 08:38:55 +10:00
Jamie Curnow db292e947d Bump version 2026-11-05 08:00:40 +10:00
Jamie Curnow ec0f30106d Migrate cypress to v16 2026-11-05 07:32:24 +10:00
Jamie Curnow 1c76fb76ea install yarn in cypress image 2026-11-04 22:08:48 +10:00
Jamie Curnow f915ca2462 Update all deps, and cypress 2026-11-04 20:54:23 +10:00
jc21 55481adede Merge pull request #6002 from mikeyhenein/frontend-deps
Frontend: refresh dependencies, react-intl 12, vitest 5, happy-dom 21; fix Tabler 1.7 regressions
2026-11-04 20:36:18 +10:00
jc21 7e240e4136 Merge pull request #6010 from NginxProxyManager/dependabot/npm_and_yarn/frontend/decode-uri-component-0.5.1
build(deps): bump decode-uri-component from 0.5.0 to 0.5.1 in /frontend
2026-11-04 14:42:39 +10:00
dependabot[bot] 3fb2f7261f build(deps): bump decode-uri-component from 0.5.0 to 0.5.1 in /frontend
Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.5.0 to 0.5.1.
- [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases)
- [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.5.0...v0.5.1)

---
updated-dependencies:
- dependency-name: decode-uri-component
  dependency-version: 0.5.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-04 04:42:08 +00:00
jc21 bea51343ef Merge pull request #5998 from mikeyhenein/vitest-node25
Frontend tests: disable Node's built-in localStorage so vitest works on Node 25+
2026-11-04 14:41:54 +10:00
jc21 42fbab8058 Merge pull request #5999 from mikeyhenein/gitattributes-lf
Add .gitattributes to keep LF line endings
2026-11-04 14:41:31 +10:00
jc21 dca46af3fc Merge pull request #6000 from mikeyhenein/s6-overlay-3.2.3.2
s6-overlay 3.2.3.2
2026-11-04 14:41:12 +10:00
jc21 4aab1f50a4 Merge pull request #6001 from mikeyhenein/backend-deps
Backend: refresh yarn.lock within the declared ranges (fixes 15 of 16 advisories, incl. body-parser DoS)
2026-11-04 14:40:53 +10:00
jc21 44ada6ad5a Merge pull request #6003 from mikeyhenein/permissions-schema
Reject unknown keys in PUT /api/users/:id/permissions (an extra key crashes the backend)
2026-11-04 14:34:24 +10:00
jc21 e3631e88c6 Merge pull request #6004 from mikeyhenein/certbot-errors
Show certbot's failure reason in the UI instead of "Internal Error"
2026-11-04 14:33:39 +10:00
jc21 fdf51a654c Merge pull request #6005 from mikeyhenein/certificate-delete-files
Remove a certificate's certbot files on delete even when revoking fails
2026-11-04 14:30:08 +10:00
jc21 0dc9933409 Merge pull request #6008 from wikrie/patch-2
Update dns-multi plugin version to 5.6.0
2026-11-04 08:28:49 +10:00
wikrie 044d093ec5 Update dns-multi plugin version to 5.6.0
update needed as used version 4.9 did not exists anymore
2026-11-03 21:27:47 +01:00
mikeyheneinandClaude Opus 5.5 d7a79a547f Remove a certificate's certbot files on delete even when revoking fails
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:28:39 -08:00
mikeyheneinandClaude Opus 5.5 d131419907 Show certbot's failure reason in the UI instead of "Internal Error"
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:28:38 -08:00
mikeyheneinandClaude Opus 5.5 c68456b205 Frontend: refresh dependencies, react-intl 12, vitest 5, happy-dom 21; fix Tabler 1.7 regressions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:28:37 -08:00
mikeyheneinandClaude Opus 5.5 5aed3a5539 Reject unknown keys in PUT /api/users/:id/permissions (an extra key crashes the backend)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:28:37 -08:00
mikeyheneinandClaude Opus 5.5 c214a49afb Backend: refresh yarn.lock within the declared ranges (fixes 15 of 16 advisories, incl. body-parser DoS)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:28:36 -08:00
mikeyheneinandClaude Opus 5.5 b28e0d5442 s6-overlay 3.2.3.2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:28:35 -08:00
mikeyheneinandClaude Opus 5.5 2a27e542e9 Add .gitattributes to keep LF line endings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:28:33 -08:00
mikeyheneinandClaude Opus 5.5 66fd49ee46 Frontend tests: disable Node's built-in localStorage so vitest works on Node 25+
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:28:31 -08:00
zarges.sandClaude Opus 5.5 d0739b7210 fix: drop DNS provider from certificate popover
Host endpoints clear the certificate meta, so the DNS provider is never
available in the host tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 13:36:15 +01:00
zarges.sandClaude Opus 5.5 435ddeee34 feat: show certificate details in host tables
The SSL column of the proxy, redirection, 404 and stream host tables
only showed the certificate type, so it was not possible to tell which
certificate a host uses.

- Popover on the SSL column with name, ID, provider (incl. DNS
  provider), domain names and expiry date
- SSL label turns red when the certificate is expired and yellow when
  it expires within 30 days
- Custom certificates show their name below the label

Refs #5705

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 13:16:22 +01:00
鹏鹏 c19ce19f90 Update package name and version for Baidu DNS plugin
The previous PyPi package has stopped being updated and is unavailable, rewrite one
2026-11-02 15:44:09 +08:00
Askhat AkhtanovandClaude Opus 5.5 c23a1cb343 fix: remove stale nginx pid file before starting nginx
When /run/nginx is already owned by PUID:PGID, the ownership step skips
it, so a leftover nginx.pid owned by root stays in place and nginx,
running as PUID:PGID, fails with "Permission denied" on every start.

Nginx is not running yet during prepare, so remove the pid file there.

Fixes #5964

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 13:33:27 +05:00
seaeagle1 d2e8e5650a Add Scaleway DNS 2026-10-28 13:29:10 +01:00
mirfalltnixein 50d413f464 Protect partial location and auth drafts 2026-10-28 00:34:50 +01:00
mirfalltnixein 5e76f904ee Address modal safeguard review feedback 2026-10-28 00:30:52 +01:00
mirfalltnixein 6838868fdc Prevent dismissing unsaved form modals 2026-10-28 00:04:24 +01:00
Роман Саркисян 26e27e3503 updated beget target plugin 2026-10-28 02:09:01 +07:00
Junker der ProvinzandPascaru 9780e22792 Skip propagation seconds option for Route 53 DNS challenge
certbot deprecated --dns-route53-propagation-seconds in 2.5.0 and
removed it in 3.0.0, so requesting a Route 53 certificate with a
propagation value fails with "unrecognized arguments". The plugin
waits for the record through the AWS GetChange API instead.

Fixes #4702
Fixes #5414

Co-authored-by: Pascaru <23747773+pascaruchan@users.noreply.github.com>
2026-10-24 19:17:37 +02:00
Juice-de-Orange 076c522ccc fix: apply the HSTS sub-domains setting per host
_hsts_map.conf defined $hsts_header with the host's own value in every
host config. A map is global to nginx, so one definition won for all
hosts and the "HSTS Sub-domains" switch of the others was ignored.

Define two fixed maps instead and let _hsts.conf pick one per host.
They get new names, so configs generated before this change, which
still define $hsts_header, can't override them. The maps keep HSTS
limited to https.

Fixes #5947
2026-10-24 16:38:10 +02:00
capitain-schos 518dd04679 added ipp certbot definitions 2026-10-24 14:15:25 +02:00
Lindsay Roberts d9c4d33a7c fix(certbot): update Hetzner plugin to v4.0.0 2026-10-18 21:19:53 +03:00
Alexis Loskoutoff b6e9038f0c Update infomaniak plugin to 0.3.3 2026-10-18 19:42:35 +02:00
CnpPt a57ffb0ea9 Update certificate.js 2026-10-17 12:20:45 +08:00
CnpPt b7d1e99e8c Reload Nginx after renewing SSL certificate 2026-10-17 11:50:38 +08:00
lennondotw 3a455595e7 fix(proxy): isolate cached assets by forwarded authority 2026-10-14 14:48:22 +08:00
lennondotw c50c06530b Merge remote-tracking branch 'upstream/develop' into fix/preserve-host-port 2026-10-14 14:37:17 +08:00
jc21 1aca775cb9 Merge pull request #5944 from jeezat/develop
fix(certbot): update Akamai Edge DNS plugin and credentials for v0.3.0
2026-10-14 07:10:53 +10:00
jc21 556738fd4d Merge pull request #5946 from Alexis-Loskoutoff/require-authentication-to-validate-certificates
Require authentication to validate certificates
2026-10-14 07:09:56 +10:00
Alexis Loskoutoff f1bd157438 Add test for certificate validation with view-only permission 2026-10-13 19:32:53 +02:00
Alexis Loskoutoff 6acf346262 Add test for unauthenticated certificate validation 2026-10-13 18:35:50 +02:00
Alexis Loskoutoff 82529d6402 Require certificates:create permission to validate certificates 2026-10-13 18:34:20 +02:00
jeezat 8c5b4d8ffd fix(certbot): update Akamai Edge DNS plugin and credentials for v0.3.0 2026-10-13 07:57:28 +02:00
jc21 d780474583 Merge pull request #5938 from Alexis-Loskoutoff/Update-FR-translations
Update french translations
2026-10-12 15:12:57 +10:00
Hafiz Ahmed Zia 9972e78dc8 fix: preserve hostname keys in HTTP certificate test results
Preserve raw response keys for the HTTP certificate test endpoint so
hostname punctuation and distinct result entries survive normalization.
Add API and mounted-modal regression coverage for the exact candidate
reviewed against develop 56ab784117.

Related: NginxProxyManager/nginx-proxy-manager#5849

AI assistance: Codex assisted with the implementation, tests, independent
review and contribution text.
2026-10-12 08:36:32 +05:00
Alexis Loskoutoff 41713715ca Improve existing french translations 2026-10-11 22:55:39 +02:00
Alexis Loskoutoff 06b02f7731 Add missing FR translations 2026-10-11 21:53:42 +02:00
jc21 56ab784117 Merge pull request #5935 from NginxProxyManager/fix/5919-access-list-render
Fix access list rules and SSL missing from re-rendered host configs
2026-10-09 22:56:56 +10:00
Jamie Curnow 717877db3f Fix flakey cypress test 2026-10-09 22:35:02 +10:00
jc21 1454252acd Merge branch 'develop' into fix/5919-access-list-render 2026-10-09 07:32:34 +10:00
lennondotw 6b26999e34 fix(proxy): preserve client ports in forwarded Host headers
Keep the incoming Host authority for default and custom locations, with the existing hostname fallback when Host is absent. Add a built-image regression that validates Host against Origin and Referer and run it after the CI image build.
2026-10-08 15:50:24 +08:00
jc21 1028e139e4 Merge pull request #5930 from Juice-de-Orange/i18n/de-it-missing
Add missing German (de) and Italian (it) translations
2026-10-07 08:35:10 +10:00
jc21 7d7ed25715 Merge pull request #5929 from NginxProxyManager/dependabot/npm_and_yarn/backend/proxy-addr-2.0.8
build(deps): bump proxy-addr from 2.0.7 to 2.0.8 in /backend
2026-10-07 08:31:37 +10:00
Juice-de-Orange 3270873cc8 Add missing German (de) and Italian (it) translations 2026-10-06 20:42:18 +02:00
dependabot[bot] 81e7044a17 build(deps): bump proxy-addr from 2.0.7 to 2.0.8 in /backend
Bumps [proxy-addr](https://github.com/jshttp/proxy-addr) from 2.0.7 to 2.0.8.
- [Release notes](https://github.com/jshttp/proxy-addr/releases)
- [Changelog](https://github.com/jshttp/proxy-addr/blob/master/HISTORY.md)
- [Commits](https://github.com/jshttp/proxy-addr/compare/v2.0.7...v2.0.8)

---
updated-dependencies:
- dependency-name: proxy-addr
  dependency-version: 2.0.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-05 23:34:59 +00:00
jc21 3f1c0f27d3 Merge pull request #5925 from NginxProxyManager/dependabot/npm_and_yarn/backend/fast-uri-3.1.8
build(deps): bump fast-uri from 3.1.7 to 3.1.8 in /backend
2026-10-06 09:33:45 +10:00
Jamie CurnowandClaude Opus 5.5 2afd723cd8 Fix access list rules and SSL missing from re-rendered host configs
Some paths rendered host configs without fully loading the relations
the templates depend on, silently dropping access rules (and SSL):

- Enabling a proxy host loaded the access list without its clients or
  auth items, so the host came back with no access restrictions.
- Requesting a Let's Encrypt certificate re-rendered every host sharing
  its domains from bare rows, dropping access rules and SSL.

generateConfig now loads any referenced certificate or access list that
isn't fully loaded, so no caller can render a partial config. The two
callers above also fetch the relations themselves, and a warning is
logged when a host's access list is missing or has no rules.

Adds integration tests covering access lists across disable/enable.

Fixes #5919, #4286, #5710

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:13:31 +10:00
dependabot[bot] f5e6dfecfb build(deps): bump fast-uri from 3.1.7 to 3.1.8 in /backend
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.7 to 3.1.8.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.7...v3.1.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-05 09:34:18 +00:00
jc21 778ab6b375 Merge pull request #5921 from NginxProxyManager/dependabot/npm_and_yarn/test/serialize-javascript-7.1.2
build(deps): bump serialize-javascript from 7.1.1 to 7.1.2 in /test
2026-10-05 19:32:57 +10:00
dependabot[bot] 092d212ad7 build(deps): bump serialize-javascript from 7.1.1 to 7.1.2 in /test
Bumps [serialize-javascript](https://github.com/yahoo/serialize-javascript) from 7.1.1 to 7.1.2.
- [Release notes](https://github.com/yahoo/serialize-javascript/releases)
- [Commits](https://github.com/yahoo/serialize-javascript/compare/v7.1.1...v7.1.2)

---
updated-dependencies:
- dependency-name: serialize-javascript
  dependency-version: 7.1.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-04 14:38:20 +00:00
nglinh28andClaude Sonnet 5 e7e69abb84 i18n(vi): complete Vietnamese locale and add vi tests
- Translate the keys added for the logs feature and access list
  inheritance so the Vietnamese UI has full key coverage
- Update Vietnamese help docs for access lists, certificates, hosts
  and streams
- Use a 24-hour clock for Vietnamese date and time formatting
- Add tests that check Vietnamese key coverage, ICU placeholders and
  that strings are translated

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-03 09:28:36 +07:00
jc21 f64d43b9ae Merge pull request #5913 from PrassanthVG/fix/ip-ranges-timer
Don't start IP ranges renewal timer when fetching is disabled
2026-10-02 08:48:48 +10:00
Prassanth 77dfc3d3c1 Don't start IP ranges renewal timer when fetching is disabled
IP_RANGES_FETCH_ENABLED=false only skipped the initial fetch; the
renewal timer still fetched the ranges every 6 hours.

Fixes #5748
2026-10-02 02:08:28 +05:30
jc21 2dfd691169 Merge pull request #5910 from NginxProxyManager/dependabot/npm_and_yarn/backend/ip-address-10.7.2
build(deps): bump ip-address from 10.5.0 to 10.7.2 in /backend
2026-09-30 07:01:24 +10:00
dependabot[bot] c699dc96d4 build(deps): bump ip-address from 10.5.0 to 10.7.2 in /backend
Bumps [ip-address](https://github.com/beaugunderson/ip-address) from 10.5.0 to 10.7.2.
- [Release notes](https://github.com/beaugunderson/ip-address/releases)
- [Commits](https://github.com/beaugunderson/ip-address/compare/v10.5.0...v10.7.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-29 07:19:54 +00:00
jc21 5dcaf28d7c Merge pull request #5891 from Amitind/fix/5128-hide-certificate-key
fix: omit certificate_key from certificate API responses
2026-09-26 19:30:31 +10:00
jc21 de65105744 Merge pull request #5886 from xJayMorex/fix-chown
fix: only chown /data/keys.json if necessary
2026-09-26 19:18:14 +10:00
Amit Yadav a54b73f153 fix: omit certificate_key from certificate API responses
The private key of custom certificates was returned in meta by GET /api/nginx/certificates and GET /api/nginx/certificates/{id}. Add meta.certificate_key to omissions(), the same way meta.dns_provider_credentials is hidden. The key stays in the database and is still written to /data/custom_ssl on upload.
2026-09-25 20:35:32 +05:30
JUNG JI YONG ccbffa76ff Update Korean translation
- Add missing strings (2FA, logs, per-location access lists, etc.)
- Fix translations that didn't match the UI context
- Minor wording fixes in Korean help docs
2026-09-25 23:19:18 +09:00
xJayMorex 4725153962 fix: only chown /data/keys.json if necessary 2026-09-25 11:49:17 +02:00
jc21 a29eae9dc2 Merge pull request #5880 from NginxProxyManager/develop
v2.16.0
2026-09-24 19:27:13 +10:00
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
jc21 47b367d61e Merge pull request #5276 from NginxProxyManager/develop
v2.13.7
2026-02-06 07:11:49 +10:00
Jamie Curnow d19f5c1960 Fix upgrade problem with otplib existing secrets 2026-02-05 13:12:54 +10:00
Jamie Curnow 77662b4e7f Use better-sqlite3 package for sqlite databases 2026-02-05 13:11:57 +10:00
Jamie Curnow c88de65d3a Fix #5274 2fa backup codes not validating properly 2026-02-05 10:51:15 +10:00
jc21 ac4efd2333 Merge branch 'master' into develop 2026-02-05 08:27:41 +10:00
Jamie Curnow eab38d8934 Bump version 2026-02-05 08:26:49 +10:00
jc21 4833dcbf3a Merge pull request #5237 from NginxProxyManager/dependabot/npm_and_yarn/backend/dev-patch-updates-2bda1081ab
Bump @biomejs/biome from 2.3.12 to 2.3.13 in /backend in the dev-patch-updates group
2026-02-05 07:58:38 +10:00
jc21 c6fba1cbfe Merge pull request #5272 from NginxProxyManager/dependabot/npm_and_yarn/backend/prod-patch-updates-627d993332
Bump mysql2 from 3.16.2 to 3.16.3 in /backend in the prod-patch-updates group
2026-02-05 07:58:00 +10:00
7heMech e72dfc3bfd Add reuseport to improve performance and reduce packet drops 2026-02-04 12:14:10 +00:00
jc21 cdde543e8a Merge pull request #5273 from NginxProxyManager/dependabot/npm_and_yarn/frontend/dev-patch-updates-1f568f1195
Bump @biomejs/biome from 2.3.13 to 2.3.14 in /frontend in the dev-patch-updates group
2026-02-04 12:06:56 +10:00
Jamie Curnow 0d62c26164 Fix linting 2026-02-04 10:43:14 +10:00
Jamie Curnow c3173d83b8 Update biome.json to match viome version 2026-02-04 10:39:43 +10:00
Jamie Curnow 6ba40216cd Update biome.json to match viome version 2026-02-04 10:38:56 +10:00
dependabot[bot] 3c54413752 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.16.2 to 3.16.3
- [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.2...v3.16.3)

---
updated-dependencies:
- dependency-name: mysql2
  dependency-version: 3.16.3
  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-04 00:15:00 +00:00
jc21 65cf8ce583 Merge pull request #5248 from NginxProxyManager/dependabot/npm_and_yarn/backend/otplib-13.2.1
Bump otplib from 12.0.1 to 13.2.1 in /backend
2026-02-04 10:13:27 +10:00
dependabot[bot] a4bc8d5d21 Bump @biomejs/biome in /frontend in the dev-patch-updates group
Bumps the dev-patch-updates group in /frontend with 1 update: [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome).


Updates `@biomejs/biome` from 2.3.13 to 2.3.14
- [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.3.14/packages/@biomejs/biome)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.3.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-03 21:52:25 +00:00
dependabot[bot] 2bcf5e91ce Bump @biomejs/biome in /backend in the dev-patch-updates group
Bumps the dev-patch-updates group in /backend with 1 update: [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome).


Updates `@biomejs/biome` from 2.3.12 to 2.3.13
- [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.3.13/packages/@biomejs/biome)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.3.13
  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-03 21:51:39 +00:00
Jamie Curnow 3e3d08b68f Change dependabot interval to weekly 2026-02-04 07:50:26 +10:00
Jamie Curnow f90066822f Fix v13 otplib upgrades 2026-02-04 07:47:16 +10:00
dependabot[bot] bb4b5fb3aa Bump otplib from 12.0.1 to 13.2.1 in /backend
Bumps [otplib](https://github.com/yeojz/otplib/tree/HEAD/packages/otplib) from 12.0.1 to 13.2.1.
- [Release notes](https://github.com/yeojz/otplib/releases)
- [Commits](https://github.com/yeojz/otplib/commits/v13.2.1/packages/otplib)

---
updated-dependencies:
- dependency-name: otplib
  dependency-version: 13.2.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 21:22:20 +00:00
jc21 8014f34195 Merge pull request #5269 from NginxProxyManager/dependabot/npm_and_yarn/backend/prod-minor-updates-2bc8aaf294
Bump pg from 8.17.2 to 8.18.0 in /backend in the prod-minor-updates group
2026-02-04 07:20:55 +10:00
jc21 4f8037ded2 Merge pull request #5270 from NginxProxyManager/dependabot/npm_and_yarn/frontend/dev-minor-updates-1492aee52e
Bump happy-dom from 20.4.0 to 20.5.0 in /frontend in the dev-minor-updates group
2026-02-04 07:18:42 +10:00
jc21 e7a1f84e45 Merge pull request #5271 from NginxProxyManager/dependabot/npm_and_yarn/frontend/prod-patch-updates-4c40e63da3
Bump react-intl from 8.1.2 to 8.1.3 in /frontend in the prod-patch-updates group
2026-02-04 07:18:31 +10:00
dependabot[bot] 6f0931bed5 Bump react-intl in /frontend in the prod-patch-updates group
Bumps the prod-patch-updates group in /frontend with 1 update: [react-intl](https://github.com/formatjs/formatjs).


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

---
updated-dependencies:
- dependency-name: react-intl
  dependency-version: 8.1.3
  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-03 13:04:48 +00:00
dependabot[bot] 7f0c5d4364 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.4.0 to 20.5.0
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.4.0...v20.5.0)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-version: 20.5.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-03 13:04:21 +00:00
dependabot[bot] 60404b6f7e Bump pg in /backend in the prod-minor-updates group
Bumps the prod-minor-updates group in /backend with 1 update: [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg).


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

---
updated-dependencies:
- dependency-name: pg
  dependency-version: 8.18.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-03 13:04:13 +00:00
jc21 c2fddee2c7 Merge pull request #5264 from NginxProxyManager/dependabot/npm_and_yarn/frontend/dev-patch-updates-cc9765ca56
Bump the dev-patch-updates group across 1 directory with 3 updates
2026-02-03 17:10:42 +10:00
Jerry8块 b7402d47a0 Merge branch 'NginxProxyManager:develop' into develop 2026-02-03 15:10:13 +08:00
jc21 f09876d31b Merge pull request #5252 from NginxProxyManager/dependabot/npm_and_yarn/backend/apidevtools/json-schema-ref-parser-14.1.1
Bump @apidevtools/json-schema-ref-parser from 11.9.3 to 14.1.1 in /backend
2026-02-03 17:06:56 +10:00
dependabot[bot] 8708a3bab8 Bump the dev-patch-updates group across 1 directory with 3 updates
Bumps the dev-patch-updates group with 3 updates in the /frontend directory: [@formatjs/cli](https://github.com/formatjs/formatjs), [@tanstack/react-query-devtools](https://github.com/TanStack/query/tree/HEAD/packages/react-query-devtools) and [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react).


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

Updates `@tanstack/react-query-devtools` from 5.91.2 to 5.91.3
- [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.91.3/packages/react-query-devtools)

Updates `@vitejs/plugin-react` from 5.1.2 to 5.1.3
- [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.3/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@formatjs/cli"
  dependency-version: 6.12.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@tanstack/react-query-devtools"
  dependency-version: 5.91.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 5.1.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-03 07:06:20 +00:00
jc21 218fadd168 Merge pull request #5254 from NginxProxyManager/dependabot/npm_and_yarn/backend/body-parser-2.2.2
Bump body-parser from 1.20.4 to 2.2.2 in /backend
2026-02-03 17:04:49 +10:00
jc21 9cf1d000c8 Merge pull request #5257 from maghuro/add-pt-pt
Add pt-PT lang
2026-02-03 17:04:19 +10:00
jc21 714bebbbc7 Merge pull request #5263 from NginxProxyManager/dependabot/npm_and_yarn/test/prod-patch-updates-5b27633cb0
Bump @quobix/vacuum from 0.23.4 to 0.23.5 in /test in the prod-patch-updates group
2026-02-03 17:02:59 +10:00
jc21 127008c9b5 Merge pull request #5265 from NginxProxyManager/dependabot/npm_and_yarn/frontend/react-intl-8.1.2
Bump react-intl from 7.1.14 to 8.1.2 in /frontend
2026-02-03 17:02:18 +10:00
dependabot[bot] 7cc2bfbf6a Bump react-intl from 7.1.14 to 8.1.2 in /frontend
Bumps [react-intl](https://github.com/formatjs/formatjs) from 7.1.14 to 8.1.2.
- [Release notes](https://github.com/formatjs/formatjs/releases)
- [Commits](https://github.com/formatjs/formatjs/compare/react-intl@7.1.14...react-intl@8.1.2)

---
updated-dependencies:
- dependency-name: react-intl
  dependency-version: 8.1.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-02 14:26:07 +00:00
dependabot[bot] de3b543d08 Bump @quobix/vacuum in /test in the prod-patch-updates group
Bumps the prod-patch-updates group in /test with 1 update: [@quobix/vacuum](https://github.com/daveshanley/vacuum).


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

---
updated-dependencies:
- dependency-name: "@quobix/vacuum"
  dependency-version: 0.23.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-02 14:25:26 +00: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
maghuro c515815b0e Remove merge conflict markers from lang-list.json 2026-01-31 12:10:36 +00:00
maghuro 3db02370fd Add Portuguese language support to IntlProvider 2026-01-31 12:08:07 +00:00
maghuro 4ad1af5576 Remove duplicate locale entries and keep pt-PT 2026-01-31 12:07:32 +00:00
maghuro a73d54fedc Add Portuguese (European) language support 2026-01-31 12:06:50 +00:00
maghuro 8c8005f817 Add Portuguese language support to HelpDoc 2026-01-31 12:05:32 +00:00
maghuro 83d993578b Add pt-PT lang
Add Portuguese (European) language
2026-01-31 11:59:35 +00:00
dependabot[bot] 8532e7520f Bump body-parser from 1.20.4 to 2.2.2 in /backend
Bumps [body-parser](https://github.com/expressjs/body-parser) from 1.20.4 to 2.2.2.
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/1.20.4...v2.2.2)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-version: 2.2.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-30 13:04:20 +00:00
dependabot[bot] 58d47cd69a Bump @apidevtools/json-schema-ref-parser in /backend
Bumps [@apidevtools/json-schema-ref-parser](https://github.com/APIDevTools/json-schema-ref-parser) from 11.9.3 to 14.1.1.
- [Release notes](https://github.com/APIDevTools/json-schema-ref-parser/releases)
- [Commits](https://github.com/APIDevTools/json-schema-ref-parser/compare/v11.9.3...v14.1.1)

---
updated-dependencies:
- dependency-name: "@apidevtools/json-schema-ref-parser"
  dependency-version: 14.1.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-30 13:04:04 +00:00
jc21 bad3eac515 Merge pull request #5245 from NginxProxyManager/dependabot/npm_and_yarn/backend/archiver-7.0.1
Bump archiver from 5.3.2 to 7.0.1 in /backend
2026-01-30 13:40:19 +10:00
dependabot[bot] 00b58f73f8 Bump archiver from 5.3.2 to 7.0.1 in /backend
Bumps [archiver](https://github.com/archiverjs/node-archiver) from 5.3.2 to 7.0.1.
- [Release notes](https://github.com/archiverjs/node-archiver/releases)
- [Changelog](https://github.com/archiverjs/node-archiver/blob/master/CHANGELOG.md)
- [Commits](https://github.com/archiverjs/node-archiver/compare/5.3.2...7.0.1)

---
updated-dependencies:
- dependency-name: archiver
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-30 01:35:10 +00:00
jc21 47981f0d56 Merge pull request #5230 from NginxProxyManager/dependabot/npm_and_yarn/frontend/prod-minor-updates-37a0ff9301
Bump the prod-minor-updates group in /frontend with 4 updates
2026-01-30 11:33:53 +10:00
jc21 38257859e2 Merge pull request #5244 from NginxProxyManager/dependabot/npm_and_yarn/backend/bcrypt-6.0.0
Bump bcrypt from 5.1.1 to 6.0.0 in /backend
2026-01-30 11:33:34 +10:00
dependabot[bot] a169e1131c Bump the prod-minor-updates group in /frontend with 4 updates
Bumps the prod-minor-updates group in /frontend with 4 updates: [@tabler/icons-react](https://github.com/tabler/tabler-icons/tree/HEAD/packages/icons-react), [country-flag-icons](https://gitlab.com/catamphetamine/country-flag-icons), [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) and [rooks](https://github.com/imbhargav5/rooks).


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

Updates `country-flag-icons` from 1.5.21 to 1.6.8
- [Changelog](https://gitlab.com/catamphetamine/country-flag-icons/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/catamphetamine/country-flag-icons/compare/v1.5.21...v1.6.8)

Updates `react-router-dom` from 7.9.5 to 7.13.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.13.0/packages/react-router-dom)

Updates `rooks` from 9.3.0 to 9.5.0
- [Release notes](https://github.com/imbhargav5/rooks/releases)
- [Commits](https://github.com/imbhargav5/rooks/compare/rooks@9.3.0...rooks@9.5.0)

---
updated-dependencies:
- dependency-name: "@tabler/icons-react"
  dependency-version: 3.36.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: country-flag-icons
  dependency-version: 1.6.8
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react-router-dom
  dependency-version: 7.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: rooks
  dependency-version: 9.5.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-01-29 23:36:00 +00:00
dependabot[bot] a99cde9cd8 Bump bcrypt from 5.1.1 to 6.0.0 in /backend
Bumps [bcrypt](https://github.com/kelektiv/node.bcrypt.js) from 5.1.1 to 6.0.0.
- [Release notes](https://github.com/kelektiv/node.bcrypt.js/releases)
- [Changelog](https://github.com/kelektiv/node.bcrypt.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kelektiv/node.bcrypt.js/compare/v5.1.1...v6.0.0)

---
updated-dependencies:
- dependency-name: bcrypt
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 23:33:36 +00:00
jc21 c69bd187af Merge pull request #5243 from NginxProxyManager/dependabot/npm_and_yarn/test/prod-patch-updates-4953ba4782
Bump axios from 1.13.3 to 1.13.4 in /test in the prod-patch-updates group
2026-01-30 09:33:18 +10:00
jc21 98fe622967 Merge pull request #5246 from NginxProxyManager/dependabot/npm_and_yarn/frontend/dev-minor-updates-a6c26cdf84
Bump happy-dom from 20.3.9 to 20.4.0 in /frontend in the dev-minor-updates group
2026-01-30 09:33:05 +10:00
jc21 eddca3597d Merge pull request #5247 from NginxProxyManager/dependabot/npm_and_yarn/backend/express-5.2.1
Bump express from 4.22.0 to 5.2.1 in /backend
2026-01-30 09:31:33 +10:00
jc21 ed0b2306a2 Merge pull request #5250 from NginxProxyManager/dependabot/npm_and_yarn/test/tar-7.5.7
Bump tar from 7.5.6 to 7.5.7 in /test
2026-01-30 09:31:23 +10:00
jc21 17f6050de2 Merge pull request #5235 from NginxProxyManager/dependabot/npm_and_yarn/frontend/prod-patch-updates-9d9e6eac1f
Bump the prod-patch-updates group across 1 directory with 4 updates
2026-01-30 09:31:12 +10:00
dependabot[bot] 469d72a2f9 Bump tar from 7.5.6 to 7.5.7 in /test
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.6 to 7.5.7.
- [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.6...v7.5.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 14:02:55 +00:00
dependabot[bot] 3ed3ec0001 Bump express from 4.22.0 to 5.2.1 in /backend
Bumps [express](https://github.com/expressjs/express) from 4.22.0 to 5.2.1.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.22.0...v5.2.1)

---
updated-dependencies:
- dependency-name: express
  dependency-version: 5.2.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-28 13:04:19 +00:00
dependabot[bot] 24ff3c7b11 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.3.9 to 20.4.0
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.3.9...v20.4.0)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-version: 20.4.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-01-28 13:04:12 +00:00
dependabot[bot] 58dda941b8 Bump axios in /test in the prod-patch-updates group
Bumps the prod-patch-updates group in /test with 1 update: [axios](https://github.com/axios/axios).


Updates `axios` from 1.13.3 to 1.13.4
- [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.3...v1.13.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.13.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-01-28 13:03:42 +00:00
dependabot[bot] f9f743499f Bump the prod-patch-updates group across 1 directory with 4 updates
Bumps the prod-patch-updates group with 4 updates in the /frontend directory: [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query), [formik](https://github.com/jaredpalmer/formik), [react](https://github.com/facebook/react/tree/HEAD/packages/react) and [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom).


Updates `@tanstack/react-query` from 5.90.6 to 5.90.20
- [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.20/packages/react-query)

Updates `formik` from 2.4.6 to 2.4.9
- [Release notes](https://github.com/jaredpalmer/formik/releases)
- [Commits](https://github.com/jaredpalmer/formik/compare/formik@2.4.6...formik@2.4.9)

Updates `react` from 19.2.3 to 19.2.4
- [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.4/packages/react)

Updates `react-dom` from 19.2.3 to 19.2.4
- [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.4/packages/react-dom)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.90.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: formik
  dependency-version: 2.4.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: react
  dependency-version: 19.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: react-dom
  dependency-version: 19.2.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-01-28 05:00:34 +00:00
Jamie Curnow 534afe6067 Implement suggestion from #5216 hopefully rectifying https -> forced https hosts 2026-01-28 14:04:32 +10:00
jc21 9580903f5d Merge pull request #5239 from NginxProxyManager/dependabot/npm_and_yarn/backend/apidevtools/swagger-parser-12.1.0
Bump @apidevtools/swagger-parser from 10.1.1 to 12.1.0 in /backend
2026-01-28 13:39:51 +10:00
dependabot[bot] df81c8425f Bump @apidevtools/swagger-parser from 10.1.1 to 12.1.0 in /backend
Bumps [@apidevtools/swagger-parser](https://github.com/APIDevTools/swagger-parser) from 10.1.1 to 12.1.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/v10.1.1...v12.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-28 02:56:17 +00:00
Jamie Curnow b6f421c5fc Update biome schema 2026-01-28 12:54:55 +10:00
jc21 c1ef3a3795 Merge pull request #5238 from NginxProxyManager/dependabot/npm_and_yarn/backend/chalk-5.6.2
Bump chalk from 4.1.2 to 5.6.2 in /backend
2026-01-28 07:45:10 +10:00
jc21 0aad939ccc Merge pull request #5221 from NginxProxyManager/dependabot/npm_and_yarn/test/prod-minor-updates-9ff43a5ae3
Bump @quobix/vacuum from 0.19.4 to 0.23.4 in /test in the prod-minor-updates group
2026-01-28 07:44:41 +10:00
jc21 7e092e265c Merge pull request #5222 from NginxProxyManager/dependabot/npm_and_yarn/backend/prod-minor-updates-61aa9782cd
Bump the prod-minor-updates group in /backend with 4 updates
2026-01-28 07:44:20 +10:00
jc21 cd01a2ee6b Merge pull request #5233 from NginxProxyManager/dependabot/npm_and_yarn/frontend/dev-patch-updates-dcc4fa4550
Bump the dev-patch-updates group across 1 directory with 3 updates
2026-01-28 07:43:59 +10:00
jc21 9e6720561a Merge pull request #5234 from NginxProxyManager/dependabot/npm_and_yarn/test/prod-patch-updates-cda2baf714
Bump the prod-patch-updates group across 1 directory with 5 updates
2026-01-28 07:43:46 +10:00
dependabot[bot] c50f0a144e Bump the prod-minor-updates group in /backend with 4 updates
Bumps the prod-minor-updates group in /backend with 4 updates: [liquidjs](https://github.com/harttle/liquidjs), [mysql2](https://github.com/sidorares/node-mysql2), [objection](https://github.com/vincit/objection.js) and [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg).


Updates `liquidjs` from 10.6.1 to 10.24.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.6.1...v10.24.0)

Updates `mysql2` from 3.15.3 to 3.16.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.15.3...v3.16.1)

Updates `objection` from 3.0.1 to 3.1.5
- [Commits](https://github.com/vincit/objection.js/compare/3.0.1...3.1.5)

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

---
updated-dependencies:
- dependency-name: liquidjs
  dependency-version: 10.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: mysql2
  dependency-version: 3.16.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: objection
  dependency-version: 3.1.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: pg
  dependency-version: 8.17.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-01-27 13:02:36 +00:00
dependabot[bot] 2a9c1df3cb Bump chalk from 4.1.2 to 5.6.2 in /backend
Bumps [chalk](https://github.com/chalk/chalk) from 4.1.2 to 5.6.2.
- [Release notes](https://github.com/chalk/chalk/releases)
- [Commits](https://github.com/chalk/chalk/compare/v4.1.2...v5.6.2)

---
updated-dependencies:
- dependency-name: chalk
  dependency-version: 5.6.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-27 13:02:11 +00:00
dependabot[bot] ef6391f22e Bump @quobix/vacuum in /test in the prod-minor-updates group
Bumps the prod-minor-updates group in /test with 1 update: [@quobix/vacuum](https://github.com/daveshanley/vacuum).


Updates `@quobix/vacuum` from 0.19.4 to 0.23.4
- [Release notes](https://github.com/daveshanley/vacuum/releases)
- [Commits](https://github.com/daveshanley/vacuum/compare/v0.19.4...v0.23.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-27 11:42:28 +00:00
dependabot[bot] 0f46337710 Bump the dev-patch-updates group across 1 directory with 3 updates
Bumps the dev-patch-updates group with 3 updates in the /frontend directory: [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome), [@testing-library/react](https://github.com/testing-library/react-testing-library) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `@biomejs/biome` from 2.3.2 to 2.3.13
- [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.3.13/packages/@biomejs/biome)

Updates `@testing-library/react` from 16.3.0 to 16.3.2
- [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.0...v16.3.2)

Updates `vitest` from 4.0.6 to 4.0.18
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.18/packages/vitest)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.3.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@testing-library/react"
  dependency-version: 16.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: vitest
  dependency-version: 4.0.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-27 11:42:25 +00:00
dependabot[bot] 1b84b8ace2 Bump the prod-patch-updates group across 1 directory with 5 updates
Bumps the prod-patch-updates group with 5 updates in the /test directory:

| Package | From | To |
| --- | --- | --- |
| [axios](https://github.com/axios/axios) | `1.13.1` | `1.13.3` |
| [eslint](https://github.com/eslint/eslint) | `9.39.0` | `9.39.2` |
| [eslint-plugin-cypress](https://github.com/cypress-io/eslint-plugin-cypress) | `5.2.0` | `5.2.1` |
| [form-data](https://github.com/form-data/form-data) | `4.0.4` | `4.0.5` |
| [mocha](https://github.com/mochajs/mocha) | `11.7.4` | `11.7.5` |



Updates `axios` from 1.13.1 to 1.13.3
- [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.1...v1.13.3)

Updates `eslint` from 9.39.0 to 9.39.2
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.0...v9.39.2)

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

Updates `form-data` from 4.0.4 to 4.0.5
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.4...v4.0.5)

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

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.13.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: eslint
  dependency-version: 9.39.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: eslint-plugin-cypress
  dependency-version: 5.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: form-data
  dependency-version: 4.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: mocha
  dependency-version: 11.7.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-01-27 11:41:51 +00:00
Jamie Curnow 8ea8286cec More cypress fixes 2026-01-27 14:02:23 +10:00
Jamie Curnow 7ca48f876b Ugh cypress changed their exec result format. 2026-01-27 11:55:54 +10:00
Jamie Curnow 7c3c59c79f Fix cypress logger 2026-01-27 11:41:12 +10:00
Jamie Curnow ef7f444404 Update docker image to match js version 2026-01-27 11:27:21 +10:00
Jamie Curnow f509e0bdba Missing export 2026-01-27 11:26:54 +10:00
Jamie Curnow 9b7af474bb Cypress ... 2026-01-27 11:22:16 +10:00
Jamie Curnow 28982b8bc2 Updated config files for cypress 2026-01-27 10:46:30 +10:00
jc21 19e654b998 Merge pull request #5228 from NginxProxyManager/dependabot/npm_and_yarn/frontend/dev-minor-updates-79aa50ef1e
Bump the dev-minor-updates group in /frontend with 6 updates
2026-01-27 08:48:40 +10:00
Jamie Curnow eaf9f5ab1e Linting/sorting for lang 2026-01-27 08:45:57 +10:00
Jamie Curnow 4af0a968f0 Cypress module conversion and updated chalk 2026-01-27 08:45:23 +10:00
jc21 df06eb6c2f Merge pull request #5204 from NginxProxyManager/dependabot/npm_and_yarn/frontend/lodash-4.17.23
Bump lodash from 4.17.21 to 4.17.23 in /frontend
2026-01-27 08:06:38 +10:00
jc21 74360cc9b3 Merge pull request #5205 from NginxProxyManager/dependabot/npm_and_yarn/test/lodash-4.17.23
Bump lodash from 4.17.21 to 4.17.23 in /test
2026-01-27 08:06:29 +10:00
jc21 16a301fc64 Merge pull request #5227 from NginxProxyManager/dependabot/npm_and_yarn/backend/knex-3.1.0
Bump knex from 2.4.2 to 3.1.0 in /backend
2026-01-27 08:02:18 +10:00
dependabot[bot] 2d774124dc Bump the dev-minor-updates group in /frontend with 6 updates
Bumps the dev-minor-updates group in /frontend with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@formatjs/cli](https://github.com/formatjs/formatjs) | `6.7.4` | `6.12.0` |
| [@tanstack/react-query-devtools](https://github.com/TanStack/query/tree/HEAD/packages/react-query-devtools) | `5.90.2` | `5.91.2` |
| [happy-dom](https://github.com/capricorn86/happy-dom) | `20.0.10` | `20.3.7` |
| [sass](https://github.com/sass/dart-sass) | `1.93.3` | `1.97.3` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.1.12` | `7.3.1` |
| [vite-plugin-checker](https://github.com/fi3ework/vite-plugin-checker) | `0.11.0` | `0.12.0` |


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

Updates `@tanstack/react-query-devtools` from 5.90.2 to 5.91.2
- [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.91.2/packages/react-query-devtools)

Updates `happy-dom` from 20.0.10 to 20.3.7
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.0.10...v20.3.7)

Updates `sass` from 1.93.3 to 1.97.3
- [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.93.3...1.97.3)

Updates `vite` from 7.1.12 to 7.3.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.1/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.1/packages/vite)

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

---
updated-dependencies:
- dependency-name: "@formatjs/cli"
  dependency-version: 6.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: "@tanstack/react-query-devtools"
  dependency-version: 5.91.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: happy-dom
  dependency-version: 20.3.7
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: sass
  dependency-version: 1.97.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: vite
  dependency-version: 7.3.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: vite-plugin-checker
  dependency-version: 0.12.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-01-26 21:21:07 +00:00
dependabot[bot] 124737bbc6 Bump knex from 2.4.2 to 3.1.0 in /backend
Bumps [knex](https://github.com/knex/knex) from 2.4.2 to 3.1.0.
- [Release notes](https://github.com/knex/knex/releases)
- [Changelog](https://github.com/knex/knex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/knex/knex/compare/2.4.2...3.1.0)

---
updated-dependencies:
- dependency-name: knex
  dependency-version: 3.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-26 21:19:32 +00:00
jc21 d5d222ef2d Merge pull request #5217 from NginxProxyManager/dependabot/github_actions/actions/stale-10
Bump actions/stale from 9 to 10
2026-01-27 07:19:17 +10:00
jc21 b96e932c64 Merge pull request #5218 from NginxProxyManager/dependabot/npm_and_yarn/backend/dev-patch-updates-166e475698
Bump @biomejs/biome from 2.3.2 to 2.3.12 in /backend in the dev-patch-updates group
2026-01-27 07:18:25 +10:00
jc21 d09cb2884c Merge pull request #5225 from NginxProxyManager/dependabot/npm_and_yarn/backend/nodemon-3.1.11
Bump nodemon from 2.0.22 to 3.1.11 in /backend
2026-01-27 07:18:11 +10:00
jc21 71deabcc67 Merge pull request #5219 from NginxProxyManager/dependabot/npm_and_yarn/backend/prod-patch-updates-1dc931d47a
Bump jsonwebtoken from 9.0.2 to 9.0.3 in /backend in the prod-patch-updates group
2026-01-27 07:17:20 +10:00
jc21 a78039b65f Merge pull request #5226 from NginxProxyManager/dependabot/npm_and_yarn/test/cypress-15.9.0
Bump cypress from 14.5.4 to 15.9.0 in /test
2026-01-27 07:16:48 +10:00
jc21 48acbd33ab Merge pull request #5231 from NginxProxyManager/dependabot/npm_and_yarn/frontend/vite-tsconfig-paths-6.0.5
Bump vite-tsconfig-paths from 5.1.4 to 6.0.5 in /frontend
2026-01-27 07:16:05 +10:00
dependabot[bot] 32cabc0f83 Bump vite-tsconfig-paths from 5.1.4 to 6.0.5 in /frontend
Bumps [vite-tsconfig-paths](https://github.com/aleclarson/vite-tsconfig-paths) from 5.1.4 to 6.0.5.
- [Release notes](https://github.com/aleclarson/vite-tsconfig-paths/releases)
- [Commits](https://github.com/aleclarson/vite-tsconfig-paths/compare/v5.1.4...v6.0.5)

---
updated-dependencies:
- dependency-name: vite-tsconfig-paths
  dependency-version: 6.0.5
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-25 12:33:54 +00:00
dependabot[bot] 03a82cd861 Bump cypress from 14.5.4 to 15.9.0 in /test
Bumps [cypress](https://github.com/cypress-io/cypress) from 14.5.4 to 15.9.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/v14.5.4...v15.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-25 12:31:34 +00:00
dependabot[bot] 5f19f7125e Bump nodemon from 2.0.22 to 3.1.11 in /backend
Bumps [nodemon](https://github.com/remy/nodemon) from 2.0.22 to 3.1.11.
- [Release notes](https://github.com/remy/nodemon/releases)
- [Commits](https://github.com/remy/nodemon/compare/v2.0.22...v3.1.11)

---
updated-dependencies:
- dependency-name: nodemon
  dependency-version: 3.1.11
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-25 12:31:28 +00:00
dependabot[bot] 8d35644190 Bump jsonwebtoken in /backend in the prod-patch-updates group
Bumps the prod-patch-updates group in /backend with 1 update: [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken).


Updates `jsonwebtoken` from 9.0.2 to 9.0.3
- [Changelog](https://github.com/auth0/node-jsonwebtoken/blob/master/CHANGELOG.md)
- [Commits](https://github.com/auth0/node-jsonwebtoken/compare/v9.0.2...v9.0.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-25 12:30:49 +00:00
dependabot[bot] ad2e4c8afe Bump @biomejs/biome in /backend in the dev-patch-updates group
Bumps the dev-patch-updates group in /backend with 1 update: [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome).


Updates `@biomejs/biome` from 2.3.2 to 2.3.12
- [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.3.12/packages/@biomejs/biome)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.3.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-25 12:30:27 +00:00
dependabot[bot] 69f9031447 Bump actions/stale from 9 to 10
Bumps [actions/stale](https://github.com/actions/stale) from 9 to 10.
- [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/v9...v10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-25 12:30:04 +00:00
jc21 3308a308df Merge pull request #5185 from Lokowitz/add-dependa-config
Add Dependabot config
2026-01-25 22:29:28 +10:00
jc21 59b0e75324 Merge pull request #5200 from toviszsolt/lang-hungarian
Add Hungarian language support and help documentation
2026-01-25 22:14:50 +10:00
dependabot[bot] 727bc944ea Bump lodash from 4.17.21 to 4.17.23 in /frontend
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-25 12:10:48 +00:00
dependabot[bot] a0ef0d9048 Bump lodash from 4.17.21 to 4.17.23 in /test
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-25 12:10:44 +00:00
jc21 d2e346c912 Merge pull request #5203 from NginxProxyManager/dependabot/npm_and_yarn/frontend/lodash-es-4.17.23
Bump lodash-es from 4.17.21 to 4.17.23 in /frontend
2026-01-25 22:09:59 +10:00
jc21 32a716b3a9 Merge pull request #5206 from NginxProxyManager/dependabot/npm_and_yarn/backend/lodash-4.17.23
Bump lodash from 4.17.21 to 4.17.23 in /backend
2026-01-25 22:09:32 +10:00
Zsolt Tovis ef6918947c fix: update (2) Hungarian translations for consistency and clarity.
- Clarification of the translation of action.add-location
2026-01-22 07:49:07 +01:00
dependabot[bot] 2deb5447d6 Bump lodash from 4.17.21 to 4.17.23 in /backend
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-21 23:43:45 +00:00
dependabot[bot] 1bb29259ea Bump lodash-es from 4.17.21 to 4.17.23 in /frontend
Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-21 23:07:00 +00:00
Zsolt Tovis fa20c7d8a4 fix: update Hungarian translations for consistency and clarity.
- Fine-tuning of some Hungarian language-specific expressions.
2026-01-20 18:44:40 +01:00
Zsolt TövisandCopilot 4ed17fef01 Update frontend/src/locale/src/hu.json
Typo-fix: GitHub

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-20 16:56:46 +01:00
Zsolt Tovis fe316252f1 Add Hungarian language support and help documentation
- Integrated Hungarian translations into the IntlProvider and lang-list.
- Added Hungarian help documentation for various topics including Access Lists, Certificates, Proxy Hosts, and more.
- Updated locale options to include Hungarian language.
2026-01-20 16:38:06 +01:00
jc21 7747db994d Merge pull request #5087 from xJayMorex/update-cloudns
Fixed #4715 by updating certbot-dns-cloudns
2026-01-18 20:00:15 +10:00
jc21 9ffced265b Merge pull request #5038 from orhnplt/feature/turkish-locale
Add Turkish locale and help documentation
2026-01-18 19:59:29 +10:00
Lokowitz 50cf275328 split directories 2026-01-18 07:00:46 +00:00
Lokowitz 7bcc34dea9 add dependabot config 2026-01-18 06:52:30 +00:00
Orhan Polat 131e5fea4f fix: remove duplicate locales in lang-list 2026-01-16 12:15:13 +03:00
Orhan Polat 4e412f18bb fix: resolve lint issues in IntlProvider and HelpDoc 2026-01-16 11:59:34 +03:00
Orhan Polat bb0a50eccb chore: trigger CI 2026-01-16 11:45:34 +03:00
Orhan Polat 4185665570 Add Turkish locale and help documentation 2026-01-16 11:44:18 +03:00
jc21 9ea6fee3ce Merge pull request #4930 from blinkerfluessigkeit/lang-de
Update German translations
2026-01-15 09:58:07 +10:00
jc21 7ee9a3c9f0 Merge pull request #4952 from GedasMirak/develop
Add french translation
2026-01-15 09:56:51 +10:00
blinkerfluessigkeit afb196e5b6 Update German translations 2026-01-14 12:41:41 +01:00
GedasMirak 0b464ac9fd Add french locale 2026-01-14 15:01:33 +10:00
jc21 f3efaae320 Merge pull request #5141 from NginxProxyManager/develop
v2.13.6
2026-01-14 14:30:49 +10:00
jc21 7b3c1fd061 Merge branch 'master' into develop 2026-01-14 13:47:51 +10:00
Jamie Curnow ee42202348 Bump version 2026-01-14 13:34:17 +10:00
Jamie Curnow c1ad7788f1 Changed 2fa delete from body to query for code
as per best practices
2026-01-14 13:24:38 +10:00
Jamie Curnow d33bb02c74 Add missing params to swagger 2026-01-14 12:46:30 +10:00
Jamie Curnow 462c134751 2fa work slight refactor
- use existing access mechanisms for validation
- adds swagger/schema and validation of incoming payload
2026-01-14 11:45:12 +10:00
jc21 b7dfaddbb1 Merge pull request #4970 from zdzichu6969/develop
Polish Translation Fixes
2026-01-14 07:33:49 +10:00
jc21 11ee4f0820 Merge pull request #4965 from archettitechnology/develop
Update Italian locale message for empty objects
2026-01-14 07:32:07 +10:00
jc21 19970a4220 Merge pull request #5095 from aindriu80/develop
feat: (i18n) Added Irish translation
2026-01-14 07:26:10 +10:00
jc21 59bac3b468 Merge pull request #5005 from NginxProxyManager/dependabot/npm_and_yarn/backend/express-4.22.0
Bump express from 4.21.2 to 4.22.0 in /backend
2026-01-13 23:35:27 +10:00
jc21 48753fb101 Merge pull request #5136 from NginxProxyManager/dependabot/npm_and_yarn/docs/mdast-util-to-hast-13.2.1
Bump mdast-util-to-hast from 13.2.0 to 13.2.1 in /docs
2026-01-13 23:35:13 +10:00
dependabot[bot] 2a3978ae3f Bump mdast-util-to-hast from 13.2.0 to 13.2.1 in /docs
Bumps [mdast-util-to-hast](https://github.com/syntax-tree/mdast-util-to-hast) from 13.2.0 to 13.2.1.
- [Release notes](https://github.com/syntax-tree/mdast-util-to-hast/releases)
- [Commits](https://github.com/syntax-tree/mdast-util-to-hast/compare/13.2.0...13.2.1)

---
updated-dependencies:
- dependency-name: mdast-util-to-hast
  dependency-version: 13.2.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-13 13:28:52 +00:00
dependabot[bot] 4ce5da5930 Bump express from 4.21.2 to 4.22.0 in /backend
Bumps [express](https://github.com/expressjs/express) from 4.21.2 to 4.22.0.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.22.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.2...4.22.0)

---
updated-dependencies:
- dependency-name: express
  dependency-version: 4.22.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-13 13:26:06 +00:00
jc21 89d3756ee6 Merge pull request #5118 from mobilandi/develop
Add DNS plugin for All-Inkl provider
2026-01-13 23:19:00 +10:00
Jamie Curnow 58c63096e4 Skip color output for vitest in ci 2026-01-13 22:55:19 +10:00
Jamie Curnow b01a22c393 Fix frontend locale tests after date-fns changed intl formatting
and also attempt to format dates in locale
2026-01-13 22:42:42 +10:00
Jamie Curnow 9c25410331 Fix locale sort not to use sponge 2026-01-13 22:15:54 +10:00
jc21 b3a901bbc5 Merge pull request #5015 from NginxProxyManager/dependabot/npm_and_yarn/backend/jws-3.2.3
Bump jws from 3.2.2 to 3.2.3 in /backend
2026-01-13 15:18:41 +10:00
jc21 3e3396ba9a Update lang-list.json 2026-01-13 15:05:13 +10:00
jc21 3eb493bb8b Merge pull request #5022 from dupsatou/add-dns-plugin-support-he-ddns
Add Hurricane Electric DDNS plugin configuration
2026-01-13 14:53:51 +10:00
jc21 8c8221a352 Merge pull request #5037 from vtj-mizuno/fix-japanese-translate
Fix Japanese translate
2026-01-13 14:53:07 +10:00
jc21 582681e3ff Merge pull request #5080 from bzuro/develop
Change visibility to permission_visibility in report.js
2026-01-13 14:52:45 +10:00
jc21 52fae6d35f Merge pull request #5084 from lacamera/security/CVE-2025-55182
security: bump react to 19.2.3 to fix CVE-2025-55182 (#5020)
2026-01-13 14:50:39 +10:00
jc21 6c0ea835ce Merge branch 'develop' into develop 2026-01-13 14:46:35 +10:00
jc21 fb52655374 Merge pull request #5103 from CamelT0E/develop
Update German locale message from 'German' to 'Deutsch'
2026-01-13 14:43:42 +10:00
Jamie Curnow 336726db8d Backend yarn lock updates 2026-01-13 14:40:10 +10:00
jc21 4a7853163e Merge pull request #5107 from teguh02/develop
feat(i18n): add Bahasa Indonesia translations and help documentation
2026-01-13 14:32:18 +10:00
jc21 b30f8e47e2 Merge pull request #5109 from piotrfx/develop
Add TOTP-based two-factor authentication
2026-01-13 14:30:48 +10:00
jc21 6fa30840be Merge pull request #5114 from Shotz5/develop
Added logging for streams based on port
2026-01-13 14:18:13 +10:00
jc21 05726aaab9 Merge pull request #5119 from manisto/develop
Added support for DNS challenges with Simply.com
2026-01-13 14:14:38 +10:00
jc21 f85bb79f13 Merge pull request #5121 from KalebCheng/feature/certificate-key-type-selection
Add option to select RSA or ECDSA key type when creating certificates
2026-01-13 14:13:22 +10:00
kk.cheng 471b62c7fe Add option to select RSA or ECDSA key type when creating certificates 2026-01-07 19:13:12 +08:00
Gert Rue Brigsted 55a1e0a4e7 Added support for DNS challenges with Simply.com 2026-01-04 21:50:47 +01:00
mobilandi f25afa3590 Change version constraint for certbot-dns-kas 2026-01-03 23:08:34 +01:00
mobilandi 9211ba6d1a Add DNS plugin for All-Inkl provider 2026-01-03 23:06:25 +01:00
Alex Kitsul aeb44244a7 Added logging for streams based on port 2025-12-30 21:44:29 -08:00
piotrfx d2d204ab8e Trigger CI 2025-12-28 12:04:35 +01:00
piotrfx 427afa55b4 Add TOTP-based two-factor authentication
- Add 2FA setup, enable, disable, and backup code management
- Integrate 2FA challenge flow into login process
- Add frontend modal for 2FA configuration
- Support backup codes for account recovery
2025-12-28 11:58:30 +01:00
Teguh Rijanandi bbe98a639a Add Indonesian locale and help docs 2025-12-27 22:35:17 +07:00
Aindriú Mac Giolla Eoin f0c0b465d9 Removiving 0x200b - Zero width space 2025-12-20 17:53:05 +00:00
Aindriú Mac Giolla Eoin 6c2f6a9d39 Fixing plural/iolra issue 2025-12-19 11:43:18 +00:00
Aindriú Mac Giolla Eoin 2f6e3ad804 Added Irish translation 2025-12-18 18:21:14 +00:00
John Taylor c9f453714b Fixed #4715 by updating certbot-dns-cloudns 2025-12-15 17:03:29 +01:00
Francesco La Camera 5e6ead1eee security: bump react to 19.2.3 to fix CVE-2025-55182 (#5020) 2025-12-15 09:54:18 +01:00
bzuro da519e72ba Change visibility to permission_visibility in report.js
fix for issue #2014
when even administrator with all_items visibility got 0 proxy hosts in dashboard.
2025-12-14 00:35:22 +01:00
Hajime MIZUNO b13ebb2247 Fix Japanese translate 2025-12-10 23:28:53 +09:00
dupsatou 6b322582b9 Add Hurricane Electric DDNS plugin configuration
Add support for dns verification using Hurricane Electric DDNS credentials as a more secure way over account root credentials.  More information available here: https://github.com/mafredri/certbot-dns-he-ddns
2025-12-08 09:45:11 -06:00
angioletto 7fe5070337 Merge branch 'NginxProxyManager:develop' into develop 2025-12-06 14:56:52 +01:00
CamelT0E 1b8f1fbb79 Update German locale message from 'German' to 'Deutsch' 2025-12-06 01:30:56 +01:00
dependabot[bot] 4abea1247d Bump jws from 3.2.2 to 3.2.3 in /backend
Bumps [jws](https://github.com/brianloveswords/node-jws) from 3.2.2 to 3.2.3.
- [Release notes](https://github.com/brianloveswords/node-jws/releases)
- [Changelog](https://github.com/auth0/node-jws/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianloveswords/node-jws/compare/v3.2.2...v3.2.3)

---
updated-dependencies:
- dependency-name: jws
  dependency-version: 3.2.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-04 16:58:07 +00:00
Mateusz Gruszczyński 073ee95e56 change 2025-12-02 12:57:09 +01:00
Jamie Curnow fec8b3b083 Show full swagger validation errors in tests 2025-12-02 07:09:54 +10:00
Mateusz Gruszczyński 168078eb40 changes 2025-11-26 10:54:30 +01:00
Mateusz Gruszczyński 2c9f8f4d64 changes 2025-11-26 10:50:41 +01:00
Mateusz Gruszczyński 8403a0c761 changes 2025-11-26 10:42:48 +01:00
jc21 d18c8cf4f1 Merge pull request #4979 from abinas-hdb/develop
Add Korean Locale
2025-11-26 14:04:31 +10:00
abinas bf4eab541a Update index.ts
Fix missing 'ko' in index.ts
2025-11-26 11:57:05 +09:00
jc21 f9edcb10e6 Merge pull request #4987 from Bare7a/patch-1
Update Locale README.md to include HelpDoc/index.tsx
2025-11-26 08:35:54 +10:00
jc21 ba43c144f6 Merge branch 'develop' into develop 2025-11-26 08:35:32 +10:00
jc21 896951f6cd Merge pull request #4985 from Bare7a/bg-locale
Add Bulgarian Language Support
2025-11-26 08:33:55 +10:00
jc21 865b566ea6 Merge pull request #4989 from alatalo/develop
Add Glesys certbot plugin
2025-11-26 08:32:03 +10:00
Ville Alatalo 45bc44c6fa Add Glesys certbot plugin 2025-11-25 07:49:24 +02:00
Bare7a 4ff402fff4 Update Locale README.md to include HelpDoc/index.tsx 2025-11-24 18:28:49 +02:00
Bare7a 1c6f54fa3c Changed the port translation 2025-11-24 18:23:40 +02:00
Bare7a e8ca72fb6a Adds bg inside HelpDoc index.ts file 2025-11-24 18:14:16 +02:00
Bare7a 4712633568 After Translate 2025-11-24 18:07:46 +02:00
Bare7a a1fb54c394 Before Translating 2025-11-24 18:04:50 +02:00
angioletto 927e57257b Merge branch 'NginxProxyManager:develop' into develop 2025-11-21 17:03:47 +01:00
abinas e353a66556 Update IntlProvider.tsx 2025-11-22 00:33:27 +09:00
abinas 991bddf891 Add Korean translation 2025-11-22 00:18:36 +09:00
abinas c076ad145c Add Korean translation 2025-11-22 00:18:19 +09:00
abinas 80cf4406d5 Update Korean language support 2025-11-22 00:15:08 +09:00
abinas 3cb124d5a0 Update Korean language support 2025-11-22 00:14:45 +09:00
abinas 03b0513a24 Add Korean translation 2025-11-22 00:12:33 +09:00
jc21 0528d65317 Merge pull request #4964 from xluyenx/develop
Correct Vietnam flag
2025-11-20 11:54:55 +10:00
jc21 f9991084fc Merge pull request #4966 from 7heMech/7heMech-patch-1
Increase max propagation seconds to 7200
2025-11-20 11:54:15 +10:00
Mateusz Gruszczyński 56875bba52 pretty :) 2025-11-19 21:23:23 +01:00
Mateusz Gruszczyński b55f51bd63 fixes1 in pl 2025-11-19 15:10:56 +01:00
7heMech 20e2d5ffb3 Increase max propagation seconds to 7200 2025-11-19 13:00:06 +02:00
Mateusz Gruszczyński 86b7394620 fixes1 2025-11-19 11:01:25 +01:00
Mateusz Gruszczyński 91a1f39c02 fixes1 2025-11-19 10:53:55 +01:00
angioletto 5c114e9db7 Update Italian locale message for empty objects
Wrong translation of line 431
2025-11-19 09:56:05 +01:00
Mateusz Gruszczyński fec9bffe29 fixes1 2025-11-19 09:13:55 +01:00
Louis Tran's e3cdc8bb30 Update IntlProvider.tsx 2025-11-19 11:37:20 +07:00
Louis Tran's ba79eefe5e Merge pull request #1 from xluyenx/xluyenx-patch-1
Update IntlProvider.tsx
2025-11-19 11:30:49 +07:00
Louis Tran's bb94ce75c1 Update IntlProvider.tsx
Correct Vietnam flag
2025-11-19 11:27:42 +07:00
jc21 847c58b170 Merge pull request #4956 from NginxProxyManager/develop
v2.13.5
2025-11-18 21:13:24 +10:00
jc21 89b8b747e1 Merge branch 'master' into develop 2025-11-18 19:46:03 +10:00
Jamie Curnow 3231023513 Bump version 2025-11-18 19:42:54 +10:00
Jamie Curnow dc89635971 Fix up locales, optimised some functions 2025-11-18 19:38:21 +10:00
jc21 cfa98361d1 Merge pull request #4955 from NginxProxyManager/lang-nl
Add Dutch language - resolves #4935
2025-11-18 19:03:48 +10:00
Jelcoo c2177abe39 Add language to frontend settings & correct some translations 2025-11-18 19:00:00 +10:00
Jelcoo 2c6d614597 Add HelpDoc translations 2025-11-18 18:58:26 +10:00
Jelcoo 484ce8db3c Add Dutch language 2025-11-18 18:57:40 +10:00
jc21 2c11c0c7e2 Merge pull request #4937 from archettitechnology/develop
Add Italian Language Support
2025-11-18 18:50:52 +10:00
jc21 f1039ce2ef Merge pull request #4928 from 7heMech/develop
UI/UX improvements
2025-11-18 18:37:22 +10:00
jc21 d49ff6e7c2 Merge pull request #4934 from zdzichu6969/develop
fix(i18n): replace "Dodaj" with "Nowy" for better Polish grammar and typo Role
2025-11-18 18:30:24 +10:00
jc21 a87f24c9dc Merge pull request #4940 from vsc55/issues_4939
Fix issues #4939, #4938
2025-11-18 18:29:04 +10:00
jc21 decdfec447 Merge branch 'develop' into develop 2025-11-18 18:27:00 +10:00
jc21 32ab3faf57 Merge pull request #4943 from NginxProxyManager/dependabot/npm_and_yarn/backend/js-yaml-4.1.1
Bump js-yaml from 4.1.0 to 4.1.1 in /backend
2025-11-18 18:24:31 +10:00
jc21 c7f999fa7a Merge pull request #4944 from gjssss/patch-1
Fix message for GitHub fork reference in zh.json
2025-11-18 18:24:14 +10:00
jc21 de7d3b0d19 Merge pull request #4950 from dominhhieu1405/develop
Add Vietnamese Support
2025-11-18 18:22:43 +10:00
jc21 2d4b7399c0 Merge pull request #4953 from dodog/develop
Update Slovak language label
2025-11-18 18:20:03 +10:00
Jamie Curnow 316b758455 Tweaks to cypress suite 2025-11-18 07:21:06 +10:00
Jozef Gaal 890d06c863 Update Slovak language label 2025-11-17 21:07:56 +01:00
dominhhieu1405 81f2aa17d4 Add vietnamese 2025-11-17 22:28:08 +07:00
Jamie Curnow 9b4c34915c Update porkbun certbot plugin 2025-11-17 08:46:31 +10:00
Javier Pastor fce569ca21 Modify host.forward-port to avoid line breaks 2025-11-16 01:53:48 +01:00
Json Gao 87ec9c4bdf Fix message for GitHub fork reference in zh.json 2025-11-15 20:09:19 +08:00
dependabot[bot] 2650648d68 Bump js-yaml from 4.1.0 to 4.1.1 in /backend
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-15 10:40:46 +00:00
7heMech fdc0c29f28 Improve modals in dark mode via a dark backdrop and shadow. 2025-11-14 15:51:54 +02:00
angioletto 6cae088432 Rename ProxyHost.md to ProxyHosts.md
i think have problem with letter s ahaha
2025-11-14 08:16:41 +01:00
angioletto 9d8c4cc30b Rename DeadHost.md to DeadHosts.md 2025-11-14 08:14:26 +01:00
angioletto 66ebecdb43 Merge branch 'develop' into develop 2025-11-14 08:01:32 +01:00
angioletto 60f3ee03c0 Fix typo in file name from 'indes.ts' to 'index.ts'
typing error
2025-11-14 08:00:30 +01:00
jc21 a4d54a0291 Merge pull request #4932 from kraineff/develop
Update Russian locale
2025-11-14 16:58:05 +10:00
angioletto 7536b1b1c9 Merge branch 'develop' into develop 2025-11-14 07:19:32 +01:00
angioletto 5288fbd7af Update index.ts 2025-11-14 07:18:14 +01:00
jc21 2c630bbdca Merge branch 'develop' into develop 2025-11-14 15:25:10 +10:00
Javier Pastor 0ec1a09c30 fix issues #4939
add other translations
2025-11-14 00:06:18 +01:00
Jamie Curnow 118c4793e3 Amend locale readme 2025-11-14 08:34:16 +10:00
Jamie Curnow d7384c568f Fix #4933 when cert may not have domain names 2025-11-14 08:33:42 +10:00
angioletto 0bcfe0bba6 Add Italian language support to lang-list.json 2025-11-13 21:12:52 +01:00
angioletto 74cbfb2c58 Create indes.ts to export HelpDoc modules 2025-11-13 21:12:15 +01:00
angioletto 8ef65caa5a Add Italian documentation for Streams feature 2025-11-13 21:11:19 +01:00
angioletto bc341c1dff Add RedirectionHosts.md with explanation in Italian 2025-11-13 21:10:36 +01:00
angioletto 5fc9febf1f Update title of ProxyHost.md in Italian 2025-11-13 21:09:40 +01:00
angioletto b23ceebfd8 Add Italian documentation for ProxyHost 2025-11-13 21:09:23 +01:00
angioletto c281fc54a1 Add Italian HelpDoc for 404 Host explanation 2025-11-13 21:08:50 +01:00
angioletto d0f7dc5b48 Add Italian HelpDoc for certificate options 2025-11-13 21:07:26 +01:00
angioletto fb53df862e Add Italian documentation for Access Lists 2025-11-13 21:03:33 +01:00
angioletto 8d8463ae41 Add Italian language support to HelpDoc 2025-11-13 20:57:52 +01:00
angioletto 8774cfe5f9 Add Italian locale to check-locales 2025-11-13 20:56:42 +01:00
angioletto 4ca5cadd19 Add Italian language support to IntlProvider 2025-11-13 20:55:35 +01:00
angioletto 45a8d50e03 Add IT Translation 2025-11-13 20:52:42 +01:00
7heMech 960d4bfe6f Revert change which should have no effect on theory 2025-11-13 14:51:00 +02:00
7heMech 8c3c964c52 Fix page offset 2025-11-13 14:27:55 +02:00
7heMech afd6134a3e Get rid of logo flicker and improve LCP 2025-11-13 14:04:37 +02:00
Alexey Krainev 9b2d60e67b Update Russian locale 2025-11-13 16:58:04 +05:00
7heMech 9807e25d45 Remove unused import 2025-11-13 12:49:48 +02:00
7heMech 824c895f52 Remove cn where not needed 2025-11-13 12:47:01 +02:00
7heMech 7f9b9dfea4 Fix for dropdown menus being clipped by table-responsive containers. 2025-11-13 12:06:36 +02:00
Mateusz Gruszczyński d848ba9f65 Fixed typo: corrected 'role' to proper Polish declension 'rola' and 'nowy' 2025-11-13 09:05:07 +01:00
Mateusz Gruszczyński 47db5c9aa6 Fixed typo: corrected 'role' to proper Polish declension 'rola' 2025-11-13 08:57:30 +01:00
Jamie Curnow 79a9653b26 Remove the compiled lang files, compile on dev server and when building in ci
This avoids confusion for new translators
2025-11-13 14:21:32 +10:00
Jamie Curnow e5aae1f365 Fix openapi schema format 2025-11-13 11:51:13 +10:00
Jamie Curnow 8959190d32 Change docker ci expose format for docker 28 :/ 2025-11-13 11:37:58 +10:00
Jamie Curnow 7e875eb27a Change docker ci expose format for docker 28 :/ 2025-11-13 11:35:11 +10:00
Jamie Curnow cf7306e766 Tweaks to showing new version available
- Added frontend translation for english
- Moved frontend api logic to hook and backend api space
- Added swagger schema for the new api endpoint
- Moved backend logic to its own internal file
- Added user agent header to github api check
- Added cypress integration test for version check api
- Added a memory cache item from github check to avoid hitting it too
  much
2025-11-13 11:20:31 +10:00
7heMech 1c442dcce6 True mobile layout with responsive table rows (sticky header) 2025-11-13 02:44:24 +02:00
7heMech dadd10f89b Fixed my troubles with text wrap 2025-11-13 02:21:58 +02:00
jc21 8838dabe8a Merge pull request #4906 from sopex/develop
Available upgrade notification
2025-11-13 10:15:33 +10:00
7heMech 75c012b558 Fix linter error 2025-11-13 01:58:48 +02:00
7heMech 9be1381ffe Uhhh, I didn't like the Standard User lol 2025-11-13 01:46:39 +02:00
7heMech f40fe56572 Add new section with theme and locale pickers. 2025-11-13 01:40:34 +02:00
Konstantinos Spartalis b4fd242eb7 remove 1 2025-11-13 00:48:49 +02:00
7heMech 911476f82f Delay before close for smooth feel. 2025-11-13 00:46:36 +02:00
7heMech 963125f963 Space scandal retified (hopefully) 2025-11-13 00:45:07 +02:00
7heMech e86a34f2f3 Close menu after navigation. 2025-11-13 00:30:45 +02:00
jc21 6ce9567e48 Merge pull request #4816 from fhennig42/azure-dns
Bump certbot-azure-dns version
2025-11-13 07:13:11 +10:00
jc21 f02145c5ef Merge pull request #4925 from NginxProxyManager/develop
v2.13.4
2025-11-13 06:57:28 +10:00
7heMech 66fa08fd8e Add profile back to main app on mobile 2025-11-12 18:12:58 +02:00
7heMech d783cc3b90 Remove unused styles 2025-11-12 17:58:54 +02:00
7heMech 17cc75fe7d Fix language and theme selectors on mobile and desktop 2025-11-12 17:43:46 +02:00
Konstantinos Spartalis 15394c6532 trigger Jenkins that failed due to internet connection problems 2025-11-12 15:50:11 +02:00
Konstantinos Spartalis 2d6252d75d https.get 2025-11-12 15:45:59 +02:00
jc21 adee0e39de Merge branch 'master' into develop 2025-11-12 23:02:28 +10:00
Jamie Curnow 5dde98cf3e Updates to polish locale after running through automated scripts 2025-11-12 23:01:40 +10:00
jc21 c41451618e Merge pull request #4924 from zdzichu6969/develop
Add Polish locale
2025-11-12 22:59:23 +10:00
jc21 1a3d45f6bc Merge branch 'develop' into develop 2025-11-12 22:14:28 +10:00
jc21 2ea54975b6 Merge pull request #4922 from NginxProxyManager/dodog-slovak
Add Slovak language by @dodog in #4911
2025-11-12 22:13:05 +10:00
Mateusz Gruszczyński 0373017a9f Add Polish locale 2025-11-12 13:10:29 +01:00
Florian Hennig b043e70fc0 add azure-mgmt-dns fix version as dependency 2025-11-12 13:00:34 +01:00
Jamie Curnow 2b5182d339 Add Slovak language by @dodog in #4911 2025-11-12 21:49:04 +10:00
jc21 3c5ff81a54 Merge pull request #4910 from 7heMech/develop
Add scheme back in destination
2025-11-12 20:48:56 +10:00
jc21 8aa46c1f40 Merge pull request #4921 from NginxProxyManager/Firfr-chinese
Add Chinese language 添加中文
2025-11-12 20:47:15 +10:00
Jamie Curnow b26db50ae7 Adds cn to check locales script 2025-11-12 20:26:22 +10:00
firfe d66bb2104a Add the new translation for "redirection-host.forward-http-code". 2025-11-12 20:23:36 +10:00
firfe 8e900dbc92 Add Chinese HelpDoc 2025-11-12 20:23:34 +10:00
firfe 66aac3eb3e Add Chinese 中文 2025-11-12 20:22:57 +10:00
jc21 221c3eddbc Merge pull request #4919 from lastsamurai26/develop
Fix: German grammatical change
2025-11-12 20:16:58 +10:00
Jamie Curnow 8460b28597 Bump version 2025-11-12 20:13:18 +10:00
Frank 0344bb3c19 fix: Grammatical change
fix: Grammatical change
2025-11-12 10:47:53 +01:00
Frank 1a36bdce76 fix: Grammatical change
fix: Grammatical change
2025-11-12 10:47:51 +01:00
Jamie Curnow 06d7db43f7 Fix Russion locale, compiled file was comitted without a source file 2025-11-12 18:59:37 +10:00
jc21 4557244744 Merge pull request #4870 from kraineff/develop
Add Russian Support
2025-11-12 18:51:43 +10:00
jc21 f649288098 Merge branch 'develop' into develop 2025-11-12 18:39:05 +10:00
jc21 28df6db52b Merge pull request #4848 from Oka-Tak/develop
Add Japanese language support and translations
2025-11-12 18:36:18 +10:00
jc21 eee749652c Merge pull request #4917 from lastsamurai26/develop
Fix: wrong translate and adding missing translations
2025-11-12 18:13:08 +10:00
jc21 f6aa25b9b3 Merge branch 'develop' into develop 2025-11-12 18:12:10 +10:00
Frank 40db26b686 Merge branch 'NginxProxyManager:develop' into develop 2025-11-12 08:06:36 +01:00
Frank f36d4e6906 Fix: CustomCertificateModal Wrong displayname
Fix: https://github.com/NginxProxyManager/nginx-proxy-manager/issues/4912 Wrong Locale for Custom
2025-11-12 07:47:06 +01:00
Frank 86c7cbddab Add certificate renewal message in German locale
Fix: add missing translation for renew certificates
2025-11-12 07:34:44 +01:00
Frank e52975bf6c Translate 'Renew Certificate' to German
Fix: add missing translation for renew certificates
2025-11-12 07:34:42 +01:00
Frank ff792f76af Add translation for 'Renew Certificate' in de.json
Fix: Add missing translation für renew Certificate
2025-11-12 07:32:34 +01:00
Jamie Curnow 711f312b71 Fix up language inconsistenties 2025-11-12 16:30:22 +10:00
Jamie Curnow 9f0f89ff03 Fix wrong translation for EN 2025-11-12 15:13:14 +10:00
jc21 f3633cb696 Merge pull request #4850 from TeenBiscuits/lang-spanish
Add Spanish language support and translations
2025-11-12 15:12:28 +10:00
Pablo Portas López 8773ce25d7 Merge branch 'develop' into lang-spanish 2025-11-12 02:14:09 +01:00
jc21 c3954e9845 Merge pull request #4824 from lastsamurai26/develop
Add German Support
2025-11-12 08:52:07 +10:00
Konstantinos Spartalis 87eef10ff8 remove useCallback logic 2025-11-11 18:30:23 +02:00
Konstantinos Spartalis dc03ad8239 minimal changes 2025-11-11 17:42:46 +02:00
7heMech 441a7262cd Add scheme back in destination 2025-11-11 12:54:01 +00:00
Pablo Portas López 1600599410 Fix column.http-code translation 2025-11-11 13:53:45 +01:00
Pablo Portas López 74d381e7fa Add missing spanish translation 2025-11-11 13:50:23 +01:00
Konstantinos Spartalis ae5faa75fa backend test 2025-11-11 10:35:00 +02:00
Frank ba79bbc750 Update German translation for HTTP code
fix: Updated column http code
2025-11-11 08:56:32 +01:00
Frank a7231777aa FIX: Update HTTP code message in German locale
fix: Updated column http code
2025-11-11 08:56:20 +01:00
jc21 2578105f86 Merge pull request #4907 from NginxProxyManager/develop
v2.13.3
2025-11-11 16:54:38 +10:00
Frank 3a6b221b0c Add HTTP Code translation to German locale
new: redirection-host.forward-http-code added
2025-11-11 07:13:13 +01:00
Frank 12b000abb9 Add HTTP Code message to German locale
new: redirection-host.forward-http-code added
2025-11-11 07:12:57 +01:00
jc21 39c9bbb167 Merge branch 'master' into develop 2025-11-11 16:06:05 +10:00
jc21 30c2781a02 Merge pull request #4765 from mamasch19/develop
add MC-HOST24 DNS plugin
2025-11-11 16:05:32 +10:00
Jamie Curnow 53e78dcc17 Bump version 2025-11-11 16:01:06 +10:00
jc21 62092b2ddc Merge pull request #4859 from 7heMech/develop
Fix hamburger menu on mobile
2025-11-11 15:37:12 +10:00
Jamie Curnow 2c26ed8b11 Revert "Fix #4831 mobile header menu not working"
This reverts commit 4bd545c88e.
2025-11-11 15:36:46 +10:00
jc21 e3f5cd9a58 Merge pull request #4871 from prospo/develop
chore: Bump certbot-dns-leaseweb to 1.0.3
2025-11-11 15:24:11 +10:00
jc21 fba14817e7 Merge pull request #4894 from eduardpaul/feat-fix-pass_auth-template
Update _access.conf to fix access_list.pass_auth logic
2025-11-11 15:23:22 +10:00
Jamie Curnow 6825a9773b Fix #4854 Added missing forward http code for redirections 2025-11-11 15:17:43 +10:00
Jamie Curnow 8bc3078d87 Fix initial setup user bug, taking the fix from #4836 2025-11-11 14:52:39 +10:00
Jamie Curnow 8aeb2fa661 Fix #4692, #4856 - stick with auto for scheme in db, change it to $scheme when rendering 2025-11-11 14:46:25 +10:00
Jamie Curnow 4bd545c88e Fix #4831 mobile header menu not working 2025-11-11 14:05:26 +10:00
Jamie Curnow 7f0cce944d Relax the email validation in frontend 2025-11-11 08:54:48 +10:00
Pablo Portas López 7cde6ee7ca Add Spanish Test 2025-11-10 21:58:23 +01:00
Pablo Portas López df1b414c2e Delete Spanish Test 2025-11-10 21:58:01 +01:00
Konstantinos Spartalis b6dbb68ef3 Update SiteFooter.tsx 2025-11-10 20:42:52 +02:00
Konstantinos Spartalis b434bba12f remove hardcoded version number 2025-11-10 20:37:25 +02:00
Konstantinos Spartalis f1d7203212 v2 2025-11-10 19:57:55 +02:00
Konstantinos Spartalis 990ba28831 Update SiteFooter.tsx 2025-11-10 19:43:38 +02:00
Jamie Curnow 311d6a1541 Tweaks to CI stack for postgres 2025-11-10 10:30:16 +10:00
mamasch19 5e7276e65b Add MC-HOST24 DNS plugin configuration
added the MC-HOST24 configuration to the new plugin file
2025-11-09 22:31:48 +01:00
Eduard Paul 2bcb942f93 Update _access.conf to ensure Authorization header remove when pass_auth = false or 0
Fixing prev commit as it's negative logic.
2025-11-09 21:02:18 +01:00
Eduard Paul b3dac3df08 Update _access.conf to fix access_list.pass_auth logic
Wrong logic to pass auth as header: when disabled (pass_auth=0) credentials are included in Authorization header. However as soon as you enable (pass_auth=1) they are not.
2025-11-09 20:11:33 +01:00
jc21 64c5a863f8 Merge pull request #4878 from NginxProxyManager/develop
v2.13.2
2025-11-09 21:16:26 +10:00
Jamie Curnow cd94863850 Bump version 2025-11-09 20:25:10 +10:00
Emil fd1d33444a chore: Bump certbot-dns-leaseweb to 1.0.3 2025-11-08 14:39:23 +01:00
Alexey Krainev 5aa56c63d4 Fixes & New Strings 2025-11-08 17:15:24 +05:00
Alexey Krainev 8fdb6091f3 More strings 2025-11-08 15:51:39 +05:00
Alexey Krainev 58182fcbdf Add Russian case 2025-11-08 15:08:08 +05:00
Alexey Krainev b3b1e94b8c Add Russian Support 2025-11-08 15:02:05 +05:00
7heMech 6fa2d6a98a Fix hamburger menu on mobile 2025-11-07 19:34:43 +00:00
Jamie Curnow 3c252db46f Fixes #4844 with more defensive date parsing 2025-11-07 21:37:22 +10:00
Jamie Curnow 8eba31913f Remove pebble certs, they removed the dockerhub image that had armv7 support.
The ghcr image doesn't have it, so it was causing builds to fail.
2025-11-07 11:18:53 +10:00
Jamie Curnow e4e3415120 Safer handling of backend date formats
and add frontend testing
2025-11-07 11:15:15 +10:00
Jamie Curnow a03bb7ebce Remove Jenkinsfile, managed in other repo now 2025-11-07 10:54:21 +10:00
Jamie Curnow 51e25d1a40 Attempt to fix race condition with database instantiation 2025-11-07 09:46:00 +10:00
Pablo Portas López 123f7d1999 Add Spanish language support and translations 2025-11-06 01:04:02 +01:00
Takahisa-Okawa 9de40f067b Add Japanese language support and translations
Co-authored-by: kz2870 <kz2870@users.noreply.github.com>
2025-11-05 22:25:15 +09:00
Frank b21d6d9d78 Fix German translations
Fix: German translations
2025-11-05 08:09:10 +01:00
Frank bf1ad15ed7 Update de.json
fix: typos
2025-11-05 08:08:50 +01:00
Frank 1209303a1d Update DeadHosts.md
fix: translation "Umgangssprachlich"
2025-11-05 08:00:15 +01:00
Frank cd3a09ebf6 Update Certificates.md
fix: typo
2025-11-05 07:59:45 +01:00
Frank d0e20d4f1b Update de.json
fix: typo dark and light mode
2025-11-05 07:57:11 +01:00
Frank ceb098fcfe Fix typo in German locale for min character length
fix: typo mainimale should be minimale
2025-11-05 07:53:56 +01:00
Frank 639ba3a525 Update de.json
fix: typo 
fix: translate Location with Pfad
2025-11-05 07:52:28 +01:00
Florian Hennig a85b5f664f Bump version after rebase 2025-11-04 20:03:09 +01:00
Frank 7e28d8a5d6 Add files via upload
add german
2025-11-03 17:51:48 +01:00
Frank 8991e88ff3 Update de.json 2025-11-03 14:22:13 +01:00
Frank e2a8ffa2d3 Add files via upload
Add German
2025-11-03 14:18:08 +01:00
514 changed files with 37783 additions and 8264 deletions

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
# Force LF everywhere: these files run inside Linux containers
* text=auto eol=lf
*.png binary
*.jpg binary
*.ico binary
*.woff binary
*.woff2 binary
+40
View File
@@ -0,0 +1,40 @@
<!--
WARNINGS:
1. An AI bot will comment on this PR! If you don't get a perfect score or don't
address any issues raised, your PR won't get merged!
2. Any PR without a description will be closed. The title is not enough!
3. 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
+104
View File
@@ -0,0 +1,104 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/backend"
schedule:
interval: "weekly"
groups:
dev-patch-updates:
dependency-type: "development"
update-types:
- "patch"
dev-minor-updates:
dependency-type: "development"
update-types:
- "minor"
prod-patch-updates:
dependency-type: "production"
update-types:
- "patch"
prod-minor-updates:
dependency-type: "production"
update-types:
- "minor"
- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "weekly"
groups:
dev-patch-updates:
dependency-type: "development"
update-types:
- "patch"
dev-minor-updates:
dependency-type: "development"
update-types:
- "minor"
prod-patch-updates:
dependency-type: "production"
update-types:
- "patch"
prod-minor-updates:
dependency-type: "production"
update-types:
- "minor"
- package-ecosystem: "npm"
directory: "/docs"
schedule:
interval: "weekly"
groups:
dev-patch-updates:
dependency-type: "development"
update-types:
- "patch"
dev-minor-updates:
dependency-type: "development"
update-types:
- "minor"
prod-patch-updates:
dependency-type: "production"
update-types:
- "patch"
prod-minor-updates:
dependency-type: "production"
update-types:
- "minor"
- package-ecosystem: "npm"
directory: "/test"
schedule:
interval: "weekly"
groups:
dev-patch-updates:
dependency-type: "development"
update-types:
- "patch"
dev-minor-updates:
dependency-type: "development"
update-types:
- "minor"
prod-patch-updates:
dependency-type: "production"
update-types:
- "patch"
prod-minor-updates:
dependency-type: "production"
update-types:
- "minor"
- package-ecosystem: "docker"
directory: "/docker"
schedule:
interval: "weekly"
groups:
updates:
update-types:
- "patch"
- "minor"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@v11
with:
stale-issue-label: 'stale'
stale-pr-label: 'stale'
+3
View File
@@ -8,3 +8,6 @@ test/node_modules
*/node_modules
docker/dev/dnsrouter-config.json.tmp
docker/dev/resolv.conf
.claude
test/__pycache__
+1 -1
View File
@@ -1 +1 @@
2.13.1
2.17.0
Vendored
-285
View File
@@ -1,285 +0,0 @@
import groovy.transform.Field
@Field
def shOutput = ""
def buildxPushTags = ""
pipeline {
agent {
label 'docker-multiarch'
}
options {
buildDiscarder(logRotator(numToKeepStr: '5'))
disableConcurrentBuilds()
ansiColor('xterm')
}
environment {
IMAGE = 'nginx-proxy-manager'
BUILD_VERSION = getVersion()
MAJOR_VERSION = '2'
BRANCH_LOWER = "${BRANCH_NAME.toLowerCase().replaceAll('\\\\', '-').replaceAll('/', '-').replaceAll('\\.', '-')}"
BUILDX_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}"
COMPOSE_INTERACTIVE_NO_CLI = 1
}
stages {
stage('Environment') {
parallel {
stage('Master') {
when {
branch 'master'
}
steps {
script {
buildxPushTags = "-t docker.io/jc21/${IMAGE}:${BUILD_VERSION} -t docker.io/jc21/${IMAGE}:${MAJOR_VERSION} -t docker.io/jc21/${IMAGE}:latest"
}
}
}
stage('Other') {
when {
not {
branch 'master'
}
}
steps {
script {
// Defaults to the Branch name, which is applies to all branches AND pr's
buildxPushTags = "-t docker.io/nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}"
}
}
}
stage('Versions') {
steps {
sh 'cat frontend/package.json | jq --arg BUILD_VERSION "${BUILD_VERSION}" \'.version = $BUILD_VERSION\' | sponge frontend/package.json'
sh 'echo -e "\\E[1;36mFrontend Version is:\\E[1;33m $(cat frontend/package.json | jq -r .version)\\E[0m"'
sh 'cat backend/package.json | jq --arg BUILD_VERSION "${BUILD_VERSION}" \'.version = $BUILD_VERSION\' | sponge backend/package.json'
sh 'echo -e "\\E[1;36mBackend Version is:\\E[1;33m $(cat backend/package.json | jq -r .version)\\E[0m"'
sh 'sed -i -E "s/(version-)[0-9]+\\.[0-9]+\\.[0-9]+(-green)/\\1${BUILD_VERSION}\\2/" README.md'
}
}
stage('Docker Login') {
steps {
withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh 'docker login -u "${duser}" -p "${dpass}"'
}
}
}
}
}
stage('Builds') {
parallel {
stage('Project') {
steps {
script {
// Frontend and Backend
def shStatusCode = sh(label: 'Checking and Building', returnStatus: true, script: '''
set -e
./scripts/ci/frontend-build > ${WORKSPACE}/tmp-sh-build 2>&1
./scripts/ci/test-and-build > ${WORKSPACE}/tmp-sh-build 2>&1
''')
shOutput = readFile "${env.WORKSPACE}/tmp-sh-build"
if (shStatusCode != 0) {
error "${shOutput}"
}
}
}
post {
always {
sh 'rm -f ${WORKSPACE}/tmp-sh-build'
}
failure {
npmGithubPrComment("CI Error:\n\n```\n${shOutput}\n```", true)
}
}
}
stage('Docs') {
steps {
dir(path: 'docs') {
sh 'yarn install'
sh 'yarn build'
}
}
}
}
}
stage('Test Sqlite') {
environment {
COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_sqlite"
COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.sqlite.yml'
}
when {
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
steps {
sh 'rm -rf ./test/results/junit/*'
sh './scripts/ci/fulltest-cypress'
}
post {
always {
// Dumps to analyze later
sh 'mkdir -p debug/sqlite'
sh 'docker logs $(docker compose ps --all -q fullstack) > debug/sqlite/docker_fullstack.log 2>&1'
sh 'docker logs $(docker compose ps --all -q stepca) > debug/sqlite/docker_stepca.log 2>&1'
sh 'docker logs $(docker compose ps --all -q pdns) > debug/sqlite/docker_pdns.log 2>&1'
sh 'docker logs $(docker compose ps --all -q pdns-db) > debug/sqlite/docker_pdns-db.log 2>&1'
sh 'docker logs $(docker compose ps --all -q dnsrouter) > debug/sqlite/docker_dnsrouter.log 2>&1'
junit 'test/results/junit/*'
sh 'docker compose down --remove-orphans --volumes -t 30 || true'
}
unstable {
dir(path: 'test/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}
}
}
stage('Test Mysql') {
environment {
COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_mysql"
COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.mysql.yml'
}
when {
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
steps {
sh 'rm -rf ./test/results/junit/*'
sh './scripts/ci/fulltest-cypress'
}
post {
always {
// Dumps to analyze later
sh 'mkdir -p debug/mysql'
sh 'docker logs $(docker compose ps --all -q fullstack) > debug/mysql/docker_fullstack.log 2>&1'
sh 'docker logs $(docker compose ps --all -q stepca) > debug/mysql/docker_stepca.log 2>&1'
sh 'docker logs $(docker compose ps --all -q pdns) > debug/mysql/docker_pdns.log 2>&1'
sh 'docker logs $(docker compose ps --all -q pdns-db) > debug/mysql/docker_pdns-db.log 2>&1'
sh 'docker logs $(docker compose ps --all -q dnsrouter) > debug/mysql/docker_dnsrouter.log 2>&1'
junit 'test/results/junit/*'
sh 'docker compose down --remove-orphans --volumes -t 30 || true'
}
unstable {
dir(path: 'test/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}
}
}
stage('Test Postgres') {
environment {
COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_postgres"
COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.postgres.yml'
}
when {
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
steps {
sh 'rm -rf ./test/results/junit/*'
sh './scripts/ci/fulltest-cypress'
}
post {
always {
// Dumps to analyze later
sh 'mkdir -p debug/postgres'
sh 'docker logs $(docker compose ps --all -q fullstack) > debug/postgres/docker_fullstack.log 2>&1'
sh 'docker logs $(docker compose ps --all -q stepca) > debug/postgres/docker_stepca.log 2>&1'
sh 'docker logs $(docker compose ps --all -q pdns) > debug/postgres/docker_pdns.log 2>&1'
sh 'docker logs $(docker compose ps --all -q pdns-db) > debug/postgres/docker_pdns-db.log 2>&1'
sh 'docker logs $(docker compose ps --all -q dnsrouter) > debug/postgres/docker_dnsrouter.log 2>&1'
sh 'docker logs $(docker compose ps --all -q db-postgres) > debug/postgres/docker_db-postgres.log 2>&1'
sh 'docker logs $(docker compose ps --all -q authentik) > debug/postgres/docker_authentik.log 2>&1'
sh 'docker logs $(docker compose ps --all -q authentik-redis) > debug/postgres/docker_authentik-redis.log 2>&1'
sh 'docker logs $(docke rcompose ps --all -q authentik-ldap) > debug/postgres/docker_authentik-ldap.log 2>&1'
junit 'test/results/junit/*'
sh 'docker compose down --remove-orphans --volumes -t 30 || true'
}
unstable {
dir(path: 'test/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}
}
}
stage('MultiArch Build') {
when {
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
steps {
sh "./scripts/buildx --push ${buildxPushTags}"
}
}
stage('Docs / Comment') {
parallel {
stage('Docs Job') {
when {
allOf {
branch pattern: "^(develop|master)\$", comparator: "REGEXP"
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
}
steps {
build wait: false, job: 'nginx-proxy-manager-docs', parameters: [string(name: 'docs_branch', value: "$BRANCH_NAME")]
}
}
stage('PR Comment') {
when {
allOf {
changeRequest()
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
}
steps {
script {
npmGithubPrComment("""Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/nginxproxymanager/${IMAGE}-dev):
```
nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}
```
> [!NOTE]
> Ensure you backup your NPM instance before testing this image! Especially if there are database changes.
> This is a different docker image namespace than the official image.
> [!WARNING]
> Changes and additions to DNS Providers require verification by at least 2 members of the community!
""", true)
}
}
}
}
}
}
post {
always {
sh 'echo Reverting ownership'
sh 'docker run --rm -v "$(pwd):/data" jc21/ci-tools chown -R "$(id -u):$(id -g)" /data'
printResult(true)
}
failure {
archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true)
}
unstable {
archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true)
}
}
}
def getVersion() {
ver = sh(script: 'cat .version', returnStdout: true)
return ver.trim()
}
def getCommit() {
ver = sh(script: 'git log -n 1 --format=%h', returnStdout: true)
return ver.trim()
}
+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.1-green.svg?style=for-the-badge">
<img src="https://img.shields.io/badge/version-2.17.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",
},
};
+72 -89
View File
@@ -1,91 +1,74 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.2/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"includes": [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx",
"!**/dist/**/*"
]
},
"formatter": {
"enabled": true,
"indentStyle": "tab",
"indentWidth": 4,
"lineWidth": 120,
"formatWithErrors": true
},
"assist": {
"actions": {
"source": {
"organizeImports": {
"level": "on",
"options": {
"groups": [
":BUN:",
":NODE:",
[
"npm:*",
"npm:*/**"
],
":PACKAGE_WITH_PROTOCOL:",
":URL:",
":PACKAGE:",
[
"/src/*",
"/src/**"
],
[
"/**"
],
[
"#*",
"#*/**"
],
":PATH:"
]
}
}
}
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"useUniqueElementIds": "off"
},
"suspicious": {
"noExplicitAny": "off"
},
"performance": {
"noDelete": "off"
},
"nursery": "off",
"a11y": {
"useSemanticElements": "off",
"useValidAnchor": "off"
},
"style": {
"noParameterAssign": "error",
"useAsConstAssertion": "error",
"useDefaultParameterLast": "error",
"useEnumInitializers": "error",
"useSelfClosingElements": "error",
"useSingleVarDeclarator": "error",
"noUnusedTemplateLiteral": "error",
"useNumberNamespace": "error",
"noInferrableTypes": "error",
"noUselessElse": "error"
}
}
}
"$schema": "https://biomejs.dev/schemas/2.6.6/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "!**/dist/**/*"]
},
"formatter": {
"enabled": true,
"indentStyle": "tab",
"indentWidth": 4,
"lineWidth": 120,
"formatWithErrors": true
},
"assist": {
"actions": {
"source": {
"organizeImports": {
"level": "on",
"options": {
"groups": [
":BUN:",
":NODE:",
["npm:*", "npm:*/**"],
":PACKAGE_WITH_PROTOCOL:",
":URL:",
":PACKAGE:",
["/src/*", "/src/**"],
["/**"],
["#*", "#*/**"],
":PATH:"
]
}
}
}
}
},
"linter": {
"enabled": true,
"rules": {
"preset": "recommended",
"correctness": {
"useUniqueElementIds": "off"
},
"suspicious": {
"noExplicitAny": "off"
},
"performance": {
"noDelete": "off"
},
"nursery": "off",
"a11y": {
"useSemanticElements": "off",
"useValidAnchor": "off"
},
"style": {
"noParameterAssign": "error",
"useAsConstAssertion": "error",
"useDefaultParameterLast": "error",
"useEnumInitializers": "error",
"useSelfClosingElements": "error",
"useSingleVarDeclarator": "error",
"noUnusedTemplateLiteral": "error",
"useNumberNamespace": "error",
"noInferrableTypes": "error",
"noUselessElse": "error"
}
}
}
}
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"
},
+13 -4
View File
@@ -1,5 +1,7 @@
import knex from "knex";
import {configGet, configHas} from "./lib/config.js";
import { configGet, configHas } from "./lib/config.js";
let instance = null;
const generateDbConfig = () => {
if (!configHas("database")) {
@@ -21,8 +23,8 @@ const generateDbConfig = () => {
user: cfg.user,
password: cfg.password,
database: cfg.name,
port: cfg.port,
...(cfg.ssl ? { ssl: cfg.ssl } : {})
port: cfg.port,
...(cfg.ssl ? { ssl: cfg.ssl } : {}),
},
migrations: {
tableName: "migrations",
@@ -30,4 +32,11 @@ const generateDbConfig = () => {
};
};
export default knex(generateDbConfig());
const getInstance = () => {
if (!instance) {
instance = knex(generateDbConfig());
}
return instance;
};
export default getInstance;
+3 -1
View File
@@ -26,7 +26,9 @@ async function appStart() {
})
.then(() => {
internalCertificate.initTimer();
internalIpRanges.initTimer();
if (IP_RANGES_FETCH_ENABLED) {
internalIpRanges.initTimer();
}
const server = app.listen(3000, () => {
logger.info(`Backend PID ${process.pid} listening on port 3000 ...`);
+301
View File
@@ -0,0 +1,301 @@
import crypto from "node:crypto";
import bcrypt from "bcrypt";
import { createGuardrails, generateSecret, generateURI, verify } from "otplib";
import errs from "../lib/error.js";
import authModel from "../models/auth.js";
import internalUser from "./user.js";
const APP_NAME = "Nginx Proxy Manager";
const BACKUP_CODE_COUNT = 8;
/**
* Generate backup codes
* @returns {Promise<{plain: string[], hashed: string[]}>}
*/
const generateBackupCodes = async () => {
const plain = [];
const hashed = [];
for (let i = 0; i < BACKUP_CODE_COUNT; i++) {
const code = crypto.randomBytes(4).toString("hex").toUpperCase();
plain.push(code);
const hash = await bcrypt.hash(code, 10);
hashed.push(hash);
}
return { plain, hashed };
};
const internal2fa = {
/**
* Check if user has 2FA enabled
* @param {number} userId
* @returns {Promise<boolean>}
*/
isEnabled: async (userId) => {
const auth = await internal2fa.getUserPasswordAuth(userId);
return auth?.meta?.totp_enabled === true;
},
/**
* Get 2FA status for user
* @param {Access} access
* @param {number} userId
* @returns {Promise<{enabled: boolean, backup_codes_remaining: number}>}
*/
getStatus: async (access, userId) => {
await access.can("users:password", userId);
await internalUser.get(access, { id: userId });
const auth = await internal2fa.getUserPasswordAuth(userId);
const enabled = auth?.meta?.totp_enabled === true;
let backup_codes_remaining = 0;
if (enabled) {
const backupCodes = auth.meta.backup_codes || [];
backup_codes_remaining = backupCodes.length;
}
return {
enabled,
backup_codes_remaining,
};
},
/**
* Start 2FA setup - store pending secret
*
* @param {Access} access
* @param {number} userId
* @returns {Promise<{secret: string, otpauth_url: string}>}
*/
startSetup: async (access, userId) => {
await access.can("users:password", userId);
const user = await internalUser.get(access, { id: userId });
const secret = generateSecret();
const otpauth_url = generateURI({
issuer: APP_NAME,
label: user.email,
secret: secret,
});
const auth = await internal2fa.getUserPasswordAuth(userId);
// ensure user isn't already setup for 2fa
const enabled = auth?.meta?.totp_enabled === true;
if (enabled) {
throw new errs.ValidationError("2FA is already enabled");
}
const meta = auth.meta || {};
meta.totp_pending_secret = secret;
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
return { secret, otpauth_url };
},
/**
* Enable 2FA after verifying code
*
* @param {Access} access
* @param {number} userId
* @param {string} code
* @returns {Promise<{backup_codes: string[]}>}
*/
enable: async (access, userId, code) => {
await access.can("users:password", userId);
await internalUser.get(access, { id: userId });
const auth = await internal2fa.getUserPasswordAuth(userId);
const secret = auth?.meta?.totp_pending_secret || false;
if (!secret) {
throw new errs.ValidationError("No pending 2FA setup found");
}
const result = await verify({ token: code, secret });
if (!result.valid) {
throw new errs.ValidationError("Invalid verification code");
}
const { plain, hashed } = await generateBackupCodes();
const meta = {
...auth.meta,
totp_secret: secret,
totp_enabled: true,
totp_enabled_at: new Date().toISOString(),
backup_codes: hashed,
};
delete meta.totp_pending_secret;
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
return { backup_codes: plain };
},
/**
* Disable 2FA
*
* @param {Access} access
* @param {number} userId
* @param {string} code
* @returns {Promise<void>}
*/
disable: async (access, userId, code) => {
await access.can("users:password", userId);
await internalUser.get(access, { id: userId });
const auth = await internal2fa.getUserPasswordAuth(userId);
const enabled = auth?.meta?.totp_enabled === true;
if (!enabled) {
throw new errs.ValidationError("2FA is not enabled");
}
const result = await verify({
token: code,
secret: auth.meta.totp_secret,
guardrails: createGuardrails({
MIN_SECRET_BYTES: 10,
}),
});
if (!result.valid) {
throw new errs.AuthError("Invalid verification code");
}
const meta = { ...auth.meta };
delete meta.totp_secret;
delete meta.totp_enabled;
delete meta.totp_enabled_at;
delete meta.backup_codes;
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
},
/**
* Verify 2FA code for login
*
* @param {number} userId
* @param {string} token
* @returns {Promise<boolean>}
*/
verifyForLogin: async (userId, token) => {
const auth = await internal2fa.getUserPasswordAuth(userId);
const secret = auth?.meta?.totp_secret || false;
if (!secret) {
return false;
}
// Try TOTP code first, if it's 6 chars. it will throw errors if it's not 6 chars
// and the backup codes are 8 chars.
if (token.length === 6) {
const result = await verify({
token,
secret,
// These guardrails lower the minimum length requirement for secrets.
// In v12 of otplib the default minimum length is 10 and in v13 it is 16.
// Since there are 2fa secrets in the wild generated with v12 we need to allow shorter secrets
// so people won't be locked out when upgrading.
guardrails: createGuardrails({
MIN_SECRET_BYTES: 10,
}),
});
if (result.valid) {
return true;
}
}
// Try backup codes
const backupCodes = auth?.meta?.backup_codes || [];
for (let i = 0; i < backupCodes.length; i++) {
const match = await bcrypt.compare(token.toUpperCase(), backupCodes[i]);
if (match) {
// Remove used backup code
const updatedCodes = [...backupCodes];
updatedCodes.splice(i, 1);
const meta = { ...auth.meta, backup_codes: updatedCodes };
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
return true;
}
}
return false;
},
/**
* Regenerate backup codes
*
* @param {Access} access
* @param {number} userId
* @param {string} token
* @returns {Promise<{backup_codes: string[]}>}
*/
regenerateBackupCodes: async (access, userId, token) => {
await access.can("users:password", userId);
await internalUser.get(access, { id: userId });
const auth = await internal2fa.getUserPasswordAuth(userId);
const enabled = auth?.meta?.totp_enabled === true;
const secret = auth?.meta?.totp_secret || false;
if (!enabled) {
throw new errs.ValidationError("2FA is not enabled");
}
if (!secret) {
throw new errs.ValidationError("No 2FA secret found");
}
const result = await verify({
token,
secret,
});
if (!result.valid) {
throw new errs.ValidationError("Invalid verification code");
}
const { plain, hashed } = await generateBackupCodes();
const meta = { ...auth.meta, backup_codes: hashed };
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
return { backup_codes: plain };
},
getUserPasswordAuth: async (userId) => {
const auth = await authModel.query().where("user_id", userId).andWhere("type", "password").first();
if (!auth) {
throw new errs.ItemNotFoundError("Auth not found");
}
return auth;
},
};
export default internal2fa;
+128 -74
View File
@@ -2,7 +2,10 @@ import fs from "node:fs";
import batchflow from "batchflow";
import _ from "lodash";
import errs from "../lib/error.js";
import { isValid, unsafeValueError } from "../lib/nginx-values.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 +18,36 @@ 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,20 +81,17 @@ const internalAccessList = {
return true;
});
// Clients
data.clients?.map((client) => {
promises.push(
accessListClientModel.query().insert({
access_list_id: row.id,
address: client.address,
directive: client.directive,
}),
);
return true;
});
await Promise.all(promises);
// Clients
for (const client of data.clients ?? []) {
await accessListClientModel.query().insert({
access_list_id: row.id,
address: client.address,
directive: client.directive,
});
}
// re-fetch with expansions
const freshRow = await internalAccessList.get(
access,
@@ -69,7 +99,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 +184,17 @@ const internalAccessList = {
// Check for clients and add/update/remove them
if (typeof data.clients !== "undefined" && data.clients) {
const clientPromises = [];
data.clients.map((client) => {
if (client.address) {
clientPromises.push(
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);
for (const client of data.clients) {
if (client.address) {
await accessListClientModel.query().insert({
access_list_id: data.id,
address: client.address,
directive: client.directive,
});
}
}
}
@@ -191,13 +213,34 @@ 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 +256,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 +280,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 +307,7 @@ const internalAccessList = {
expand: ["proxy_hosts", "items", "clients"],
});
if (!row || !row.id) {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
@@ -278,28 +317,41 @@ const internalAccessList = {
// 4. audit log
// 1. update row to be deleted
await accessListModel
.query()
.where("id", row.id)
.patch({
is_deleted: 1,
});
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
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 });
}
// 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);
}
}
// 3. reconfigure those hosts from fresh rows, then reload nginx
if (affectedHostIds.size) {
const affectedHosts = 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;
});
await internalNginx.bulkGenerateConfigs("proxy_host", row.proxy_hosts);
.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 +388,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 +430,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 +492,29 @@ 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) {
// One "user:hash" line each; a ":" or line break in the name would corrupt the file
if (!isValid("accessUsername", item.username)) {
reject(unsafeValueError(`Access List #${list.id}: user name is not allowed`));
return;
}
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 +536,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(", ")}]`);
+416 -34
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";
@@ -24,8 +24,163 @@ const certbotCommand = "certbot";
const certbotLogsDir = "/data/logs";
const certbotWorkDir = "/tmp/letsencrypt-lib";
// Certbot output lines that say nothing about why a request failed
const certbotNoise = [
/^Saving debug log/,
/^Ask for help/,
/^See the logfile/,
/^(- )+-?$/,
/^Processing \//,
/^Renewing an existing/,
/^Requesting a certificate/,
/^Plugins selected/,
/^Account registered/,
/^Waiting \d+ seconds/,
/^All renewals failed/,
/^\S+\/fullchain\.pem \(failure\)$/,
/^\d+ renew failure\(s\)/,
];
// Plain-language hints for failures that are common and fixable outside NPM.
// [pattern, hint, dnsProvider?]: a hint with a dnsProvider only applies to certificates using that
// provider, so a generic message (e.g. "Authentication error" from another DNS plugin) isn't
// reported as a Cloudflare problem.
const certbotHints = [
// Checked first: a rate limit is unambiguous and the most useful thing to report
[
/too many (certificates|failed authorizations|new orders)|rateLimited/i,
() => "Let's Encrypt rate limit reached. Wait before retrying (see https://letsencrypt.org/docs/rate-limits/).",
],
[
/expected \S+ to be a symlink/i,
() =>
"This certificate's files in /etc/letsencrypt were copied without their symlinks (live/ must link to archive/). Restore /etc/letsencrypt from a copy made with tar or rsync -a, or delete this certificate and create it again.",
],
[
/Renewal configuration file \S+ is broken/i,
() =>
"This certificate's files in /etc/letsencrypt are damaged or missing. Restore /etc/letsencrypt from a backup, or delete this certificate and create it again.",
],
[
/\b9109\b.*?location: ([0-9a-f.:]+)/i,
(m) => {
const ip = m[1].replace(/\.+$/, "");
return `Cloudflare refused the API token from this server's public IP (${ip}). The token has Client IP Address Filtering: add ${ip} to the token in Cloudflare (My Profile > API Tokens), or remove the filter, then renew again.`;
},
"cloudflare",
],
[
// certbot-dns-cloudflare errors read "<context>: <code> <message>", e.g.
// "Error determining zone_id: 9109 Invalid access token."
/: (6003|6111|9103|9109|10000) [A-Za-z]|Invalid request headers|Invalid access token|Authentication error/i,
() =>
"Cloudflare rejected the API token. Check the token value and that it has Zone > DNS > Edit for this domain's zone.",
"cloudflare",
],
[
/NXDOMAIN|DNS problem/i,
() => "Let's Encrypt could not look up the domain. Check the DNS records of every domain on the certificate.",
],
[
/Timeout during connect|Connection refused|Invalid response from/i,
() =>
"Let's Encrypt could not reach this server over HTTP (port 80) for the challenge. Check that the domain points here and port 80 is open, or use a DNS challenge.",
],
];
/**
* The values in a DNS credentials file ("key = value" lines), for redacting from certbot output.
*
* @param {String} [credentials]
* @returns {String[]}
*/
const credentialValues = (credentials) =>
`${credentials || ""}`
.split("\n")
.map((line) => line.replace(/^[^=:]*[=:]/, "").trim())
.filter((value) => value.length >= 4);
/**
* Removes anything that could be a secret from certbot output before it leaves the server.
* DNS plugins can echo provider errors verbatim, including request URLs that carry API keys in
* the query string (e.g. "401 Client Error ... for url: https://...?token=..."), so this strips
* the given credential values, URL query strings and userinfo, and key=value pairs whose name
* looks secret.
*
* @param {String} text
* @param {String[]} [secrets]
* @returns {String}
*/
const redactCertbotOutput = (text, secrets = []) => {
let out = `${text || ""}`;
for (const secret of [...secrets].sort((a, b) => b.length - a.length)) {
out = out.split(secret).join("[REDACTED]");
}
return out
.replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, "$1 [REDACTED]")
.replace(/(\b[a-z][a-z0-9+.-]*:\/\/)[^/\s@]*@/gi, "$1[REDACTED]@")
.replace(/(\b[a-z][a-z0-9+.-]*:\/\/[^\s?#]*)\?[^\s#]*/gi, "$1?[REDACTED]")
.replace(
/\b([\w-]*(?:token|key|secret|password|passwd|pass|auth|credential|signature|sig)[\w-]*)(["']?\s*[=:]\s*["']?)[^\s"'&,;]+/gi,
"$1$2[REDACTED]",
);
};
/**
* Turns certbot's stderr into a message the UI can show: a hint for common causes plus certbot's own reason.
* The output is redacted first (see redactCertbotOutput), since DNS plugins can echo credentials.
*
* @param {String} stderr
* @param {String} [dnsProvider] The certificate's DNS provider, if it uses a DNS challenge
* @param {String[]} [secrets] Values that must never appear in the message
* @returns {String}
*/
const certbotFailureMessage = (stderr, dnsProvider, secrets) => {
const reason = redactCertbotOutput(stderr, secrets)
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !certbotNoise.some((re) => re.test(line)))
.join(" ")
.slice(0, 800);
for (const [pattern, hint, provider] of certbotHints) {
if (provider && provider !== dnsProvider) {
continue;
}
const match = reason.match(pattern);
if (match) {
return `${hint(match)} (Certbot: ${reason})`;
}
}
return reason ? `Certbot failed: ${reason}` : "Certbot failed. See /data/logs/letsencrypt.log for details.";
};
/**
* Runs certbot; a failure becomes a public error with the real reason instead of "Internal Error".
*
* @param {Array} args
* @param {Object} [opts]
* @param {String} [dnsProvider] Passed through so provider-specific hints only show for that provider
* @param {String} [credentials] The DNS credentials file contents, so its values are redacted from errors
* @returns {Promise}
*/
const runCertbot = async (args, opts, dnsProvider, credentials) => {
try {
return await utils.execFile(certbotCommand, args, opts);
} catch (err) {
if (err instanceof error.CommandError) {
const secrets = credentialValues(credentials);
logger.error(redactCertbotOutput(err.message, secrets));
// The raw error is not attached as `previous`: the API error handler returns
// `previous` to the client for certificate routes, and it holds unredacted output.
throw new error.ValidationError(certbotFailureMessage(err.message, dnsProvider, secrets));
}
throw err;
}
};
const omissions = () => {
return ["is_deleted", "owner.is_deleted", "meta.dns_provider_credentials"];
return ["is_deleted", "owner.is_deleted", "meta.dns_provider_credentials", "meta.certificate_key"];
};
const internalCertificate = {
@@ -66,7 +221,7 @@ const internalCertificate = {
.andWhere("provider", "letsencrypt")
.andWhere("expires_on", "<", expirationThreshold)
.then((certificates) => {
if (!certificates || !certificates.length) {
if (!certificates?.length) {
return null;
}
@@ -75,6 +230,7 @@ const internalCertificate = {
* instance of Certbot is already running.'
*/
let sequence = Promise.resolve();
let renewed = 0;
certificates.forEach((certificate) => {
sequence = sequence.then(() =>
@@ -88,7 +244,11 @@ const internalCertificate = {
token: tokenModel(),
},
{ id: certificate.id },
{ reload: false }, // one reload for the whole batch, below
)
.then(() => {
renewed++;
})
.catch((err) => {
// Don't want to stop the train here, just log the error
logger.error(err.message);
@@ -96,7 +256,11 @@ const internalCertificate = {
);
});
return sequence;
return sequence.then(() => {
if (renewed) {
return internalCertificate.reloadAfterRenew();
}
});
})
.then(() => {
logger.info("Completed SSL cert renew process");
@@ -143,7 +307,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 +469,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 +534,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) => {
@@ -395,12 +559,35 @@ const internalCertificate = {
*/
delete: async (access, data) => {
await access.can("certificates:delete", data.id);
const row = await internalCertificate.get(access, { id: data.id });
const hostRelations = ["proxy_hosts", "redirection_hosts", "dead_hosts", "streams"];
const row = await internalCertificate.get(access, { id: data.id, expand: hostRelations });
if (!row || !row.id) {
if (!row?.id) {
throw new error.ItemNotFoundError(data.id);
}
// A certificate that hosts still use can't be deleted: their rendered configs include its
// files, and revoking it would leave them serving a revoked certificate. Hosts may belong
// to other users, so only counts are reported.
const labels = {
proxy_hosts: "proxy host",
redirection_hosts: "redirection host",
dead_hosts: "404 host",
streams: "stream",
};
const usage = hostRelations
.map((rel) => ({ rel, count: row[rel]?.length || 0 }))
.filter((u) => u.count > 0)
.map((u) => `${u.count} ${labels[u.rel]}${u.count === 1 ? "" : "s"}`);
if (usage.length) {
throw new error.ValidationError(
`Certificate is in use by ${usage.join(", ")}. Remove it from them before deleting it.`,
);
}
for (const rel of hostRelations) {
delete row[rel];
}
await certificateModel.query().where("id", row.id).patch({
is_deleted: 1,
});
@@ -416,12 +603,47 @@ const internalCertificate = {
});
if (row.provider === "letsencrypt") {
// Revoke the cert
// Revoke the cert. Errors are logged by revokeLetsEncryptSsl.
await internalCertificate.revokeLetsEncryptSsl(row);
// Certbot leaves the files behind when it can't revoke (e.g. a broken renewal config),
// so remove them either way. No host uses them; that was checked above.
// The row is already deleted and audited, so leftover files are logged, not thrown:
// an error here would report a completed delete as failed, and a retry would 404.
const leftover = internalCertificate.removeLetsEncryptFiles(row.id);
if (leftover.length) {
logger.warn(
`Certificate #${row.id} was deleted, but these files could not be removed and should be deleted by hand: ${leftover.join(", ")}`,
);
}
}
return true;
},
/**
* Removes a certificate's certbot files, as `certbot delete` would. Never throws.
*
* @param {Number} certificateId
* @returns {String[]} paths that could not be removed
*/
removeLetsEncryptFiles: (certificateId) => {
const paths = [
internalCertificate.getLiveCertPath(certificateId),
`/etc/letsencrypt/archive/npm-${certificateId}`,
`/etc/letsencrypt/renewal/npm-${certificateId}.conf`,
`/etc/letsencrypt/credentials/credentials-${certificateId}`,
];
const failed = [];
for (const p of paths) {
try {
fs.rmSync(p, { recursive: true, force: true });
} catch (err) {
logger.error(`Could not remove ${p}: ${err.message}`);
failed.push(p);
}
}
return failed;
},
/**
* All Certs
*
@@ -545,13 +767,15 @@ const internalCertificate = {
/**
* Validates that the certs provided are good.
* No access required here, nothing is changed or stored.
* Nothing is changed or stored, but the files go through openssl, so it needs the same access as creating one.
*
* @param {Access} access
* @param {Object} data
* @param {Object} data.files
* @returns {Promise}
*/
validate: (data) => {
validate: async (access, data) => {
await access.can("certificates:create");
// Put file contents into an object
const files = {};
_.map(data.files, (file, name) => {
@@ -600,27 +824,75 @@ const internalCertificate = {
throw new error.ValidationError("Cannot upload certificates for this type of provider");
}
const validations = await internalCertificate.validate(data);
const validations = await internalCertificate.validate(access, data);
if (typeof validations.certificate === "undefined") {
throw new error.ValidationError("Certificate file was not provided");
}
// row came through get(), which omits meta.certificate_key so it never goes out over
// the API. update() replaces meta wholesale, so build on the stored meta (read from the
// model directly) or an upload without a key would erase the stored one.
const stored = await certificateModel.query().where("id", row.id).first();
const meta = { ...(stored?.meta || {}) };
const uploaded = [];
_.map(data.files, (file, name) => {
if (internalCertificate.allowedSslFiles.indexOf(name) !== -1) {
row.meta[name] = file.data.toString();
meta[name] = file.data.toString();
uploaded.push(name);
}
});
// Fail before anything is written: without a key, writeCustomCert would overwrite
// fullchain.pem and then fail on privkey.pem, leaving nginx with a mismatched pair.
if (!meta.certificate_key) {
throw new error.ValidationError("Certificate Key file was not provided");
}
// The key may be the stored one (a certificate-only upload), so check the pair belongs
// together; a mismatch would make nginx -t fail for every host on the next reload.
await internalCertificate.checkKeyMatchesCertificate(meta.certificate, meta.certificate_key);
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],
meta: _.clone(row.meta), // Prevent the update method from changing this value that we'll use later
domain_names: validations.certificate.cn ? [validations.certificate.cn] : [],
meta: _.clone(meta), // Prevent the update method from changing this value that we'll use later
});
certificate.meta = row.meta;
certificate.meta = meta;
await internalCertificate.writeCustomCert(certificate);
return _.pick(row.meta, internalCertificate.allowedSslFiles);
// Only echo back what was uploaded in this request, never the stored key
return _.pick(meta, uploaded);
},
/**
* Throws unless the private key is the one the certificate was issued for, by comparing the
* public key derived from each.
*
* @param {String} certificate PEM certificate (the leaf comes first)
* @param {String} privateKey PEM private key
* @returns {Promise}
*/
checkKeyMatchesCertificate: async (certificate, privateKey) => {
const certPath = await tempWrite(certificate);
const keyPath = await tempWrite(privateKey);
try {
const [certPub, keyPub] = await Promise.all([
utils.execFile("openssl", ["x509", "-in", certPath, "-pubkey", "-noout"]),
utils.execFile("openssl", ["pkey", "-in", keyPath, "-pubout"]),
]);
if (!certPub.trim() || certPub.trim() !== keyPub.trim()) {
throw new error.ValidationError("Certificate does not match the Certificate Key");
}
} catch (err) {
if (err instanceof error.ValidationError) {
throw err;
}
throw new error.ValidationError("Could not check that the Certificate matches the Certificate Key");
} finally {
fs.rmSync(certPath, { force: true });
fs.rmSync(keyPath, { force: true });
}
},
/**
@@ -630,7 +902,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 +932,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 +955,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 +1053,7 @@ const internalCertificate = {
const args = [
"certonly",
"-n", // non-interactive
"--config",
letsencryptConfig,
"--work-dir",
@@ -798,12 +1073,17 @@ const internalCertificate = {
certificate.domain_names.join(","),
];
// Add key-type parameter if specified
if (certificate.meta?.key_type) {
args.push("--key-type", certificate.meta.key_type);
}
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id);
args.push(...adds.args);
logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
const result = await utils.execFile(certbotCommand, args, adds.opts);
const result = await runCertbot(args, adds.opts);
logger.success(result);
return result;
},
@@ -829,6 +1109,7 @@ const internalCertificate = {
const args = [
"certonly",
"-n", // non-interactive
"--config",
letsencryptConfig,
"--work-dir",
@@ -851,26 +1132,47 @@ const internalCertificate = {
if (hasConfigArg) {
args.push(`--${dnsPlugin.full_plugin_name}-credentials`, credentialsLocation);
}
if (certificate.meta.propagation_seconds !== undefined) {
// The route53 plugin waits for the change through the AWS API and has no propagation option
if (certificate.meta.propagation_seconds !== undefined && certificate.meta.dns_provider !== "route53") {
args.push(
`--${dnsPlugin.full_plugin_name}-propagation-seconds`,
certificate.meta.propagation_seconds.toString(),
);
}
// Add key-type parameter if specified
if (certificate.meta?.key_type) {
args.push("--key-type", certificate.meta.key_type);
}
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);
args.push(...adds.args);
logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
try {
const result = await utils.execFile(certbotCommand, args, adds.opts);
const result = await runCertbot(
args,
adds.opts,
certificate.meta.dns_provider,
certificate.meta.dns_provider_credentials,
);
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;
}
},
@@ -880,7 +1182,8 @@ const internalCertificate = {
* @param {Number} data.id
* @returns {Promise}
*/
renew: async (access, data) => {
renew: async (access, data, options = {}) => {
const reload = options.reload !== false;
await access.can("certificates:update", data);
const certificate = await internalCertificate.get(access, data);
@@ -906,12 +1209,34 @@ const internalCertificate = {
meta: updatedCertificate,
});
// The renewal has succeeded and been recorded at this point. A reload failure
// (e.g. an unrelated host's config is broken) must not turn it into an error;
// nginx picks the new files up on the next successful reload.
if (reload) {
await internalCertificate.reloadAfterRenew();
}
return updatedCertificate;
}
throw new error.ValidationError("Only Let'sEncrypt certificates can be renewed");
},
/**
* Reloads nginx so it serves renewed certificate files. Errors are logged, not thrown.
*
* @returns {Promise<Boolean>} whether the reload succeeded
*/
reloadAfterRenew: async () => {
try {
await internalNginx.reload();
return true;
} catch (err) {
logger.error(`Certificate renewed but nginx could not be reloaded: ${err.message}`);
return false;
}
},
/**
* @param {Object} certificate the certificate row
* @returns {Promise}
@@ -938,12 +1263,17 @@ const internalCertificate = {
"--disable-hook-validation",
];
// Add key-type parameter if specified
if (certificate.meta?.key_type) {
args.push("--key-type", certificate.meta.key_type);
}
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);
args.push(...adds.args);
logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
const result = await utils.execFile(certbotCommand, args, adds.opts);
const result = await runCertbot(args, adds.opts);
logger.info(result);
return result;
},
@@ -962,6 +1292,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",
@@ -979,14 +1346,29 @@ const internalCertificate = {
"--no-random-sleep-on-renew",
];
// Add key-type parameter if specified
if (certificate.meta?.key_type) {
args.push("--key-type", certificate.meta.key_type);
}
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);
args.push(...adds.args);
logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
const result = await utils.execFile(certbotCommand, args, adds.opts);
logger.info(result);
return result;
try {
const result = await runCertbot(args, adds.opts, certificate.meta.dns_provider, credentials);
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, () => {});
}
}
},
/**
@@ -1018,7 +1400,7 @@ const internalCertificate = {
logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`);
try {
const result = await utils.execFile(certbotCommand, args, adds.opts);
const result = await runCertbot(args, adds.opts);
await utils.exec(`rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`);
logger.info(result);
return result;
+36 -43
View File
@@ -54,9 +54,9 @@ const internalDeadHost = {
thisData.advanced_config = "";
}
const row = await deadHostModel.query()
.insertAndFetch(thisData)
.then(utils.omitRow(omissions()));
internalHost.assertAdvancedConfigAllowed(access, thisData);
const row = await deadHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
// Add to audit log
await internalAuditLog.add(access, {
@@ -132,6 +132,9 @@ const internalDeadHost = {
);
}
// Before the certificate is created, so a refused request doesn't leave one behind
internalHost.assertAdvancedConfigAllowed(access, data, row);
if (createCertificate) {
const cert = await internalCertificate.createQuickCertificate(access, {
domain_names: data.domain_names || row.domain_names,
@@ -153,12 +156,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 +167,18 @@ const internalDeadHost = {
meta: thisData,
});
const thisRow = await internalDeadHost
.get(access, {
id: thisData.id,
expand: ["owner", "certificate"],
});
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 +196,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 +208,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,18 +226,15 @@ 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({
is_deleted: 1,
});
await deadHostModel.query().where("id", row.id).patch({
is_deleted: 1,
});
// Delete Nginx Config
await internalNginx.deleteConfig("dead_host", row);
@@ -259,12 +258,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,12 +272,9 @@ const internalDeadHost = {
row.enabled = 1;
await deadHostModel
.query()
.where("id", row.id)
.patch({
enabled: 1,
});
await deadHostModel.query().where("id", row.id).patch({
enabled: 1,
});
// Configure nginx
await internalNginx.configure(deadHostModel, "dead_host", row);
@@ -301,9 +297,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,12 +308,9 @@ const internalDeadHost = {
row.enabled = 0;
await deadHostModel
.query()
.where("id", row.id)
.patch({
enabled: 0,
});
await deadHostModel.query().where("id", row.id).patch({
enabled: 0,
});
// Delete Nginx Config
await internalNginx.deleteConfig("dead_host", row);
@@ -342,12 +335,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") {
+103
View File
@@ -1,10 +1,110 @@
import _ from "lodash";
import errs from "../lib/error.js";
import { castJsonIfNeed } from "../lib/helpers.js";
import deadHostModel from "../models/dead_host.js";
import proxyHostModel from "../models/proxy_host.js";
import redirectionHostModel from "../models/redirection_host.js";
const internalHost = {
/**
* Only admins may set or change advanced_config (raw nginx config). Throws for anyone else.
* On create (no `row`), any non-empty advanced_config is refused, including on locations.
* On update, the host's advanced_config must be unchanged (or omitted), and locations must
* only carry advanced configs the host already has (see assertLocationAdvancedConfigUnchanged).
*
* @param {Access} access
* @param {Object} data incoming host data; data.locations may be filled in on update
* @param {Object} [row] the existing host, when updating
* @throws {PermissionError}
*/
assertAdvancedConfigAllowed: (access, data, row) => {
if (access.isAdmin?.()) {
return;
}
const deny = () => {
throw new errs.PermissionError("You do not have permission to modify advanced configuration");
};
if (!row) {
const hasConfig = (v) => typeof v === "string" && v.trim() !== "";
if (hasConfig(data.advanced_config)) {
deny();
}
if (Array.isArray(data.locations) && data.locations.some((loc) => hasConfig(loc?.advanced_config))) {
deny();
}
return;
}
if (typeof data.advanced_config !== "undefined" && data.advanced_config !== row.advanced_config) {
deny();
}
if (Array.isArray(data.locations)) {
internalHost.assertLocationAdvancedConfigUnchanged(data.locations, row.locations);
}
},
/**
* For non-admins: every location's advanced_config must be empty or one that already exists
* on this host. Matching is by value, not path, so renaming or reordering a location that
* carries admin-written config is allowed, but new or edited config is not. Each existing
* config can be used once (no duplicating it onto other paths), and same-path matches are
* taken first. Clearing a config is allowed: deleting the location is, and has the same
* effect, and a location deleted and re-added at the same path arrives with an empty config.
* A location that omits advanced_config keeps the one at its path, unless another location
* explicitly carries it. Mutates `locations`.
*
* @param {Array} locations incoming locations
* @param {Array} [existingLocations] the host's current locations
* @throws {PermissionError}
*/
assertLocationAdvancedConfigUnchanged: (locations, existingLocations) => {
const existing = Array.isArray(existingLocations) ? existingLocations : [];
const advAtPath = (path) => existing.find((l) => l.path === path)?.advanced_config || "";
const pool = existing.map((l) => l.advanced_config || "").filter(Boolean);
const take = (value) => {
const i = pool.indexOf(value);
if (i === -1) {
return false;
}
pool.splice(i, 1);
return true;
};
const deny = () => {
throw new errs.PermissionError("You do not have permission to modify advanced configuration");
};
// Pass 1: locations that keep the config at their own path
const unresolved = [];
const omitted = [];
for (const loc of locations) {
const atPath = advAtPath(loc.path);
if (typeof loc.advanced_config === "undefined") {
omitted.push(loc);
} else if ((loc.advanced_config || "") === atPath && (!atPath || take(atPath))) {
// unchanged
} else {
unresolved.push(loc);
}
}
// Pass 2: renamed/moved locations may carry an existing config not already claimed, or
// none at all
for (const loc of unresolved) {
if (loc.advanced_config && !take(loc.advanced_config)) {
deny();
}
}
// Pass 3: locations that omit advanced_config keep the one at their path, if no location
// that names it explicitly has claimed it. Running this last stops a new location at an
// old path taking the config away from the location it was renamed to.
for (const loc of omitted) {
const atPath = advAtPath(loc.path);
loc.advanced_config = atPath && take(atPath) ? atPath : "";
}
},
/**
* Makes sure that the ssl_* and hsts_* fields play nicely together.
* ie: if there is no cert, then force_ssl is off.
@@ -79,6 +179,9 @@ const internalHost = {
redirection_hosts: [],
};
// No relations are eager-loaded here: this scans every host just to match domains, and
// the few that match are rendered through internalNginx.generateConfig(), which loads
// their certificate and access list itself (loadRenderRelations).
const proxyRes = await proxyHostModel.query().where("is_deleted", 0);
responseObject.proxy_hosts = internalHost._getHostsWithDomains(proxyRes, domainNames);
responseObject.total_count += responseObject.proxy_hosts.length;
+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;
+168 -24
View File
@@ -1,10 +1,14 @@
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 { assertAccessListRenderable, assertHostRenderable, isTokenSafe, unsafeValueError } from "../lib/nginx-values.js";
import utils from "../lib/utils.js";
import { debug, nginx as logger } from "../logger.js";
import accessListModel from "../models/access_list.js";
import certificateModel from "../models/certificate.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -34,12 +38,33 @@ 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);
return internalNginx.generateConfig(host_type, host).then(
() => true,
(err) => {
if (!err.unsafeNginxValue) {
throw err;
}
// Nothing was written. Report it on the host like a failed nginx test.
logger.error(err.message);
combined_meta = _.assign({}, host.meta, {
nginx_online: false,
nginx_err: err.message,
});
return model
.query()
.where("id", host.id)
.patch({ meta: combined_meta })
.then(() => false);
},
);
})
.then(() => {
.then((generated) => {
if (!generated) {
return;
}
// Test nginx again and update meta with result
return internalNginx
.test()
@@ -83,10 +108,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 +192,38 @@ 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) {
assertAccessListRenderable(
`Proxy Host #${host.id} location ${locationCopy.path}`,
locationAccessList,
);
locationCopy.access_list = locationAccessList;
} else if (
locationCopy.access_list_id > 0 &&
locationCopy.access_list_id !== host.access_list?.id
) {
// The location names its own list but it's gone. Don't fall back to the
// host's (possibly less restrictive) list; _access.conf denies all instead.
logger.error(
`Proxy Host #${host.id} location "${locationCopy.path}" references Access List #${locationCopy.access_list_id} which does not exist, all access will be denied`,
);
locationCopy.access_list = null;
} 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 +235,9 @@ const internalNginx = {
}
};
locationRendering().then(() => resolve(renderedLocations));
locationRendering()
.then(() => resolve(renderedLocations))
.catch(reject);
});
},
@@ -185,11 +246,16 @@ const internalNginx = {
* @param {Object} host
* @returns {Promise}
*/
generateConfig: (host_type, host_row) => {
generateConfig: async (host_type, host_row) => {
// Prevent modifying the original object:
const host = JSON.parse(JSON.stringify(host_row));
const nice_host_type = internalNginx.getFileFriendlyHostType(host_type);
await internalNginx.loadRenderRelations(nice_host_type, host);
// Values are written into the config as-is; refuse any that could add directives
assertHostRenderable(nice_host_type, host);
debug(logger, `Generating ${nice_host_type} Config:`, JSON.stringify(host, null, 2));
const renderEngine = utils.getRenderEngine();
@@ -216,6 +282,22 @@ 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
) {
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);
@@ -236,26 +318,79 @@ const internalNginx = {
// Set the IPv6 setting for the host
host.ipv6 = internalNginx.ipv6Enabled();
locationsPromise.then(() => {
renderEngine
.parseAndRender(template, host)
.then((config_text) => {
fs.writeFileSync(filename, config_text, { encoding: "utf8" });
debug(logger, "Wrote config:", filename, config_text);
locationsPromise
.then(() => {
renderEngine
.parseAndRender(template, host)
.then((config_text) => {
fs.writeFileSync(filename, config_text, { encoding: "utf8" });
debug(logger, "Wrote config:", filename, config_text);
// Restore locations array
host.locations = origLocations;
// Restore locations array
host.locations = origLocations;
resolve(true);
})
.catch((err) => {
debug(logger, `Could not write ${filename}:`, err.message);
reject(new errs.ConfigurationError(err.message));
});
});
resolve(true);
})
.catch((err) => {
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));
});
});
},
/**
* The templates rely on the certificate and access list relations being loaded.
* Not every caller fetches them (or fetches them fully), and when they're missing
* the config silently renders without SSL or without any access rules (#5919).
* Load anything that is referenced but not loaded onto the given host object.
*
* @param {String} host_type
* @param {Object} host
* @returns {Promise}
*/
loadRenderRelations: async (host_type, host) => {
if (host.certificate_id > 0 && host.certificate?.id !== host.certificate_id) {
const certificate = await certificateModel
.query()
.where("is_deleted", 0)
.andWhere("id", host.certificate_id)
.first();
host.certificate = certificate ? JSON.parse(JSON.stringify(certificate)) : null;
}
if (host_type === "proxy_host" && host.access_list_id > 0) {
if (
host.access_list?.id !== host.access_list_id ||
!Array.isArray(host.access_list.clients) ||
!Array.isArray(host.access_list.items)
) {
const accessList = await accessListModel
.query()
.where("is_deleted", 0)
.andWhere("id", host.access_list_id)
.withGraphFetched("[clients,items]")
.first();
host.access_list = accessList ? JSON.parse(JSON.stringify(accessList)) : null;
}
if (!host.access_list) {
// _access.conf renders "deny all" when the list is missing, so fail closed
logger.error(
`Proxy Host #${host.id} references Access List #${host.access_list_id} which does not exist, all access will be denied`,
);
} else if (!host.access_list.clients.length && !host.access_list.items.length) {
logger.warn(
`Proxy Host #${host.id} uses Access List #${host.access_list_id} which has no clients or authorizations, no access restrictions will apply`,
);
}
}
},
/**
* This generates a temporary nginx config listening on port 80 for the domain names listed
* in the certificate setup. It allows the letsencrypt acme challenge to be requested by letsencrypt
@@ -268,6 +403,15 @@ const internalNginx = {
debug(logger, "Generating LetsEncrypt Request Config:", certificate);
const renderEngine = utils.getRenderEngine();
const unsafe = (certificate.domain_names || []).find((name) => !isTokenSafe(name));
if (typeof unsafe !== "undefined") {
return Promise.reject(
unsafeValueError(
`Certificate #${certificate.id}: domain name ${JSON.stringify(unsafe)} is not allowed`,
),
);
}
return new Promise((resolve, reject) => {
let template = null;
const filename = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`;
@@ -370,8 +514,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();
+42 -43
View File
@@ -2,7 +2,7 @@ import _ from "lodash";
import errs from "../lib/error.js";
import { castJsonIfNeed } from "../lib/helpers.js";
import utils from "../lib/utils.js";
import proxyHostModel from "../models/proxy_host.js";
import proxyHostModel, { scopeToOwnerOrAllowed } from "../models/proxy_host.js";
import internalAuditLog from "./audit-log.js";
import internalCertificate from "./certificate.js";
import internalHost from "./host.js";
@@ -57,6 +57,8 @@ const internalProxyHost = {
thisData.advanced_config = "";
}
internalHost.assertAdvancedConfigAllowed(access, thisData);
return proxyHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
})
.then((row) => {
@@ -83,7 +85,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 +117,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 +157,10 @@ const internalProxyHost = {
);
}
if (create_certificate) {
// Before the certificate is created, so a refused request doesn't leave one behind
internalHost.assertAdvancedConfigAllowed(access, thisData, row);
if (createCertificate) {
return internalCertificate
.createQuickCertificate(access, {
domain_names: thisData.domain_names || row.domain_names,
@@ -208,7 +213,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 +237,6 @@ const internalProxyHost = {
*/
get: (access, data) => {
const thisData = data || {};
return access
.can("proxy_hosts:get", thisData.id)
.then((access_data) => {
@@ -240,11 +244,11 @@ 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") {
query.andWhere("owner_user_id", access.token.getUserId(1));
scopeToOwnerOrAllowed(query, access.token.getUserId(1), access.getPermissions?.()?.meta);
}
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
@@ -254,7 +258,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 +284,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);
}
@@ -324,11 +328,11 @@ const internalProxyHost = {
.then(() => {
return internalProxyHost.get(access, {
id: data.id,
expand: ["certificate", "owner", "access_list"],
expand: ["certificate", "owner", "access_list.[clients,items]"],
});
})
.then((row) => {
if (!row || !row.id) {
.then(async (row) => {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
@@ -337,27 +341,21 @@ const internalProxyHost = {
row.enabled = 1;
return proxyHostModel
.query()
.where("id", row.id)
.patch({
enabled: 1,
})
.then(() => {
// Configure nginx
return internalNginx.configure(proxyHostModel, "proxy_host", row);
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "enabled",
object_type: "proxy-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
.then(() => {
await proxyHostModel.query().where("id", row.id).patch({
enabled: 1,
});
// Configure nginx
await internalNginx.configure(proxyHostModel, "proxy_host", row);
// Add to audit log
await internalAuditLog.add(access, {
action: "enabled",
object_type: "proxy-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
return true;
});
},
@@ -376,7 +374,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,15 +420,16 @@ 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") {
query.andWhere("owner_user_id", access.token.getUserId(1));
scopeToOwnerOrAllowed(query, access.token.getUserId(1), access.getPermissions?.()?.meta);
}
// Query is used for searching
@@ -456,18 +455,18 @@ const internalProxyHost = {
*
* @param {Number} user_id
* @param {String} visibility
* @param {Object} [permissionsMeta] the user's permissions meta, as already loaded by access
* @returns {Promise}
*/
getCount: (user_id, visibility) => {
getCount: async (user_id, visibility, permissionsMeta) => {
const query = proxyHostModel.query().count("id as count").where("is_deleted", 0);
if (visibility !== "all") {
query.andWhere("owner_user_id", user_id);
scopeToOwnerOrAllowed(query, user_id, permissionsMeta);
}
return query.first().then((row) => {
return Number.parseInt(row.count, 10);
});
const row = await query.first();
return Number.parseInt(row.count, 10);
},
};
+13 -9
View File
@@ -53,10 +53,12 @@ const internalRedirectionHost = {
// Fix for db field not having a default value
// for this optional field.
if (typeof data.advanced_config === "undefined") {
data.advanced_config = "";
if (typeof thisData.advanced_config === "undefined") {
thisData.advanced_config = "";
}
internalHost.assertAdvancedConfigAllowed(access, thisData);
return redirectionHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
})
.then((row) => {
@@ -155,6 +157,9 @@ const internalRedirectionHost = {
);
}
// Before the certificate is created, so a refused request doesn't leave one behind
internalHost.assertAdvancedConfigAllowed(access, thisData, row);
if (createCertificate) {
return internalCertificate
.createQuickCertificate(access, {
@@ -229,7 +234,6 @@ const internalRedirectionHost = {
*/
get: (access, data) => {
const thisData = data || {};
return access
.can("redirection_hosts:get", thisData.id)
.then((access_data) => {
@@ -237,7 +241,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 +256,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 +282,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 +330,7 @@ const internalRedirectionHost = {
});
})
.then((row) => {
if (!row || !row.id) {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
@@ -374,7 +378,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 +430,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") {
+84
View File
@@ -0,0 +1,84 @@
import https from "node:https";
import { ProxyAgent } from "proxy-agent";
import { debug, remoteVersion as logger } from "../logger.js";
import pjson from "../package.json" with { type: "json" };
const VERSION_URL = "https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager/releases/latest";
const internalRemoteVersion = {
cache_timeout: 1000 * 60 * 15, // 15 minutes
last_result: null,
last_fetch_time: null,
/**
* Fetch the latest version info, using a cached result if within the cache timeout period.
* @return {Promise<{current: string, latest: string, update_available: boolean}>} Version info
*/
get: async () => {
if (
!internalRemoteVersion.last_result ||
!internalRemoteVersion.last_fetch_time ||
Date.now() - internalRemoteVersion.last_fetch_time > internalRemoteVersion.cache_timeout
) {
const raw = await internalRemoteVersion.fetchUrl(VERSION_URL);
const data = JSON.parse(raw);
internalRemoteVersion.last_result = data;
internalRemoteVersion.last_fetch_time = Date.now();
} else {
debug(logger, "Using cached remote version result");
}
const latestVersion = internalRemoteVersion.last_result.tag_name;
const version = pjson.version.split("-").shift().split(".");
const currentVersion = `v${version[0]}.${version[1]}.${version[2]}`;
return {
current: currentVersion,
latest: latestVersion,
update_available: internalRemoteVersion.compareVersions(currentVersion, latestVersion),
};
},
fetchUrl: (url) => {
const agent = new ProxyAgent();
const headers = {
"User-Agent": `NginxProxyManager v${pjson.version}`,
};
return new Promise((resolve, reject) => {
logger.info(`Fetching ${url}`);
return https
.get(url, { agent, headers }, (res) => {
res.setEncoding("utf8");
let raw_data = "";
res.on("data", (chunk) => {
raw_data += chunk;
});
res.on("end", () => {
resolve(raw_data);
});
})
.on("error", (err) => {
reject(err);
});
});
},
compareVersions: (current, latest) => {
const cleanCurrent = current.replace(/^v/, "");
const cleanLatest = latest.replace(/^v/, "");
const currentParts = cleanCurrent.split(".").map(Number);
const latestParts = cleanLatest.split(".").map(Number);
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
const curr = currentParts[i] || 0;
const lat = latestParts[i] || 0;
if (lat > curr) return true;
if (lat < curr) return false;
}
return false;
},
};
export default internalRemoteVersion;
+8 -4
View File
@@ -15,10 +15,14 @@ const internalReport = {
const userId = access.token.getUserId(1);
const promises = [
internalProxyHost.getCount(userId, access_data.visibility),
internalRedirectionHost.getCount(userId, access_data.visibility),
internalStream.getCount(userId, access_data.visibility),
internalDeadHost.getCount(userId, access_data.visibility),
internalProxyHost.getCount(
userId,
access_data.permission_visibility,
access.getPermissions?.()?.meta,
),
internalRedirectionHost.getCount(userId, access_data.permission_visibility),
internalStream.getCount(userId, access_data.permission_visibility),
internalDeadHost.getCount(userId, access_data.permission_visibility),
];
return Promise.all(promises);
+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") {
+88 -15
View File
@@ -4,9 +4,12 @@ import { parseDatePeriod } from "../lib/helpers.js";
import authModel from "../models/auth.js";
import TokenModel from "../models/token.js";
import userModel from "../models/user.js";
import twoFactor from "./2fa.js";
const ERROR_MESSAGE_INVALID_AUTH = "Invalid email or password";
const ERROR_MESSAGE_INVALID_AUTH_I18N = "error.invalid-auth";
const ERROR_MESSAGE_INVALID_2FA = "Invalid verification code";
const ERROR_MESSAGE_INVALID_2FA_I18N = "error.invalid-2fa";
export default {
/**
@@ -31,26 +34,21 @@ export default {
.andWhere("is_disabled", 0)
.first();
if (!user) {
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
}
// An unknown, disabled or passwordless account goes through the same queries and the same password check
// as a real one, and fails with the same error: answering any sooner or any differently would tell anyone
// trying addresses which ones have an account. There is no user 0, so its lookup finds nothing. An empty
// secret, which creating a user through the API can store, is no hash: bcrypt would reject it at once.
const auth = await authModel
.query()
.where("user_id", "=", user.id)
.where("user_id", "=", user?.id ?? 0)
.where("type", "=", "password")
.first();
if (!auth) {
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
}
const valid = await auth.verifyPassword(data.secret);
if (!valid) {
throw new errs.AuthError(
ERROR_MESSAGE_INVALID_AUTH,
ERROR_MESSAGE_INVALID_AUTH_I18N,
);
const valid = auth?.secret
? await auth.verifyPassword(data.secret)
: await authModel.verifyDummyPassword(data.secret);
if (!user || !valid) {
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH, ERROR_MESSAGE_INVALID_AUTH_I18N);
}
if (data.scope !== "user" && _.indexOf(user.roles, data.scope) === -1) {
@@ -59,6 +57,25 @@ export default {
throw new errs.AuthError(`Invalid scope: ${data.scope}`);
}
// Check if 2FA is enabled
const has2FA = await twoFactor.isEnabled(user.id);
if (has2FA) {
// Return challenge token instead of full token
const challengeToken = await Token.create({
iss: issuer || "api",
attrs: {
id: user.id,
},
scope: ["2fa-challenge"],
expiresIn: "5m",
});
return {
requires_2fa: true,
challenge_token: challengeToken.token,
};
}
// Create a moment of the expiry expression
const expiry = parseDatePeriod(data.expiry);
if (expiry === null) {
@@ -129,6 +146,62 @@ export default {
throw new error.AssertionFailedError("Existing token contained invalid user data");
},
/**
* Verify 2FA code and return full token
* @param {string} challengeToken
* @param {string} code
* @param {string} [expiry]
* @returns {Promise}
*/
verify2FA: async (challengeToken, code, expiry) => {
const Token = TokenModel();
const tokenExpiry = expiry || "1d";
// Verify challenge token
let tokenData;
try {
tokenData = await Token.load(challengeToken);
} catch {
throw new errs.AuthError("Invalid or expired challenge token");
}
// Check scope
if (tokenData.scope?.[0] !== "2fa-challenge") {
throw new errs.AuthError("Invalid challenge token");
}
const userId = tokenData.attrs?.id;
if (!userId) {
throw new errs.AuthError("Invalid challenge token");
}
// 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);
}
// Create full token
const expiryDate = parseDatePeriod(tokenExpiry);
if (expiryDate === null) {
throw new errs.AuthError(`Invalid expiry time: ${tokenExpiry}`);
}
const signed = await Token.create({
iss: "api",
attrs: {
id: userId,
},
scope: ["user"],
expiresIn: tokenExpiry,
});
return {
token: signed.token,
expires: expiryDate.toISOString(),
};
},
/**
* @param {Object} user
* @returns {Promise}
+59 -12
View File
@@ -3,6 +3,7 @@ import _ from "lodash";
import errs from "../lib/error.js";
import utils from "../lib/utils.js";
import authModel from "../models/auth.js";
import proxyHostModel from "../models/proxy_host.js";
import userModel from "../models/user.js";
import userPermissionModel from "../models/user_permission.js";
import internalAuditLog from "./audit-log.js";
@@ -87,7 +88,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 +178,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,11 +258,9 @@ const internalUser = {
},
deleteAll: async () => {
await userModel
.query()
.patch({
is_deleted: 1,
});
await userModel.query().patch({
is_deleted: 1,
});
},
/**
@@ -384,11 +389,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 +411,7 @@ const internalUser = {
user_id: user.id,
type: data.type,
secret: data.secret,
meta: {},
meta: { password_changed_at },
});
})
.then(() => {
@@ -430,7 +445,7 @@ const internalUser = {
return internalUser.get(access, { id: data.id });
})
.then((user) => {
if (user.id !== data.id) {
if (Number(user.id) !== Number(data.id)) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,
@@ -440,21 +455,23 @@ const internalUser = {
return user;
})
.then((user) => {
const permData = _.omit(data, ["id"]);
// Get perms row, patch if it exists
return userPermissionModel
.query()
.where("user_id", user.id)
.first()
.then((existing_auth) => {
.then(async (existing_auth) => {
permData.meta = await internalUser.normalisePermissionMeta(existing_auth, permData);
if (existing_auth) {
// patch
return userPermissionModel
.query()
.where("user_id", user.id)
.patchAndFetchById(existing_auth.id, _.assign({ user_id: user.id }, data));
.patchAndFetchById(existing_auth.id, _.assign({ user_id: user.id }, permData));
}
// insert
return userPermissionModel.query().insertAndFetch(_.assign({ user_id: user.id }, data));
return userPermissionModel.query().insertAndFetch(_.assign({ user_id: user.id }, permData));
})
.then((permissions) => {
// Add to Audit Log
@@ -474,6 +491,36 @@ const internalUser = {
});
},
/**
* The permission meta to store. Proxy host grants (meta.proxy_host_ids) only apply while
* visibility is "user" and proxy hosts are not hidden, so they're cleared otherwise; kept,
* they would silently come back when proxy hosts are shown again. Ids of hosts that no
* longer exist are dropped. `permData` may be partial, so the stored values fill the gaps.
*
* @param {Object} [existing] the stored user_permission row
* @param {Object} permData incoming permission fields
* @returns {Promise<Object>}
*/
normalisePermissionMeta: async (existing, permData) => {
const effective = _.assign(
{ visibility: "user", proxy_hosts: "hidden" },
existing || {},
_.omit(permData, ["meta"]),
);
const meta = _.assign({}, existing?.meta || {}, permData.meta || {});
const requested = Array.isArray(meta.proxy_host_ids) ? _.uniq(meta.proxy_host_ids) : [];
if (effective.visibility !== "user" || effective.proxy_hosts === "hidden" || !requested.length) {
meta.proxy_host_ids = [];
return meta;
}
const rows = await proxyHostModel.query().select("id").where("is_deleted", 0).whereIn("id", requested);
const found = new Set(rows.map((r) => r.id));
meta.proxy_host_ids = requested.filter((id) => found.has(id));
return meta;
},
/**
* @param {Access} access
* @param {Object} data
+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",
},
},
};
+34 -8
View File
@@ -12,7 +12,8 @@ import { fileURLToPath } from "node:url";
import Ajv from "ajv/dist/2020.js";
import _ from "lodash";
import { access as logger } from "../logger.js";
import proxyHostModel from "../models/proxy_host.js";
import authModel from "../models/auth.js";
import proxyHostModel, { scopeToOwnerOrAllowed } from "../models/proxy_host.js";
import TokenModel from "../models/token.js";
import userModel from "../models/user.js";
import permsSchema from "./access/permissions.json" with { type: "json" };
@@ -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,13 +136,10 @@ 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);
scopeToOwnerOrAllowed(query, tokenUserId, permissions.meta);
}
const rows = await query;
@@ -199,6 +213,11 @@ export default function (tokenString) {
return {
token: Token,
getPermissions: () => permissions,
getRoles: () => userRoles,
hasRole: (role) => userRoles.includes(role),
isAdmin: () => userRoles.includes("admin") || Token.hasScope("admin"),
/**
*
* @param {Boolean} [allowInternal]
@@ -206,7 +225,8 @@ export default function (tokenString) {
*/
load: async (allowInternal) => {
if (tokenString) {
return await Token.load(tokenString);
await this.init();
return tokenData;
}
allowInternalAccess = allowInternal;
return allowInternal || null;
@@ -241,6 +261,7 @@ export default function (tokenString) {
permission_streams: permissions.streams,
permission_access_lists: permissions.access_lists,
permission_certificates: permissions.certificates,
permission_meta: permissions.meta,
},
};
@@ -271,7 +292,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 };
+42 -18
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 = '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 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);
if (envMysqlHost && envMysqlUser && envMysqlName) {
// we have enough mysql creds to go with mysql
logger.info("Using MySQL configuration");
@@ -55,8 +64,10 @@ const configure = () => {
port: process.env.DB_MYSQL_PORT || 3306,
user: envMysqlUser,
password: process.env.DB_MYSQL_PASSWORD,
name: envMysqlName,
ssl: envMysqlSSL ? { rejectUnauthorized: envMysqlSSLRejectUnauthorized, verifyIdentity: envMysqlSSLVerifyIdentity } : false,
name: envMysqlName,
ssl: envMysqlSSL
? { rejectUnauthorized: envMysqlSSLRejectUnauthorized, verifyIdentity: envMysqlSSLVerifyIdentity }
: false,
},
keys: getKeys(),
};
@@ -84,6 +95,7 @@ const configure = () => {
}
const envSqliteFile = process.env.DB_SQLITE_FILE || "/data/database.sqlite";
logger.info(`Using Sqlite: ${envSqliteFile}`);
instance = {
database: {
@@ -133,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);
@@ -183,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);
};
/**
@@ -207,7 +219,7 @@ const isPostgres = () => {
};
/**
* Are we running in debug mdoe?
* Are we running in debug mode?
*
* @returns {boolean}
*/
@@ -218,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
@@ -255,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();
};
+1 -1
View File
@@ -1,4 +1,4 @@
import _ from "lodash";
import _ from "lodash";
export default (default_sort, default_offset, default_limit, max_limit) => {
/**
+155
View File
@@ -0,0 +1,155 @@
import crypto from "node:crypto";
import { isCI } from "../config.js";
/**
* In-memory fixed window rate limiter for authentication endpoints
*/
// Keys longer than this are hashed, so a client can't make the store hold arbitrarily large
// strings (keys can include request body values such as the login identity).
const MAX_KEY_LENGTH = 128;
// Entries per limiter. A flood of unique keys sweeps expired entries, then evicts the oldest,
// instead of growing the store until the periodic cleanup runs.
const MAX_STORE_SIZE = 50000;
// One store per limiter, so separate endpoints never share buckets
const stores = new Set();
// Clean up old entries every 2 minutes
const cleanupTimer = setInterval(() => {
const now = Date.now();
for (const store of stores) {
for (const [key, record] of store.entries()) {
if (now - record.startTime > record.windowMs * 2) {
store.delete(key);
}
}
}
}, 120000);
cleanupTimer.unref();
// Generated at container start (prepare/45-admin-port.sh) and sent by the admin nginx as
// X-NPM-Peer-Secret alongside X-NPM-Peer-Addr. Unset (development, or a mounted production.conf)
// means the header is never trusted.
const peerSecret = Buffer.from(process.env.NPM_PEER_SECRET || "");
const hasPeerSecret = (value) => {
if (!peerSecret.length || typeof value !== "string") {
return false;
}
const given = Buffer.from(value);
return given.length === peerSecret.length && crypto.timingSafeEqual(given, peerSecret);
};
/**
* The address to rate limit on: the TCP peer of the admin nginx, which it passes as
* X-NPM-Peer-Addr ($realip_remote_addr). That header is only honoured from a loopback socket
* carrying the startup secret, so it can only have come from the admin nginx: a proxy host or
* stream forwarding to the backend port is also a loopback peer, but can't know the secret.
* The realip-resolved client address is deliberately not used: nginx trusts X-Real-IP from
* private ranges, so a client on the LAN or Docker network could pick a new address, and a fresh
* bucket, on every request. Behind another proxy (including an NPM proxy host in front of the
* admin UI) this resolves to that proxy's address.
*
* @param {Object} req
* @returns {String}
*/
export const getClientIp = (req) => {
const socketAddr = req.socket?.remoteAddress || "unknown";
const isLoopback = socketAddr === "127.0.0.1" || socketAddr === "::1" || socketAddr === "::ffff:127.0.0.1";
const trusted = isLoopback && hasPeerSecret(req.headers["x-npm-peer-secret"]);
const peerAddr = trusted ? req.headers["x-npm-peer-addr"]?.trim() : null;
return peerAddr || socketAddr;
};
const makeRoom = (store, now) => {
for (const [k, r] of store.entries()) {
if (now - r.startTime > r.windowMs) {
store.delete(k);
}
}
// Maps iterate in insertion order, so this drops the oldest windows first
for (const k of store.keys()) {
if (store.size < MAX_STORE_SIZE) {
break;
}
store.delete(k);
}
};
const boundedKey = (key) =>
key.length > MAX_KEY_LENGTH ? `sha256:${crypto.createHash("sha256").update(key).digest("hex")}` : key;
/**
* @param {Object} [options]
* @param {Number} [options.windowMs]
* @param {Number} [options.max]
* @param {String} [options.message]
* @param {Boolean} [options.skipSuccessfulRequests]
* @param {Function} [options.keyGenerator] (req, clientIp) => String. Defaults to the client IP.
* Including the target account in the key stops every
* user being locked out when many clients share an IP
* (e.g. the admin UI behind another reverse proxy).
*/
export default function createRateLimiter(options = {}) {
const windowMs = options.windowMs || 60 * 1000; // 1 minute default
const max = options.max || 10; // 10 requests default
const message = options.message || "Too many attempts from this IP, please try again later.";
const skipSuccessfulRequests = options.skipSuccessfulRequests !== false;
const keyGenerator = options.keyGenerator || ((_req, clientIp) => clientIp);
const store = new Map();
stores.add(store);
return (req, res, next) => {
// Bypass rate limiting in CI, test environments, or when explicitly disabled
if (
isCI() ||
process.env.CI === "true" ||
process.env.NODE_ENV === "test" ||
process.env.DISABLE_RATE_LIMIT === "true"
) {
return next();
}
const key = boundedKey(String(keyGenerator(req, getClientIp(req))));
const now = Date.now();
let record = store.get(key);
if (!record || now - record.startTime > windowMs) {
record = {
count: 0,
startTime: now,
windowMs,
};
if (!store.has(key) && store.size >= MAX_STORE_SIZE) {
makeRoom(store, now);
}
store.set(key, record);
}
record.count++;
if (skipSuccessfulRequests) {
const currentRecord = record;
res.on("finish", () => {
if (res.statusCode < 400 && currentRecord.count > 0) {
currentRecord.count--;
}
});
}
if (record.count > max) {
res.setHeader("Retry-After", Math.ceil((record.startTime + windowMs - now) / 1000));
return res.status(429).json({
error: {
code: 429,
message,
},
});
}
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);
}
+134
View File
@@ -0,0 +1,134 @@
import net from "node:net";
import errs from "./error.js";
/**
* Rules for user-supplied values that the templates write into nginx config as-is.
*
* Anything that ends or changes an nginx token (whitespace, ";", "{", "}", quotes, "#" which
* starts a comment, and "\" which escapes the next character) would let the value add its own
* directives, which only admins may do (advanced_config). The API schema rejects these values
* with the same patterns (a test keeps the two in step). generateConfig() checks stored values
* again, so rows saved before these rules can't add directives either. That check is only about
* nginx structure, not format: a stored value in a format the API no longer accepts, but which
* can't add directives, still renders.
*
* Patterns are strings so they can be compared with the JSON schema; compile them with the "u"
* flag, as ajv does.
*/
// One character that can't end or change an unquoted nginx token
const TOKEN_CHAR = String.raw`[^\s\p{Cc};{}"'#\\]`;
// Host part of a forward host: also no "$" (no variables in the upstream address) and no "/"
const HOST_CHARS = String.raw`[^\s\p{Cc};{}"'#\\$/]+`;
// The existing domain name rule ("a.b.c", no "*", no empty labels), written so it can't
// backtrack exponentially
const DOMAIN = String.raw`[^.*]+(?:\.[^.*]+)*\.?[^.]`;
export const patterns = {
// server_name entries. The old character blacklist plus whitespace and control characters.
domainName: "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"\\s\\p{Cc}]+$",
// proxy_host forward_host and location forward_host: a host, optionally followed by a path
// (locations split "host/path" at render time)
forwardHost: String.raw`^${HOST_CHARS}(?:/${TOKEN_CHAR}*)?$`,
// location forward_path, appended straight after the port
forwardPath: String.raw`^${TOKEN_CHAR}*$`,
// location path: an optional modifier, then one token, either unquoted (a "\" must escape a
// visible character, so it can't swallow the space before "{") or double quoted. A modifier
// on its own isn't a location.
locationPath: String.raw`^(?!(?:=|~\*?|\^~)$)(?:(?:=|~\*?|\^~) +)?(?:"(?:[^"\\\p{Cc}]|\\[^\p{Cc}])+"|(?:[^\s\p{Cc};{}"'#\\]|\\[^\s\p{Cc}])+)$`,
// redirection forward_domain_name, e.g. "example.com:8443/path"
redirectDomain: String.raw`^(?=${TOKEN_CHAR}+$)${DOMAIN}$`,
// stream forwarding_host when it is a domain name (IPs are matched separately)
streamDomain: String.raw`^(?=${HOST_CHARS}$)${DOMAIN}$`,
// access list client address: IPv6 with an optional prefix. No zone id ("%eth0"): nginx's
// allow/deny rejects them, and the old "%.+" let anything through, including "; allow all"
clientIpv6: String.raw`^(?:(?:[0-9A-Fa-f]{1,4}:){7}(?:[0-9A-Fa-f]{1,4}|:)|(?:[0-9A-Fa-f]{1,4}:){6}(?::[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}|:)|(?:[0-9A-Fa-f]{1,4}:){5}(?:(?::[0-9A-Fa-f]{1,4}){1,2}|:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}|:)|(?:[0-9A-Fa-f]{1,4}:){4}(?:(?::[0-9A-Fa-f]{1,4}){1,3}|(?::[0-9A-Fa-f]{1,4})?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}|:)|(?:[0-9A-Fa-f]{1,4}:){3}(?:(?::[0-9A-Fa-f]{1,4}){1,4}|(?::[0-9A-Fa-f]{1,4}){0,2}:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}|:)|(?:[0-9A-Fa-f]{1,4}:){2}(?:(?::[0-9A-Fa-f]{1,4}){1,5}|(?::[0-9A-Fa-f]{1,4}){0,3}:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}|:)|(?:[0-9A-Fa-f]{1,4}:)(?:(?::[0-9A-Fa-f]{1,4}){1,6}|(?::[0-9A-Fa-f]{1,4}){0,4}:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}|:)|:(?:(?::[0-9A-Fa-f]{1,4}){1,7}|(?::[0-9A-Fa-f]{1,4}){0,5}:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}|:))(?:/(?:[0-9]|[1-9][0-9]|1[01][0-9]|12[0-8]))?$`,
// htpasswd user name: a ":" or a line break would corrupt the file
accessUsername: String.raw`^[^:\p{Cc}]+$`,
};
const re = Object.fromEntries(Object.entries(patterns).map(([k, v]) => [k, new RegExp(v, "u")]));
const unsafeToken = /[\s\p{Cc};{}"'#\\]/u;
const ipv4WithPrefix = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}(?:\/(?:[0-9]|[1-2][0-9]|3[0-2]))?$/;
export const isValid = (name, value) => typeof value === "string" && re[name].test(value);
export const isValidClientAddress = (value) =>
typeof value === "string" && (value === "all" || ipv4WithPrefix.test(value) || re.clientIpv6.test(value));
export const isValidStreamHost = (value) =>
typeof value === "string" && (net.isIP(value) !== 0 || re.streamDomain.test(value));
/**
* Thrown when a stored value can't be written into nginx config safely
*
* @param {String} message
*/
export const unsafeValueError = (message) => {
const err = new errs.ConfigurationError(message);
err.unsafeNginxValue = true;
return err;
};
const describe = (value) => JSON.stringify(String(value)).slice(0, 80);
/**
* @param {String} label e.g. "Proxy Host #3"
* @param {Object} [accessList]
* @throws {ConfigurationError}
*/
export const assertAccessListRenderable = (label, accessList) => {
for (const client of accessList?.clients || []) {
// "address" is rendered unquoted ("allow <address>;")
if (typeof client.address !== "string" || unsafeToken.test(client.address)) {
throw unsafeValueError(`${label}: access list client address ${describe(client.address)} is not allowed`);
}
}
};
// Can be written into config without ending or changing a token
export const isTokenSafe = (value) => typeof value === "string" && value.length > 0 && !unsafeToken.test(value);
/**
* Checks every user-supplied value the host's template renders. Call before rendering.
*
* @param {String} hostType proxy_host, redirection_host, dead_host, stream or default
* @param {Object} host with access_list loaded, for proxy hosts
* @throws {ConfigurationError}
*/
export const assertHostRenderable = (hostType, host) => {
const label = `${hostType.replace(/_/g, " ")} #${host.id}`;
const check = (ok, what, value) => {
if (!ok) {
throw unsafeValueError(`${label}: ${what} ${describe(value)} is not allowed in nginx config`);
}
};
if (hostType !== "stream" && hostType !== "default") {
for (const name of host.domain_names || []) {
check(isTokenSafe(name), "domain name", name);
}
}
if (hostType === "proxy_host") {
check(isTokenSafe(host.forward_host), "forward host", host.forward_host);
assertAccessListRenderable(label, host.access_list);
for (const loc of host.locations || []) {
// The path is several tokens (modifier, then a quoted or unquoted URI): its rule is the
// structural one
check(isValid("locationPath", loc.path), "location path", loc.path);
check(isTokenSafe(loc.forward_host), "location forward host", loc.forward_host);
if (loc.forward_path) {
check(isTokenSafe(loc.forward_path), "location forward path", loc.forward_path);
}
}
}
if (hostType === "redirection_host") {
check(isTokenSafe(host.forward_domain_name), "forward domain", host.forward_domain_name);
}
if (hostType === "stream") {
check(isTokenSafe(host.forwarding_host), "forwarding host", host.forwarding_host);
}
};
-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) => {
+11 -5
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,21 +27,24 @@ 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;
}
// Keep ajv's wording, which clients may match on, but not the regex it quotes: some are long
// and mean nothing to a user
for (const e of validate.errors || []) {
if (e.keyword === "pattern") {
e.message = "must match pattern";
}
}
const message = ajv.errorsText(validate.errors);
const err = new errs.ValidationError(message);
err.debug = {validationErrors: validate.errors, payload};
err.debug = { validationErrors: validate.errors, payload };
throw err;
};
+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
+112 -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,16 +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 });
// 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()) {
@@ -22,4 +123,4 @@ const debug = (logger, ...args) => {
}
};
export { debug, global, migrate, express, access, nginx, ssl, certbot, importer, setup, ipRanges };
export { debug, global, migrate, express, access, nginx, ssl, certbot, importer, setup, ipRanges, remoteVersion };
+2 -2
View File
@@ -2,9 +2,9 @@ import db from "./db.js";
import { migrate as logger } from "./logger.js";
const migrateUp = async () => {
const version = await db.migrate.currentVersion();
const version = await db().migrate.currentVersion();
logger.info("Current database version:", version);
return await db.migrate.latest({
return await db().migrate.latest({
tableName: "migrations",
directory: "migrations",
});
@@ -13,13 +13,14 @@ 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`);
});
@@ -0,0 +1,46 @@
import { migrate as logger } from "../logger.js";
const migrateName = "redirect_auto_scheme";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.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" });
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
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" });
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
});
};
export { up, down };
@@ -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 };
@@ -0,0 +1,41 @@
import { migrate as logger } from "../logger.js";
const migrateName = "user_permission_meta";
/**
* Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.alterTable("user_permission", (table) => {
table.json("meta").nullable();
})
.then(() => {
logger.info(`[${migrateName}] user_permission Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema
.alterTable("user_permission", (table) => {
table.dropColumn("meta");
})
.then(() => {
logger.info(`[${migrateName}] user_permission Table altered`);
});
};
export { up, down };
+5 -1
View File
@@ -10,7 +10,7 @@ import now from "./now_helper.js";
import ProxyHostModel from "./proxy_host.js";
import User from "./user.js";
Model.knex(db);
Model.knex(db());
const boolFields = ["is_deleted", "satisfy_any", "pass_auth"];
@@ -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);
}
+1 -1
View File
@@ -6,7 +6,7 @@ import db from "../db.js";
import accessListModel from "./access_list.js";
import now from "./now_helper.js";
Model.knex(db);
Model.knex(db());
class AccessListAuth extends Model {
$beforeInsert() {
+1 -1
View File
@@ -6,7 +6,7 @@ import db from "../db.js";
import accessListModel from "./access_list.js";
import now from "./now_helper.js";
Model.knex(db);
Model.knex(db());
class AccessListClient extends Model {
$beforeInsert() {
+1 -1
View File
@@ -6,7 +6,7 @@ import db from "../db.js";
import now from "./now_helper.js";
import User from "./user.js";
Model.knex(db);
Model.knex(db());
class AuditLog extends Model {
$beforeInsert() {
+20 -2
View File
@@ -1,6 +1,7 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
import crypto from "node:crypto";
import bcrypt from "bcrypt";
import { Model } from "objection";
import db from "../db.js";
@@ -8,13 +9,18 @@ import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.j
import now from "./now_helper.js";
import User from "./user.js";
Model.knex(db);
Model.knex(db());
const boolFields = ["is_deleted"];
const saltRounds = 13;
// A hash of a random value, for a login that names an account with no password to check it against. Hashed
// with the same cost as real passwords, and made at startup so the first such login takes no longer either.
const dummyHash = bcrypt.hash(crypto.randomBytes(32).toString("hex"), saltRounds);
function encryptPassword() {
if (this.type === "password" && this.secret) {
return bcrypt.hash(this.secret, 13).then((hash) => {
return bcrypt.hash(this.secret, saltRounds).then((hash) => {
this.secret = hash;
});
}
@@ -60,6 +66,18 @@ class Auth extends Model {
return bcrypt.compare(password, this.secret);
}
/**
* Take as long as verifyPassword does, then fail. bcrypt is nearly all of the time a login takes, so a login
* that skipped it for an unknown account would answer far sooner, and say which accounts exist.
*
* @param {String} password
* @returns {Promise<false>}
*/
static async verifyDummyPassword(password) {
await bcrypt.compare(password, await dummyHash);
return false;
}
static get name() {
return "Auth";
}
+15 -7
View File
@@ -11,10 +11,20 @@ import redirectionHostModel from "./redirection_host.js";
import streamModel from "./stream.js";
import userModel from "./user.js";
Model.knex(db);
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);
}
}
+14 -2
View File
@@ -3,12 +3,12 @@
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";
Model.knex(db);
Model.knex(db());
const boolFields = ["is_deleted", "ssl_forced", "http2_support", "enabled", "hsts_enabled", "hsts_subdomains"];
@@ -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: {
+1 -1
View File
@@ -2,7 +2,7 @@ import { Model } from "objection";
import db from "../db.js";
import { isSqlite } from "../lib/config.js";
Model.knex(db);
Model.knex(db());
export default () => {
if (isSqlite()) {
+34 -2
View File
@@ -3,13 +3,13 @@
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";
import User from "./user.js";
Model.knex(db);
Model.knex(db());
const boolFields = [
"is_deleted",
@@ -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: {
@@ -111,4 +124,23 @@ class ProxyHost extends Model {
}
}
/**
* Restricts a proxy host query to hosts the user owns or has been granted by id
* (user permission meta.proxy_host_ids).
*
* @param {Object} query objection query on proxy_host
* @param {Number} userId
* @param {Object} [permissionMeta] the user's permission meta
* @returns {Object} the query
*/
export const scopeToOwnerOrAllowed = (query, userId, permissionMeta) => {
const allowedIds = permissionMeta?.proxy_host_ids || [];
return query.andWhere((builder) => {
builder.where("owner_user_id", userId);
if (allowedIds.length > 0) {
builder.orWhereIn("id", allowedIds);
}
});
};
export default ProxyHost;
+14 -2
View File
@@ -3,12 +3,12 @@
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";
Model.knex(db);
Model.knex(db());
const boolFields = [
"is_deleted",
@@ -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: {
+9 -9
View File
@@ -4,26 +4,26 @@
import { Model } from "objection";
import db from "../db.js";
Model.knex(db);
Model.knex(db());
class Setting extends Model {
$beforeInsert () {
$beforeInsert() {
// Default for meta
if (typeof this.meta === 'undefined') {
if (typeof this.meta === "undefined") {
this.meta = {};
}
}
static get name () {
return 'Setting';
static get name() {
return "Setting";
}
static get tableName () {
return 'setting';
static get tableName() {
return "setting";
}
static get jsonAttributes () {
return ['meta'];
static get jsonAttributes() {
return ["meta"];
}
}
+14 -2
View File
@@ -1,11 +1,11 @@
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";
Model.knex(db);
Model.knex(db());
const boolFields = ["is_deleted", "enabled", "tcp_forwarding", "udp_forwarding"];
@@ -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: {
+1 -1
View File
@@ -7,7 +7,7 @@ import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.j
import now from "./now_helper.js";
import UserPermission from "./user_permission.js";
Model.knex(db);
Model.knex(db());
const boolFields = ["is_deleted", "is_disabled"];
+12 -8
View File
@@ -5,24 +5,28 @@ import { Model } from "objection";
import db from "../db.js";
import now from "./now_helper.js";
Model.knex(db);
Model.knex(db());
class UserPermission extends Model {
$beforeInsert () {
this.created_on = now();
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
}
$beforeUpdate () {
$beforeUpdate() {
this.modified_on = now();
}
static get name () {
return 'UserPermission';
static get name() {
return "UserPermission";
}
static get tableName () {
return 'user_permission';
static get tableName() {
return "user_permission";
}
static get jsonAttributes() {
return ["meta"];
}
}
+29 -25
View File
@@ -8,40 +8,44 @@
"type": "module",
"scripts": {
"lint": "biome lint",
"test": "node --test test/",
"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": "^11.7.0",
"ajv": "^8.17.1",
"archiver": "^5.3.0",
"@apidevtools/json-schema-ref-parser": "^16.0.6",
"ajv": "^8.20.0",
"archiver": "^8.0.0",
"batchflow": "^0.4.0",
"bcrypt": "^5.0.0",
"body-parser": "^1.20.3",
"compression": "^1.7.4",
"express": "^4.20.0",
"express-fileupload": "^1.5.2",
"bcrypt": "^6.0.0",
"better-sqlite3": "^13.0.3",
"body-parser": "^2.4.0",
"chalk": "6.0.1",
"compression": "^1.8.2",
"express": "^5.3.0",
"express-fileupload": "^1.6.0",
"gravatar": "^1.8.2",
"jsonwebtoken": "^9.0.2",
"knex": "2.4.2",
"liquidjs": "10.6.1",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"mysql2": "^3.15.3",
"node-rsa": "^1.1.1",
"objection": "3.0.1",
"jsonwebtoken": "^9.0.3",
"knex": "3.3.0",
"liquidjs": "10.30.1",
"lodash": "^4.18.1",
"moment": "^2.31.0",
"mysql2": "^3.24.6",
"node-rsa": "^2.0.0",
"objection": "4.4.3",
"otplib": "^13.6.0",
"path": "^0.12.7",
"pg": "^8.16.3",
"proxy-agent": "^6.5.0",
"pg": "^8.23.1",
"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": "^10.1.0",
"@biomejs/biome": "^2.3.2",
"chalk": "4.1.2",
"nodemon": "^2.0.2"
"@apidevtools/swagger-parser": "^13.1.0",
"@biomejs/biome": "^2.6.6",
"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;
+112
View File
@@ -0,0 +1,112 @@
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;
+14
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";
@@ -14,6 +18,7 @@ import schemaRoutes from "./schema.js";
import settingsRoutes from "./settings.js";
import tokensRoutes from "./tokens.js";
import usersRoutes from "./users.js";
import versionRoutes from "./version.js";
const router = express.Router({
caseSensitive: true,
@@ -21,6 +26,8 @@ const router = express.Router({
mergeParams: true,
});
router.use(logRequest);
/**
* Health Check
* GET /api
@@ -44,8 +51,10 @@ 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);
router.use("/nginx/proxy-hosts", proxyHostsRoutes);
router.use("/nginx/redirection-hosts", redirectionHostsRoutes);
router.use("/nginx/dead-hosts", deadHostsRoutes);
@@ -53,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
*
+8 -30
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}`);
@@ -180,7 +161,7 @@ router
}
try {
const result = await internalCertificate.validate({
const result = await internalCertificate.validate(res.locals.access, {
files: req.files,
});
res.status(200).send(result);
@@ -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();
+69 -1
View File
@@ -1,10 +1,56 @@
import express from "express";
import internalToken from "../internal/token.js";
import jwtdecode from "../lib/express/jwt-decode.js";
import createRateLimiter from "../lib/express/rate-limit.js";
import apiValidator from "../lib/validator/api.js";
import { debug, express as logger } from "../logger.js";
import { getValidationSchema } from "../schema/index.js";
// Two limits apply to logins. Per IP + target account: when the admin UI sits behind another
// proxy every client can resolve to the same IP, and an IP-only bucket at this size would let
// anyone lock out every user with a handful of bad logins.
const tokenRateLimiter = createRateLimiter({
windowMs: 60 * 1000,
max: 10,
message: "Too many login attempts. Please try again in 1 minute.",
skipSuccessfulRequests: true,
keyGenerator: (req, clientIp) => {
const identity = typeof req.body?.identity === "string" ? req.body.identity.trim().toLowerCase() : "";
return `${clientIp}|${identity}`;
},
});
// And per IP across all accounts, so one address can't spray passwords over many accounts at
// 10 a minute each. Only failures count. Behind a shared proxy this cap is shared by everyone
// using it, which is why it's higher than the per-account one.
const tokenIpRateLimiter = createRateLimiter({
windowMs: 60 * 1000,
max: 30,
message: "Too many login attempts. Please try again in 1 minute.",
skipSuccessfulRequests: true,
});
// Keyed on the user the challenge belongs to, not the challenge token itself, so fetching
// fresh challenge tokens doesn't reset the code-guessing budget. The payload is read without
// verifying the signature; verify2FA does that. A forged token only affects buckets for the
// sender's own IP.
const challengeUserId = (token) => {
try {
const payload = JSON.parse(Buffer.from(String(token).split(".")[1], "base64url").toString());
return String(payload?.attrs?.id ?? "");
} catch {
return "";
}
};
const twoFactorRateLimiter = createRateLimiter({
windowMs: 60 * 1000,
max: 10,
message: "Too many login attempts. Please try again in 1 minute.",
skipSuccessfulRequests: true,
keyGenerator: (req, clientIp) => `${clientIp}|${challengeUserId(req.body?.challenge_token)}`,
});
const router = express.Router({
caseSensitive: true,
strict: true,
@@ -42,7 +88,7 @@ router
*
* Create a new Token
*/
.post(async (req, res, next) => {
.post(tokenIpRateLimiter, tokenRateLimiter, async (req, res, next) => {
try {
const data = await apiValidator(getValidationSchema("/tokens", "post"), req.body);
const result = await internalToken.getTokenFromEmail(data);
@@ -53,4 +99,26 @@ router
}
});
router
.route("/2fa")
.options((_, res) => {
res.sendStatus(204);
})
/**
* POST /tokens/2fa
*
* Verify 2FA code and get full token
*/
.post(twoFactorRateLimiter, async (req, res, next) => {
try {
const { challenge_token, code } = await apiValidator(getValidationSchema("/tokens/2fa", "post"), req.body);
const result = await internalToken.verify2FA(challenge_token, code);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;
+134 -38
View File
@@ -1,4 +1,5 @@
import express from "express";
import internal2FA from "../internal/2fa.js";
import internalUser from "../internal/user.js";
import Access from "../lib/access.js";
import { isCI } from "../lib/config.js";
@@ -47,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}`);
@@ -94,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) {
@@ -168,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) {
@@ -197,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);
@@ -247,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);
@@ -280,15 +259,9 @@ router
*/
.put(async (req, res, next) => {
try {
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 payload = await apiValidator(getValidationSchema("/users/{userID}/permissions", "put"), req.body);
payload.id = Number.parseInt(req.params.user_id, 10);
const result = await internalUser.setPermissions(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
@@ -325,4 +298,127 @@ router
}
});
/**
* User 2FA status
*
* /api/users/123/2fa
*/
router
.route("/:user_id/2fa")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* POST /api/users/123/2fa
*
* Start 2FA setup, returns QR code URL
*/
.post(async (req, res, next) => {
try {
const result = await internal2FA.startSetup(res.locals.access, req.params.user_id);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* GET /api/users/123/2fa
*
* Get 2FA status for a user
*/
.get(async (req, res, next) => {
try {
const status = await internal2FA.getStatus(res.locals.access, req.params.user_id);
res.status(200).send(status);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/users/123/2fa?code=XXXXXX
*
* Disable 2FA for a user
*/
.delete(async (req, res, next) => {
try {
const code = typeof req.query.code === "string" ? req.query.code : null;
if (!code) {
throw new errs.ValidationError("Missing required parameter: code");
}
await internal2FA.disable(res.locals.access, req.params.user_id, code);
res.status(200).send(true);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* User 2FA enable
*
* /api/users/123/2fa/enable
*/
router
.route("/:user_id/2fa/enable")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* POST /api/users/123/2fa/enable
*
* Verify code and enable 2FA
*/
.post(async (req, res, next) => {
try {
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) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* User 2FA backup codes
*
* /api/users/123/2fa/backup-codes
*/
router
.route("/:user_id/2fa/backup-codes")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* POST /api/users/123/2fa/backup-codes
*
* Regenerate backup codes
*/
.post(async (req, res, next) => {
try {
const { code } = await apiValidator(
getValidationSchema("/users/{userID}/2fa/backup-codes", "post"),
req.body,
);
const result = await internal2FA.regenerateBackupCodes(res.locals.access, req.params.user_id, code);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;
+40
View File
@@ -0,0 +1,40 @@
import express from "express";
import internalRemoteVersion from "../internal/remote-version.js";
import { debug, express as logger } from "../logger.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/version/check
*/
router
.route("/check")
.options((_, res) => {
res.sendStatus(204);
})
/**
* GET /api/version/check
*
* Check for available updates
*/
.get(async (req, res, _next) => {
try {
const data = await internalRemoteVersion.get();
res.status(200).send(data);
} catch (error) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${error}`);
// Send 200 even though there's an error to avoid triggering update checks repeatedly
res.status(200).send({
current: null,
latest: null,
update_available: false,
});
}
});
export default router;
+4 -3
View File
@@ -84,7 +84,7 @@
"uniqueItems": true,
"items": {
"type": "string",
"pattern": "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"]+$"
"pattern": "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"\\s\\p{Cc}]+$"
},
"example": ["example.com", "www.example.com"]
},
@@ -147,7 +147,7 @@
},
{
"type": "string",
"pattern": "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$"
"pattern": "^(?:(?:[0-9A-Fa-f]{1,4}:){7}(?:[0-9A-Fa-f]{1,4}|:)|(?:[0-9A-Fa-f]{1,4}:){6}(?::[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}|:)|(?:[0-9A-Fa-f]{1,4}:){5}(?:(?::[0-9A-Fa-f]{1,4}){1,2}|:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}|:)|(?:[0-9A-Fa-f]{1,4}:){4}(?:(?::[0-9A-Fa-f]{1,4}){1,3}|(?::[0-9A-Fa-f]{1,4})?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}|:)|(?:[0-9A-Fa-f]{1,4}:){3}(?:(?::[0-9A-Fa-f]{1,4}){1,4}|(?::[0-9A-Fa-f]{1,4}){0,2}:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}|:)|(?:[0-9A-Fa-f]{1,4}:){2}(?:(?::[0-9A-Fa-f]{1,4}){1,5}|(?::[0-9A-Fa-f]{1,4}){0,3}:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}|:)|(?:[0-9A-Fa-f]{1,4}:)(?:(?::[0-9A-Fa-f]{1,4}){1,6}|(?::[0-9A-Fa-f]{1,4}){0,4}:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}|:)|:(?:(?::[0-9A-Fa-f]{1,4}){1,7}|(?::[0-9A-Fa-f]{1,4}){0,5}:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}|:))(?:/(?:[0-9]|[1-9][0-9]|1[01][0-9]|12[0-8]))?$"
},
{
"type": "string",
@@ -164,7 +164,8 @@
"properties": {
"username": {
"type": "string",
"minLength": 1
"minLength": 1,
"pattern": "^[^:\\p{Cc}]+$"
},
"password": {
"type": "string"
@@ -31,7 +31,7 @@
"uniqueItems": true,
"items": {
"type": "string",
"pattern": "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"]+$"
"pattern": "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"\\s\\p{Cc}]+$"
},
"example": ["example.com", "www.example.com"]
},
@@ -71,6 +71,11 @@
"propagation_seconds": {
"type": "integer",
"minimum": 0
},
"key_type": {
"type": "string",
"enum": ["rsa", "ecdsa"],
"default": "rsa"
}
},
"example": {
@@ -0,0 +1,23 @@
{
"type": "object",
"description": "Check Version object",
"additionalProperties": false,
"required": ["current", "latest", "update_available"],
"properties": {
"current": {
"type": ["string", "null"],
"description": "Current version string",
"example": "v2.10.1"
},
"latest": {
"type": ["string", "null"],
"description": "Latest version string",
"example": "v2.13.4"
},
"update_available": {
"type": "boolean",
"description": "Whether there's an update available",
"example": true
}
}
}
@@ -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
}
}
}
@@ -1,6 +1,7 @@
{
"type": "object",
"minProperties": 1,
"additionalProperties": false,
"properties": {
"visibility": {
"type": "string",
@@ -43,6 +44,25 @@
"description": "Certificates Permissions",
"enum": ["hidden", "view", "manage"],
"example": "hidden"
},
"meta": {
"type": "object",
"description": "Permission metadata",
"example": {
"proxy_host_ids": [1]
},
"properties": {
"proxy_host_ids": {
"type": "array",
"description": "Allowed Proxy Host IDs",
"example": [1],
"items": {
"type": "integer",
"description": "Proxy Host ID",
"example": 1
}
}
}
}
}
}
@@ -22,7 +22,8 @@
"enabled",
"locations",
"hsts_enabled",
"hsts_subdomains"
"hsts_subdomains",
"trust_forwarded_proto"
],
"properties": {
"id": {
@@ -44,6 +45,7 @@
"type": "string",
"minLength": 1,
"maxLength": 255,
"pattern": "^[^\\s\\p{Cc};{}\"'#\\\\$/]+(?:/[^\\s\\p{Cc};{}\"'#\\\\]*)?$",
"example": "127.0.0.1"
},
"forward_port": {
@@ -107,7 +109,8 @@
},
"path": {
"type": "string",
"minLength": 1
"minLength": 1,
"pattern": "^(?!(?:=|~\\*?|\\^~)$)(?:(?:=|~\\*?|\\^~) +)?(?:\"(?:[^\"\\\\\\p{Cc}]|\\\\[^\\p{Cc}])+\"|(?:[^\\s\\p{Cc};{}\"'#\\\\]|\\\\[^\\s\\p{Cc}])+)$"
},
"forward_scheme": {
"$ref": "#/properties/forward_scheme"
@@ -119,10 +122,14 @@
"$ref": "#/properties/forward_port"
},
"forward_path": {
"type": "string"
"type": "string",
"pattern": "^[^\\s\\p{Cc};{}\"'#\\\\]*$"
},
"advanced_config": {
"type": "string"
},
"access_list_id": {
"$ref": "../common.json#/properties/access_list_id"
}
}
},
@@ -131,7 +138,8 @@
"path": "/app",
"forward_scheme": "http",
"forward_host": "example.com",
"forward_port": 80
"forward_port": 80,
"access_list_id": 0
}
]
},
@@ -141,6 +149,11 @@
"hsts_subdomains": {
"$ref": "../common.json#/properties/hsts_subdomains"
},
"trust_forwarded_proto":{
"type": "boolean",
"description": "Trust the forwarded headers",
"example": false
},
"certificate": {
"oneOf": [
{
@@ -57,7 +57,7 @@
"forward_domain_name": {
"description": "Domain Name",
"type": "string",
"pattern": "^(?:[^.*]+\\.?)+[^.]$",
"pattern": "^(?=[^\\s\\p{Cc};{}\"'#\\\\]+$)[^.*]+(?:\\.[^.*]+)*\\.?[^.]$",
"example": "jc21.com"
},
"preserve_path": {
+2 -2
View File
@@ -39,12 +39,12 @@
{
"description": "Domain Name",
"type": "string",
"pattern": "^(?:[^.*]+\\.?)+[^.]$",
"pattern": "^(?=[^\\s\\p{Cc};{}\"'#\\\\$/]+$)[^.*]+(?:\\.[^.*]+)*\\.?[^.]$",
"example": "example.com"
},
{
"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,18 @@
{
"type": "object",
"description": "Token object",
"required": ["requires_2fa", "challenge_token"],
"additionalProperties": false,
"properties": {
"requires_2fa": {
"description": "Whether this token request requires two-factor authentication",
"example": true,
"type": "boolean"
},
"challenge_token": {
"description": "Challenge Token used in subsequent 2FA verification",
"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
"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"
}
}
}
}
}
}
@@ -35,6 +35,26 @@
}
}
}
},
"400": {
"description": "400 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"error": {
"code": 400,
"message": "Certificate is in use by 1 proxy host. Remove it from them before deleting it."
}
}
}
},
"schema": {
"$ref": "../../../../components/error.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,
+55
View File
@@ -0,0 +1,55 @@
{
"operationId": "loginWith2FA",
"summary": "Verify 2FA code and get full token",
"tags": ["tokens"],
"requestBody": {
"description": "2fa Challenge Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"challenge_token": {
"minLength": 1,
"type": "string",
"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
},
"code": {
"minLength": 6,
"maxLength": 8,
"type": "string",
"example": "012345"
}
},
"required": ["challenge_token", "code"],
"type": "object"
},
"example": {
"challenge_token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
"code": "012345"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"expires": "2025-02-04T20:40:46.340Z",
"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
}
}
},
"schema": {
"$ref": "../../../components/token-object.json"
}
}
},
"description": "200 response"
}
}
}
+8 -1
View File
@@ -50,7 +50,14 @@
}
},
"schema": {
"$ref": "../../components/token-object.json"
"oneOf": [
{
"$ref": "../../components/token-object.json"
},
{
"$ref": "../../components/token-challenge.json"
}
]
}
}
},
@@ -0,0 +1,92 @@
{
"operationId": "regenUser2faCodes",
"summary": "Regenerate 2FA backup codes",
"tags": ["users"],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
}
],
"requestBody": {
"description": "Verification Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"code": {
"minLength": 6,
"maxLength": 8,
"type": "string",
"example": "123456"
}
},
"required": ["code"],
"type": "object"
},
"example": {
"code": "123456"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"backup_codes": [
"6CD7CB06",
"495302F3",
"D8037852",
"A6FFC956",
"BC1A1851",
"A05E644F",
"A406D2E8",
"0AE3C522"
]
}
}
},
"schema": {
"type": "object",
"required": ["backup_codes"],
"additionalProperties": false,
"properties": {
"backup_codes": {
"description": "Backup codes",
"example": [
"6CD7CB06",
"495302F3",
"D8037852",
"A6FFC956",
"BC1A1851",
"A05E644F",
"A406D2E8",
"0AE3C522"
],
"type": "array",
"items": {
"type": "string",
"example": "6CD7CB06"
}
}
}
}
}
},
"description": "200 response"
}
}
}
@@ -0,0 +1,48 @@
{
"operationId": "disableUser2fa",
"summary": "Disable 2fa for user",
"tags": ["users"],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
},
{
"in": "query",
"name": "code",
"schema": {
"type": "string",
"minLength": 6,
"maxLength": 6,
"example": "012345"
},
"required": true,
"description": "2fa Code",
"example": "012345"
}
],
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
},
"description": "200 response"
}
}
}
@@ -0,0 +1,92 @@
{
"operationId": "enableUser2fa",
"summary": "Verify code and enable 2FA",
"tags": ["users"],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
}
],
"requestBody": {
"description": "Verification Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"code": {
"minLength": 6,
"maxLength": 8,
"type": "string",
"example": "123456"
}
},
"required": ["code"],
"type": "object"
},
"example": {
"code": "123456"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"backup_codes": [
"6CD7CB06",
"495302F3",
"D8037852",
"A6FFC956",
"BC1A1851",
"A05E644F",
"A406D2E8",
"0AE3C522"
]
}
}
},
"schema": {
"type": "object",
"required": ["backup_codes"],
"additionalProperties": false,
"properties": {
"backup_codes": {
"description": "Backup codes",
"example": [
"6CD7CB06",
"495302F3",
"D8037852",
"A6FFC956",
"BC1A1851",
"A05E644F",
"A406D2E8",
"0AE3C522"
],
"type": "array",
"items": {
"type": "string",
"example": "6CD7CB06"
}
}
}
}
}
},
"description": "200 response"
}
}
}
@@ -0,0 +1,57 @@
{
"operationId": "getUser2faStatus",
"summary": "Get user 2fa Status",
"tags": ["users"],
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"enabled": false,
"backup_codes_remaining": 0
}
}
},
"schema": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "backup_codes_remaining"],
"properties": {
"enabled": {
"type": "boolean",
"description": "Is 2FA enabled for this user",
"example": true
},
"backup_codes_remaining": {
"type": "integer",
"description": "Number of remaining backup codes for this user",
"example": 5
}
}
}
}
}
}
}
}
Loaded 100 of 514 files, more files were not shown because too many files have changed in this diff. Show more