Compare commits

..
863 Commits
Author SHA1 Message Date
jc21andGitHub 76f09db610 Merge pull request #5619 from NginxProxyManager/develop
v2.15.1
2026-06-03 14:14:30 +10:00
jc21andGitHub 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
Jamie Curnow 0d832092a5 Only modify files in startup when they aren't mounted 2026-06-02 22:09:21 +10:00
jc21andGitHub 52a7b7dca1 Merge pull request #5593 from NginxProxyManager/develop
v2.15.0
2026-06-01 08:28:34 +10:00
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub e48695401e Merge branch 'develop' into dependabot/npm_and_yarn/backend/qs-6.15.2 2026-05-27 11:08:07 +10:00
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 840a720d09 Merge pull request #5550 from rfos/develop
Add Hostinger DNS plugin configuration
2026-05-18 07:44:59 +10:00
Reinaldo FerroandGitHub 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
jc21andGitHub 43e0911ce4 Merge pull request #5352 from MarioGervais/patch-1
feat(i18n): complete French (fr) translation
2026-05-14 14:16:56 +10:00
jc21andGitHub 0242187db0 Add message for available update in French locale 2026-05-14 14:16:00 +10:00
jc21andGitHub 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
jc21andGitHub ba2f714b55 Merge branch 'develop' into docs/add-selfhostingsh-guide 2026-05-14 14:03:27 +10:00
jc21andGitHub 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
jc21andGitHub ebcd2a209c Merge pull request #5415 from 12LuA/develop
Fix typo in debug mode comment
2026-05-14 11:43:08 +10:00
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub d3ba40e066 Merge pull request #5485 from butalex11/add-hosterby-dns-provider
Add hoster.by DNS provider
2026-05-14 10:51:00 +10:00
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 70538af311 Merge pull request #5520 from D0LBA3B/feature/sortable-tables
Adds column sorting to Nginx tables
2026-05-14 08:56:35 +10:00
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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
jc21andGitHub 6719cfc6a0 Merge pull request #5437 from Flop2691/develop
Updating french translation
2026-05-13 15:03:24 +10:00
jc21andGitHub 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
jc21andGitHub a0fb4510f7 Merge pull request #5395 from kraineff/develop
Update ru.json
2026-05-13 14:57:13 +10:00
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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
MarkusandGitHub b66fd9d3ae Added PUT and DELETE method to Access-Control-Allow-Methods field 2026-05-12 09:24:28 +02:00
LudovicandGitHub 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 PaternotteandGitHub a6f8b7dd1d Update nl.json 2026-05-05 19:53:16 +02:00
Stephan PaternotteandGitHub 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
Tomáš Bára 26a2bcab42 Fix translation in streams table ignoring current enable/disable state 2026-04-16 09:13:10 +02:00
Alexander GumiandGitHub cea69e4ba5 Add hoster.by DNS provider 2026-04-14 11:05:03 +03:00
Jhonatan carvajal antiguaandGitHub 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 PaternotteandGitHub 218e32bf15 Update nl.json
Hyphen inserted for "404-Host"
2026-04-13 17:24:39 +02:00
Stephan PaternotteandGitHub 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 PaternotteandGitHub 871787ac95 Update nl.json
fixes for CRs by @TheMazeIsAmazing
Thanks. Well spotted!
2026-04-13 16:18:04 +02:00
MichaelJanssenNesaiandGitHub 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 PaternotteandGitHub 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 JainandGitHub 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 PaternotteandGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
dependabot[bot]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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
jc21andGitHub 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]andGitHub 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
MarioGervaisandGitHub 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
MarioGervaisandGitHub 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]andGitHub 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
jc21andGitHub 52be66c43e Merge pull request #5346 from siimaarmaa/develop
Added Estonia langugae support.
2026-02-25 08:38:17 +10:00
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 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]andJamie Curnow 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]andJamie Curnow 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
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 846b94f7e8 Merge pull request #5324 from biodland/develop
chore: added Norwegian translation
2026-02-19 07:51:23 +10:00
BirgerandGitHub 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
jc21andGitHub 84fb2729af Merge pull request #5318 from NginxProxyManager/develop
v2.14.0
2026-02-17 15:32:19 +10:00
jc21andGitHub c7437ddf8f Merge branch 'master' into develop 2026-02-17 15:02:29 +10:00
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub e5df45e9ef Merge pull request #5279 from dodog/develop
Update Slovak translation
2026-02-17 11:05:51 +10:00
dependabot[bot]andGitHub 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]andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub f3c46487f6 Merge pull request #5303 from 7heMech/fix-2fa-logout
Add guardrail to fix disabling 2fa
2026-02-16 10:27:58 +10:00
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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
kiaxseventhandGitHub 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 GaalandGitHub 7c67fafedf Update Slovak translation
Updated Slovak translations for 2FA and other features
2026-02-06 01:23:59 +01:00
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 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块andGitHub b7402d47a0 Merge branch 'NginxProxyManager:develop' into develop 2026-02-03 15:10:13 +08:00
jc21andGitHub 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]andGitHub 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
jc21andGitHub 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
jc21andGitHub 9cf1d000c8 Merge pull request #5257 from maghuro/add-pt-pt
Add pt-PT lang
2026-02-03 17:04:19 +10:00
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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
maghuroandGitHub c515815b0e Remove merge conflict markers from lang-list.json 2026-01-31 12:10:36 +00:00
maghuroandGitHub 3db02370fd Add Portuguese language support to IntlProvider 2026-01-31 12:08:07 +00:00
maghuroandGitHub 4ad1af5576 Remove duplicate locale entries and keep pt-PT 2026-01-31 12:07:32 +00:00
maghuroandGitHub a73d54fedc Add Portuguese (European) language support 2026-01-31 12:06:50 +00:00
maghuroandGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 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]andGitHub 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
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 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]andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 3308a308df Merge pull request #5185 from Lokowitz/add-dependa-config
Add Dependabot config
2026-01-25 22:29:28 +10:00
jc21andGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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
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
jc21andGitHub 7747db994d Merge pull request #5087 from xJayMorex/update-cloudns
Fixed #4715 by updating certbot-dns-cloudns
2026-01-18 20:00:15 +10:00
jc21andGitHub 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
jc21andGitHub 9ea6fee3ce Merge pull request #4930 from blinkerfluessigkeit/lang-de
Update German translations
2026-01-15 09:58:07 +10:00
jc21andGitHub 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
GedasMirakandJamie Curnow 0b464ac9fd Add french locale 2026-01-14 15:01:33 +10:00
jc21andGitHub f3efaae320 Merge pull request #5141 from NginxProxyManager/develop
v2.13.6
2026-01-14 14:30:49 +10:00
jc21andGitHub 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
jc21andGitHub b7dfaddbb1 Merge pull request #4970 from zdzichu6969/develop
Polish Translation Fixes
2026-01-14 07:33:49 +10:00
jc21andGitHub 11ee4f0820 Merge pull request #4965 from archettitechnology/develop
Update Italian locale message for empty objects
2026-01-14 07:32:07 +10:00
jc21andGitHub 19970a4220 Merge pull request #5095 from aindriu80/develop
feat: (i18n) Added Irish translation
2026-01-14 07:26:10 +10:00
jc21andGitHub 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
jc21andGitHub 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]andGitHub 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]andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 3e3396ba9a Update lang-list.json 2026-01-13 15:05:13 +10:00
jc21andGitHub 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
jc21andGitHub 8c8221a352 Merge pull request #5037 from vtj-mizuno/fix-japanese-translate
Fix Japanese translate
2026-01-13 14:53:07 +10:00
jc21andGitHub 582681e3ff Merge pull request #5080 from bzuro/develop
Change visibility to permission_visibility in report.js
2026-01-13 14:52:45 +10:00
jc21andGitHub 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
jc21andGitHub 6c0ea835ce Merge branch 'develop' into develop 2026-01-13 14:46:35 +10:00
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub b30f8e47e2 Merge pull request #5109 from piotrfx/develop
Add TOTP-based two-factor authentication
2026-01-13 14:30:48 +10:00
jc21andGitHub 6fa30840be Merge pull request #5114 from Shotz5/develop
Added logging for streams based on port
2026-01-13 14:18:13 +10:00
jc21andGitHub 05726aaab9 Merge pull request #5119 from manisto/develop
Added support for DNS challenges with Simply.com
2026-01-13 14:14:38 +10:00
jc21andGitHub 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
mobilandiandGitHub f25afa3590 Change version constraint for certbot-dns-kas 2026-01-03 23:08:34 +01:00
mobilandiandGitHub 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
bzuroandGitHub 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
dupsatouandGitHub 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
angiolettoandGitHub 7fe5070337 Merge branch 'NginxProxyManager:develop' into develop 2025-12-06 14:56:52 +01:00
CamelT0EandGitHub 1b8f1fbb79 Update German locale message from 'German' to 'Deutsch' 2025-12-06 01:30:56 +01:00
dependabot[bot]andGitHub 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
jc21andGitHub d18c8cf4f1 Merge pull request #4979 from abinas-hdb/develop
Add Korean Locale
2025-11-26 14:04:31 +10:00
abinasandGitHub bf4eab541a Update index.ts
Fix missing 'ko' in index.ts
2025-11-26 11:57:05 +09:00
jc21andGitHub 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
jc21andGitHub ba43c144f6 Merge branch 'develop' into develop 2025-11-26 08:35:32 +10:00
jc21andGitHub 896951f6cd Merge pull request #4985 from Bare7a/bg-locale
Add Bulgarian Language Support
2025-11-26 08:33:55 +10:00
jc21andGitHub 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
Bare7aandGitHub 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
angiolettoandGitHub 927e57257b Merge branch 'NginxProxyManager:develop' into develop 2025-11-21 17:03:47 +01:00
abinasandGitHub e353a66556 Update IntlProvider.tsx 2025-11-22 00:33:27 +09:00
abinasandGitHub 991bddf891 Add Korean translation 2025-11-22 00:18:36 +09:00
abinasandGitHub c076ad145c Add Korean translation 2025-11-22 00:18:19 +09:00
abinasandGitHub 80cf4406d5 Update Korean language support 2025-11-22 00:15:08 +09:00
abinasandGitHub 3cb124d5a0 Update Korean language support 2025-11-22 00:14:45 +09:00
abinasandGitHub 03b0513a24 Add Korean translation 2025-11-22 00:12:33 +09:00
jc21andGitHub 0528d65317 Merge pull request #4964 from xluyenx/develop
Correct Vietnam flag
2025-11-20 11:54:55 +10:00
jc21andGitHub 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
7heMechandGitHub 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
angiolettoandGitHub 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'sandGitHub e3cdc8bb30 Update IntlProvider.tsx 2025-11-19 11:37:20 +07:00
Louis Tran'sandGitHub ba79eefe5e Merge pull request #1 from xluyenx/xluyenx-patch-1
Update IntlProvider.tsx
2025-11-19 11:30:49 +07:00
Louis Tran'sandGitHub bb94ce75c1 Update IntlProvider.tsx
Correct Vietnam flag
2025-11-19 11:27:42 +07:00
jc21andGitHub 847c58b170 Merge pull request #4956 from NginxProxyManager/develop
v2.13.5
2025-11-18 21:13:24 +10:00
jc21andGitHub 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
jc21andGitHub cfa98361d1 Merge pull request #4955 from NginxProxyManager/lang-nl
Add Dutch language - resolves #4935
2025-11-18 19:03:48 +10:00
JelcooandJamie Curnow c2177abe39 Add language to frontend settings & correct some translations 2025-11-18 19:00:00 +10:00
JelcooandJamie Curnow 2c6d614597 Add HelpDoc translations 2025-11-18 18:58:26 +10:00
JelcooandJamie Curnow 484ce8db3c Add Dutch language 2025-11-18 18:57:40 +10:00
jc21andGitHub 2c11c0c7e2 Merge pull request #4937 from archettitechnology/develop
Add Italian Language Support
2025-11-18 18:50:52 +10:00
jc21andGitHub f1039ce2ef Merge pull request #4928 from 7heMech/develop
UI/UX improvements
2025-11-18 18:37:22 +10:00
jc21andGitHub 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
jc21andGitHub a87f24c9dc Merge pull request #4940 from vsc55/issues_4939
Fix issues #4939, #4938
2025-11-18 18:29:04 +10:00
jc21andGitHub decdfec447 Merge branch 'develop' into develop 2025-11-18 18:27:00 +10:00
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub de7d3b0d19 Merge pull request #4950 from dominhhieu1405/develop
Add Vietnamese Support
2025-11-18 18:22:43 +10:00
jc21andGitHub 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 GaalandGitHub 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 GaoandGitHub 87ec9c4bdf Fix message for GitHub fork reference in zh.json 2025-11-15 20:09:19 +08:00
dependabot[bot]andGitHub 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
angiolettoandGitHub 6cae088432 Rename ProxyHost.md to ProxyHosts.md
i think have problem with letter s ahaha
2025-11-14 08:16:41 +01:00
angiolettoandGitHub 9d8c4cc30b Rename DeadHost.md to DeadHosts.md 2025-11-14 08:14:26 +01:00
angiolettoandGitHub 66ebecdb43 Merge branch 'develop' into develop 2025-11-14 08:01:32 +01:00
angiolettoandGitHub 60f3ee03c0 Fix typo in file name from 'indes.ts' to 'index.ts'
typing error
2025-11-14 08:00:30 +01:00
jc21andGitHub a4d54a0291 Merge pull request #4932 from kraineff/develop
Update Russian locale
2025-11-14 16:58:05 +10:00
angiolettoandGitHub 7536b1b1c9 Merge branch 'develop' into develop 2025-11-14 07:19:32 +01:00
angiolettoandGitHub 5288fbd7af Update index.ts 2025-11-14 07:18:14 +01:00
jc21andGitHub 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
angiolettoandGitHub 0bcfe0bba6 Add Italian language support to lang-list.json 2025-11-13 21:12:52 +01:00
angiolettoandGitHub 74cbfb2c58 Create indes.ts to export HelpDoc modules 2025-11-13 21:12:15 +01:00
angiolettoandGitHub 8ef65caa5a Add Italian documentation for Streams feature 2025-11-13 21:11:19 +01:00
angiolettoandGitHub bc341c1dff Add RedirectionHosts.md with explanation in Italian 2025-11-13 21:10:36 +01:00
angiolettoandGitHub 5fc9febf1f Update title of ProxyHost.md in Italian 2025-11-13 21:09:40 +01:00
angiolettoandGitHub b23ceebfd8 Add Italian documentation for ProxyHost 2025-11-13 21:09:23 +01:00
angiolettoandGitHub c281fc54a1 Add Italian HelpDoc for 404 Host explanation 2025-11-13 21:08:50 +01:00
angiolettoandGitHub d0f7dc5b48 Add Italian HelpDoc for certificate options 2025-11-13 21:07:26 +01:00
angiolettoandGitHub fb53df862e Add Italian documentation for Access Lists 2025-11-13 21:03:33 +01:00
angiolettoandGitHub 8d8463ae41 Add Italian language support to HelpDoc 2025-11-13 20:57:52 +01:00
angiolettoandGitHub 8774cfe5f9 Add Italian locale to check-locales 2025-11-13 20:56:42 +01:00
angiolettoandGitHub 4ca5cadd19 Add Italian language support to IntlProvider 2025-11-13 20:55:35 +01:00
angiolettoandGitHub 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
jc21andGitHub 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 SpartalisandGitHub 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
7heMechandGitHub 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
jc21andGitHub 6ce9567e48 Merge pull request #4816 from fhennig42/azure-dns
Bump certbot-azure-dns version
2025-11-13 07:13:11 +10:00
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub c41451618e Merge pull request #4924 from zdzichu6969/develop
Add Polish locale
2025-11-12 22:59:23 +10:00
jc21andGitHub 1a3d45f6bc Merge branch 'develop' into develop 2025-11-12 22:14:28 +10:00
jc21andGitHub 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
jc21andGitHub 3c5ff81a54 Merge pull request #4910 from 7heMech/develop
Add scheme back in destination
2025-11-12 20:48:56 +10:00
jc21andGitHub 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
firfeandJamie Curnow d66bb2104a Add the new translation for "redirection-host.forward-http-code". 2025-11-12 20:23:36 +10:00
firfeandJamie Curnow 8e900dbc92 Add Chinese HelpDoc 2025-11-12 20:23:34 +10:00
firfeandJamie Curnow 66aac3eb3e Add Chinese 中文 2025-11-12 20:22:57 +10:00
jc21andGitHub 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
FrankandGitHub 0344bb3c19 fix: Grammatical change
fix: Grammatical change
2025-11-12 10:47:53 +01:00
FrankandGitHub 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
jc21andGitHub 4557244744 Merge pull request #4870 from kraineff/develop
Add Russian Support
2025-11-12 18:51:43 +10:00
jc21andGitHub f649288098 Merge branch 'develop' into develop 2025-11-12 18:39:05 +10:00
jc21andGitHub 28df6db52b Merge pull request #4848 from Oka-Tak/develop
Add Japanese language support and translations
2025-11-12 18:36:18 +10:00
jc21andGitHub eee749652c Merge pull request #4917 from lastsamurai26/develop
Fix: wrong translate and adding missing translations
2025-11-12 18:13:08 +10:00
jc21andGitHub f6aa25b9b3 Merge branch 'develop' into develop 2025-11-12 18:12:10 +10:00
FrankandGitHub 40db26b686 Merge branch 'NginxProxyManager:develop' into develop 2025-11-12 08:06:36 +01:00
FrankandGitHub 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
FrankandGitHub 86c7cbddab Add certificate renewal message in German locale
Fix: add missing translation for renew certificates
2025-11-12 07:34:44 +01:00
FrankandGitHub e52975bf6c Translate 'Renew Certificate' to German
Fix: add missing translation for renew certificates
2025-11-12 07:34:42 +01:00
FrankandGitHub 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
jc21andGitHub 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ópezandGitHub 8773ce25d7 Merge branch 'develop' into lang-spanish 2025-11-12 02:14:09 +01:00
jc21andGitHub 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 SpartalisandGitHub 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
FrankandGitHub ba79bbc750 Update German translation for HTTP code
fix: Updated column http code
2025-11-11 08:56:32 +01:00
FrankandGitHub a7231777aa FIX: Update HTTP code message in German locale
fix: Updated column http code
2025-11-11 08:56:20 +01:00
jc21andGitHub 2578105f86 Merge pull request #4907 from NginxProxyManager/develop
v2.13.3
2025-11-11 16:54:38 +10:00
FrankandGitHub 3a6b221b0c Add HTTP Code translation to German locale
new: redirection-host.forward-http-code added
2025-11-11 07:13:13 +01:00
FrankandGitHub 12b000abb9 Add HTTP Code message to German locale
new: redirection-host.forward-http-code added
2025-11-11 07:12:57 +01:00
jc21andGitHub 39c9bbb167 Merge branch 'master' into develop 2025-11-11 16:06:05 +10:00
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
jc21andGitHub 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
mamasch19andGitHub 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 PaulandGitHub 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 PaulandGitHub 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
jc21andGitHub 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
FrankandGitHub b21d6d9d78 Fix German translations
Fix: German translations
2025-11-05 08:09:10 +01:00
FrankandGitHub bf1ad15ed7 Update de.json
fix: typos
2025-11-05 08:08:50 +01:00
FrankandGitHub 1209303a1d Update DeadHosts.md
fix: translation "Umgangssprachlich"
2025-11-05 08:00:15 +01:00
FrankandGitHub cd3a09ebf6 Update Certificates.md
fix: typo
2025-11-05 07:59:45 +01:00
FrankandGitHub d0e20d4f1b Update de.json
fix: typo dark and light mode
2025-11-05 07:57:11 +01:00
FrankandGitHub ceb098fcfe Fix typo in German locale for min character length
fix: typo mainimale should be minimale
2025-11-05 07:53:56 +01:00
FrankandGitHub 639ba3a525 Update de.json
fix: typo 
fix: translate Location with Pfad
2025-11-05 07:52:28 +01:00
jc21andGitHub e88d55f1d2 Merge pull request #4839 from NginxProxyManager/develop
v2.13.1
2025-11-05 15:40:32 +10:00
Jamie Curnow 4cb85f6480 Fix #4833 supports the usual proxy env vars for outgoing admin related requests 2025-11-05 15:16:42 +10:00
jc21andGitHub df7dea2d16 Merge branch 'master' into develop 2025-11-05 12:35:06 +10:00
Jamie Curnow 23f4948bde Bump version 2025-11-05 12:33:59 +10:00
Jamie Curnow 0ceb7d0892 Fix #4838 when showing avatars of deleted users 2025-11-05 12:33:13 +10:00
Jamie Curnow f35671db21 Fix #4837 for those with older config 2025-11-05 10:56:23 +10:00
Jamie Curnow a3a0614948 Fix #4828 showing incorrect certicificate value 2025-11-05 10:21:55 +10:00
Florian Hennig a85b5f664f Bump version after rebase 2025-11-04 20:03:09 +01:00
Jamie Curnow 06b67ed4bc Remove user name column from audit log 2025-11-04 14:57:10 +10:00
Jamie Curnow 4a0e27572e Fix missing translation for renew cert dialog 2025-11-04 14:54:02 +10:00
jc21andGitHub fbea8dfa9e Merge pull request #4825 from NginxProxyManager/develop
v2.13.0
2025-11-04 14:23:00 +10:00
Jamie Curnow 8c37348b65 Properly wrap debug calls 2025-11-04 13:43:52 +10:00
Jamie Curnow 2b3e9d72f4 Updated docs screenshots 2025-11-04 13:05:21 +10:00
jc21andGitHub a3e5235d81 Merge branch 'master' into develop 2025-11-04 07:47:04 +10:00
jc21andGitHub 9875fa92f1 Merge pull request #4794 from Johno-ACSLive/develop
Add basic MySQL TLS support
2025-11-04 07:13:15 +10:00
FrankandGitHub 7e28d8a5d6 Add files via upload
add german
2025-11-03 17:51:48 +01:00
FrankandGitHub 8991e88ff3 Update de.json 2025-11-03 14:22:13 +01:00
FrankandGitHub e2a8ffa2d3 Add files via upload
Add German
2025-11-03 14:18:08 +01:00
jc21andGitHub ef5156b613 Merge pull request #4813 from potatojuicemachine/develop
Adds Hetzner Cloud to available plugins
2025-11-03 13:38:11 +10:00
Jamie Curnow b9a34ebb7e Revert to cypress 14, 15 was causing problems with executing external commands 2025-11-03 12:53:23 +10:00
Jamie Curnow 7642d0a000 Cleanup cypress tests 2025-11-03 12:35:58 +10:00
Jamie Curnow 7a6a9de0ea Update frontend deps 2025-11-03 10:53:46 +10:00
Jamie Curnow a5d50f9588 Update test deps 2025-11-03 10:52:53 +10:00
Jamie Curnow 612695c2e8 Upgrade biomejs 2025-11-03 10:51:16 +10:00
Jonathon Aroutsidis 71a2277b9b Replace spaces with tabs 2025-11-03 10:48:14 +11:00
Jonathon Aroutsidis 5acf287ea7 Aligned Assignments and arrow-parens 2025-11-03 10:48:14 +11:00
Jonathon Aroutsidis e34206b526 Include SSL Options for MySQL 2025-11-03 10:46:20 +11:00
jc21andGitHub 6b00adf8b9 Merge pull request #4725 from NginxProxyManager/dependabot/npm_and_yarn/test/eslint/plugin-kit-0.3.5
Bump @eslint/plugin-kit from 0.3.2 to 0.3.5 in /test
2025-11-03 08:49:30 +10:00
jc21andGitHub a93558278e Merge pull request #4763 from NginxProxyManager/dependabot/npm_and_yarn/test/axios-1.12.0
Bump axios from 1.10.0 to 1.12.0 in /test
2025-11-03 08:37:03 +10:00
jc21andGitHub bc2867b357 Merge pull request #4803 from NginxProxyManager/dependabot/npm_and_yarn/docs/vite-5.4.21
Bump vite from 5.4.19 to 5.4.21 in /docs
2025-11-03 08:18:00 +10:00
jc21andGitHub 52093ba258 Merge pull request #4805 from vlauciani/patch-1
Update PostgreSQL volume path in setup documentation for 18+
2025-11-03 08:15:23 +10:00
jc21andGitHub 24216f1f2f Merge pull request #4785 from NginxProxyManager/react
v2.13.0 React UI
2025-11-02 22:48:16 +10:00
Jamie Curnow 52e528f217 Remove incomplete languages and cleanup 2025-11-02 21:28:25 +10:00
Jamie Curnow 4709f9826c Permissions polish for restricted users 2025-10-31 12:50:54 +10:00
Jamie Curnow 74a8c5d806 Fix app crash when do unautorized things 2025-10-30 15:03:01 +10:00
Jamie Curnow 82a1a86c3a Log in as user support 2025-10-30 14:45:22 +10:00
Jamie Curnow 95957a192c Re-add dns_provider_credentials to swagger schema 2025-10-30 12:24:17 +10:00
Jamie Curnow 906ce8ced2 Swagger/openapi schema mega fixes and Cypress validation/enforcement 2025-10-30 11:50:51 +10:00
Tim Burr e0985bee43 Merge remote-tracking branch 'base/react' into develop 2025-10-29 13:15:58 +01:00
Tim Burr 51dd6e6a1b Sets postgres version to 17 2025-10-29 10:59:01 +01:00
Jamie Curnow 89abb9d559 Fix bugs from feedback 2025-10-29 08:48:29 +10:00
Jamie Curnow 5d6916dcf0 Tidy up
- Add help docs for most sections
- Add translations documentation
- Fix up todos
- Remove german translation
2025-10-28 15:41:11 +10:00
Jamie Curnow 0f718570d6 Use status components for true/false things 2025-10-28 14:18:52 +10:00
Jamie Curnow fac5f2cbc5 Cert column provider tweaks 2025-10-28 11:51:27 +10:00
Jamie Curnow 3b9beaeae5 Various tweaks and backend improvements 2025-10-28 11:38:26 +10:00
Jamie Curnow 7331cb3675 Audit log tweaks for certificates 2025-10-28 10:38:05 +10:00
Jamie Curnow 678593111e Settings polish 2025-10-28 08:53:01 +10:00
Tim Burr a2ea63a539 Adds Hetzner Cloud 2025-10-27 13:48:41 +01:00
Jamie Curnow c08b1be3cb Use code edit for dns provider config dialog 2025-10-27 19:42:58 +10:00
Jamie Curnow ca3c9aa39a Show cert expiry date in yellow when < 30 days 2025-10-27 19:34:25 +10:00
Jamie Curnow e4e5fb3b58 Update biome 2025-10-27 19:29:14 +10:00
Jamie Curnow 83a2c79e16 Custom certificate upload 2025-10-27 19:26:33 +10:00
Jamie Curnow 0de26f2950 Certificates react work
- renewal and download
- table columns rendering
- searching
- deleting
2025-10-27 18:08:37 +10:00
Jamie Curnow 7b5c70ed35 Fix cert renewal backend bug after refactor 2025-10-27 18:04:58 +10:00
Jamie Curnow e4d9f48870 Fix creating wrong cert type when trying dns 2025-10-27 18:04:29 +10:00
jc21andGitHub 2893ffb1e4 Merge pull request #4801 from sopex/react
QoL: Link to dashboard 2.13
2025-10-27 09:52:50 +10:00
Jamie Curnow 1a117a267c Fix to postgres 17 2025-10-27 08:13:03 +10:00
Jamie Curnow c303b69649 Update deps, the safe ones 2025-10-26 00:39:06 +10:00
Jamie Curnow bb6c9c8daf Certificates section react work 2025-10-26 00:28:39 +10:00
Jamie Curnow 5b7013b8d5 Moved certrbot plugin list to backend
frontend doesn't include when building in react version
adds swagger for existing dns-providers endpoint
2025-10-26 00:28:03 +10:00
Valentino LaucianiandGitHub bfcd057755 Update PostgreSQL volume path in setup documentation for 18+ 2025-10-24 09:30:19 +02:00
dependabot[bot]andGitHub 08bdc23131 Bump vite from 5.4.19 to 5.4.21 in /docs
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.19 to 5.4.21.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.21/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.21/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 5.4.21
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-21 07:13:05 +00:00
Konstantinos Spartalis b8e3e594fb ;) 2025-10-17 16:00:59 +03:00
Konstantinos SpartalisandGitHub 71251d2a0d :) 2025-10-17 13:51:06 +03:00
Jamie Curnow f2b5b19a83 More react
- consolidated lang items
- proxy host paths work
2025-10-16 18:59:19 +10:00
Jamie Curnow 7af01d0fc7 Use a modal manager 2025-10-14 17:49:56 +10:00
Jamie Curnow e6f7ae3fba Move from docker-compose to docker compose 2025-10-14 07:54:25 +10:00
Jamie Curnow 43599b4028 Access list modal polish 2025-10-09 22:14:54 +10:00
Jamie Curnow 227e818040 Wrap intl in span identifying translation 2025-10-02 23:06:51 +10:00
Jamie Curnow fcb08d3003 Bump version 2025-10-02 08:57:46 +10:00
Jamie Curnow d0767baafa Proxy host modal basis, other improvements 2025-10-02 08:12:37 +10:00
Jamie Curnow abdf8866e0 Auto sorting of locale files 2025-10-02 08:12:37 +10:00
Jamie Curnow e36c1b99a5 Redirection hosts ui 2025-10-02 08:12:37 +10:00
Jamie Curnow 9339626933 Streams polish 2025-10-02 08:12:37 +10:00
Jamie Curnow 100a7e3ff8 Streams modal 2025-10-02 08:12:37 +10:00
Jamie Curnow 4866988772 Fix stream creation with new ssl cert 2025-10-02 08:12:37 +10:00
Jamie Curnow 8884e3b261 TZ for dev db 2025-10-02 08:12:37 +10:00
Jamie Curnow a3d17249d0 User table polish and audit log updates 2025-10-02 08:12:37 +10:00
Jamie Curnow fc8a5e8b97 404 hosts search 2025-10-02 08:12:37 +10:00
Jamie Curnow da68fe29ac 404 hosts polish 2025-10-02 08:12:37 +10:00
Jamie Curnow 18537b9288 404 hosts add update complete, fix certbot renewals
and remove the need for email and agreement on cert requests
2025-10-02 08:12:37 +10:00
Jamie Curnow d85e515ab9 Dark UI for react-select 2025-10-02 08:12:37 +10:00
Jamie Curnow 94375bbc5f DNS Provider configuration 2025-10-02 08:12:37 +10:00
Jamie Curnow 54e036276a API lib cleanup, 404 hosts WIP 2025-10-02 08:12:36 +10:00
Jamie Curnow 058f49ceea Certificates react table basis 2025-10-02 08:12:33 +10:00
Jamie Curnow efcefe0c17 Fix custom cert writes, fix schema 2025-10-02 08:12:33 +10:00
Jamie Curnow 429046f32e Audit log table and modal 2025-10-02 08:12:33 +10:00
Jamie Curnow 8ad95c5695 Set password for users 2025-10-02 08:12:31 +10:00
Jamie Curnow 038de3e5f9 Refactor from Promises to async/await 2025-10-02 08:12:28 +10:00
Jamie Curnow 1928e554fd Fix proxy hosts routes throwing errors 2025-10-02 08:12:28 +10:00
Jamie Curnow d40e290a89 Biome update 2025-10-02 08:12:24 +10:00
Jamie Curnow fb2708d81d Fix cypress tests following user wizard changes 2025-10-02 08:12:09 +10:00
Jamie Curnow 7a6efd8ebb User Permissions Modal 2025-10-02 08:12:09 +10:00
Jamie Curnow 0b2fa826e0 Introducing the Setup Wizard for creating the first user
- no longer setup a default
- still able to do that with env vars however
2025-10-02 08:12:05 +10:00
Jamie Curnow 6ab7198e61 User table polishing, user delete modal 2025-10-02 08:11:17 +10:00
Jamie Curnow 61a92906f3 Notification toasts, nicer loading, add new user support 2025-10-02 08:11:14 +10:00
Jamie Curnow fadec9751e React 2025-10-02 08:10:42 +10:00
Jamie Curnow 330993f028 Convert backend to ESM
- About 5 years overdue
- Remove eslint, use bomejs instead
2025-10-02 08:10:18 +10:00
dependabot[bot]andGitHub c9aba0c928 Bump axios from 1.10.0 to 1.12.0 in /test
Bumps [axios](https://github.com/axios/axios) from 1.10.0 to 1.12.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.10.0...v1.12.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-13 15:18:55 +00:00
Jamie Curnow 487fa6d31b Attempt to fix frontend build for node 22
replaced node-sass with sass
2025-09-10 10:38:21 +10:00
dependabot[bot]andGitHub 4397f57a51 Bump @eslint/plugin-kit from 0.3.2 to 0.3.5 in /test
Bumps [@eslint/plugin-kit](https://github.com/eslint/rewrite/tree/HEAD/packages/plugin-kit) from 0.3.2 to 0.3.5.
- [Release notes](https://github.com/eslint/rewrite/releases)
- [Changelog](https://github.com/eslint/rewrite/blob/main/packages/plugin-kit/CHANGELOG.md)
- [Commits](https://github.com/eslint/rewrite/commits/plugin-kit-v0.3.5/packages/plugin-kit)

---
updated-dependencies:
- dependency-name: "@eslint/plugin-kit"
  dependency-version: 0.3.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-22 02:38:28 +00:00
jc21andGitHub 5b6ca1bf00 Merge pull request #4664 from JMDirksen/develop
Fix initial email with uppercase
2025-08-22 12:38:22 +10:00
jc21andGitHub 5039738aa3 Merge pull request #4696 from NginxProxyManager/dependabot/npm_and_yarn/test/tmp-0.2.4
Bump tmp from 0.2.3 to 0.2.4 in /test
2025-08-22 12:34:03 +10:00
jc21andGitHub 4451be8f1c Merge pull request #4722 from NginxProxyManager/dependabot/npm_and_yarn/frontend/cipher-base-1.0.6
Bump cipher-base from 1.0.4 to 1.0.6 in /frontend
2025-08-22 12:22:49 +10:00
jc21andGitHub bee2fd1978 Merge pull request #4723 from NginxProxyManager/dependabot/npm_and_yarn/frontend/sha.js-2.4.12
Bump sha.js from 2.4.11 to 2.4.12 in /frontend
2025-08-22 12:22:39 +10:00
dependabot[bot]andGitHub c8adbdfc15 Bump sha.js from 2.4.11 to 2.4.12 in /frontend
Bumps [sha.js](https://github.com/crypto-browserify/sha.js) from 2.4.11 to 2.4.12.
- [Changelog](https://github.com/browserify/sha.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crypto-browserify/sha.js/compare/v2.4.11...v2.4.12)

---
updated-dependencies:
- dependency-name: sha.js
  dependency-version: 2.4.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-21 15:45:37 +00:00
dependabot[bot]andGitHub aff4182ab8 Bump cipher-base from 1.0.4 to 1.0.6 in /frontend
Bumps [cipher-base](https://github.com/crypto-browserify/cipher-base) from 1.0.4 to 1.0.6.
- [Changelog](https://github.com/browserify/cipher-base/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crypto-browserify/cipher-base/compare/v1.0.4...v1.0.6)

---
updated-dependencies:
- dependency-name: cipher-base
  dependency-version: 1.0.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-21 15:13:31 +00:00
Jamie Curnow 8c9d2745e2 Fix remote execution bug where email address can contain malicious code
also convert almost all cmd execs for certificates to properly escape arguments
2025-08-20 10:57:24 +10:00
dependabot[bot]andGitHub 076d14b5e4 Bump tmp from 0.2.3 to 0.2.4 in /test
Bumps [tmp](https://github.com/raszi/node-tmp) from 0.2.3 to 0.2.4.
- [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/raszi/node-tmp/compare/v0.2.3...v0.2.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-06 17:12:10 +00:00
JMDirksen 8a6d815152 Fix initial email with upper case 2025-07-20 08:36:43 +02:00
jc21andGitHub 356eaa0691 Merge pull request #4653 from NginxProxyManager/develop
v2.12.6
2025-07-10 07:18:53 +10:00
Jamie Curnow 54d463ac36 Safer and flexible boolean env vars 2025-07-09 21:27:50 +10:00
Jamie Curnow a23dc24021 Tweak ownership output 2025-07-09 21:01:21 +10:00
Jamie Curnow 4f9df893c8 Ownership script shakeup
- Don't touch a file to determine if we need to run
- Instead, check ownership of each location and skip it if we are happy
- Keeping SKIP_CERTBOT_OWNERSHIP flag
- More vebose logging of outcomes
2025-07-09 20:30:27 +10:00
Jamie Curnow 304b38e82b Fix ownership if statement 2025-07-09 18:19:50 +10:00
jc21andGitHub 1b0929ade6 Merge branch 'master' into develop 2025-07-09 16:36:26 +10:00
Jamie Curnow ddbafb62a6 bump version 2025-07-09 16:33:50 +10:00
Jamie Curnow 9a0383bc73 Move SKIP_CERTBOT_OWNERSHIP check around the entire certbot code 2025-07-09 16:30:45 +10:00
jc21andGitHub 307cb94e84 Merge pull request #4651 from NginxProxyManager/develop
v2.12.5
2025-07-09 14:22:26 +10:00
jc21andGitHub 63ae924fbc Merge branch 'master' into develop 2025-07-09 13:16:38 +10:00
Jamie Curnow 1710a263c0 Bump version 2025-07-09 13:15:15 +10:00
Jamie Curnow 1357774f21 Add SKIP_CERTBOT_OWNERSHIP env var support to skip certbot folder ownership 2025-07-09 13:14:27 +10:00
Jamie Curnow 5f54490d86 Set SETUPTOOLS_USE_DISTUTILS for all plugin installs, seems like they all need it. 2025-07-09 12:35:20 +10:00
Jamie Curnow c97b8a339d Some auto formatting changes suggested by ide 2025-07-09 11:34:57 +10:00
Jamie Curnow ed1d90ee7f Fix powerdns dns plugin install, deps are outrageously old ;( 2025-07-09 11:34:19 +10:00
Jamie Curnow 70894e55b8 Remove cloudflare dep for certbot plugin, tested 2025-07-09 09:36:57 +10:00
Jamie Curnow 817021a43d Update s6 overlay 2025-07-08 17:32:23 +10:00
Jamie Curnow 36e3449a56 Update cloudflare dependency 2025-07-08 17:14:20 +10:00
Jamie Curnow db9f25638f Update PR comments to highlight verification requirements 2025-07-08 17:08:31 +10:00
jc21andGitHub ddd3355d95 Merge pull request #4645 from NginxProxyManager/revert-4574-develop
Revert "Update 'global/certbot-dns-plugins.json' to apply SSL certs for CloudFlare."
2025-07-08 11:19:53 +10:00
jc21andGitHub aade8b42fc Revert "Update 'global/certbot-dns-plugins.json' to apply SSL certs for CloudFlare." 2025-07-08 10:26:46 +10:00
Jamie Curnow 3735f3c11d Formating for ownership script 2025-07-08 09:44:10 +10:00
jc21andGitHub b84762b5b9 Merge pull request #4605 from NginxProxyManager/develop
v2.12.4
2025-07-01 11:12:08 +10:00
jc21andGitHub 953faeac15 Merge branch 'master' into develop 2025-07-01 07:33:33 +10:00
Jamie Curnow c58f3f3ec9 Bump version 2025-07-01 07:32:39 +10:00
jc21andGitHub 0ee4d04d5f Merge pull request #4491 from addievo/fix-certbot-startup-time
fix: optimize certbot ownership script to reduce container startup time
2025-06-30 15:31:09 +10:00
jc21andGitHub 94f6756250 Merge pull request #4557 from 1ukastesar/patch-1
fix(modal): make textarea font actually monospace
2025-06-30 15:19:05 +10:00
jc21andGitHub 27e3f73854 Merge pull request #4353 from mordyovits/patch-1
Update frontend copyright year to 2025
2025-06-30 14:57:07 +10:00
jc21andGitHub d98f4b43dc Merge pull request #4398 from cg-zhou/feature/add-ip-ranges-env-var
Added IP_RANGES_FETCH_ENABLED environment variable
2025-06-30 14:54:40 +10:00
jc21andGitHub ff3116a626 Merge pull request #4604 from NginxProxyManager/dependabot/npm_and_yarn/backend/brace-expansion-1.1.12
Bump brace-expansion from 1.1.11 to 1.1.12 in /backend
2025-06-30 14:47:58 +10:00
jc21andGitHub 7047750b04 Merge pull request #4358 from pustekuchen91/update-cpanel-certbot-plugin
use latest certbot-dns-cpanel version
2025-06-30 14:43:48 +10:00
cg-zhou 0792fc0768 Remove unnecessary Promise.resolve() calls 2025-06-30 12:31:23 +08:00
dependabot[bot]andGitHub 9758c12ca3 Bump brace-expansion from 1.1.11 to 1.1.12 in /backend
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.11 to 1.1.12.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 03:55:09 +00:00
jc21andGitHub ccd69c8867 Update certbot-dns-plugins.json 2025-06-30 13:52:07 +10:00
jc21andGitHub 23fd1fec6c Merge branch 'develop' into update-cpanel-certbot-plugin 2025-06-30 13:51:19 +10:00
jc21andGitHub 6f04543744 Merge pull request #4368 from wisewtf/patch-1
Fixed error in sqlite installation compose file
2025-06-30 13:49:40 +10:00
jc21andGitHub cbb1fe44ca Merge pull request #4381 from ZeroDeng01/ZeroDeng01-patch-1
Fixed an issue with the 500 error code on the Stream list page
2025-06-30 13:45:05 +10:00
jc21andGitHub 4c23f22d5b Merge pull request #4601 from NginxProxyManager/dependabot/npm_and_yarn/test/axios-1.10.0
Bump axios from 1.7.7 to 1.10.0 in /test
2025-06-30 13:13:02 +10:00
jc21andGitHub af5d3eccd6 Merge pull request #4602 from NginxProxyManager/dependabot/npm_and_yarn/docs/vite-5.4.19
Bump vite from 5.4.14 to 5.4.19 in /docs
2025-06-30 13:12:51 +10:00
jc21andGitHub a87283b030 Merge pull request #4603 from NginxProxyManager/dependabot/npm_and_yarn/frontend/elliptic-6.6.1
Bump elliptic from 6.6.0 to 6.6.1 in /frontend
2025-06-30 13:12:42 +10:00
Jamie Curnow 97dbbdd60f Fix incorrect swagger for streams list 2025-06-30 13:00:25 +10:00
Jamie Curnow ec81f2489a Add cypress test to list streams 2025-06-30 11:10:45 +10:00
dependabot[bot]andGitHub d0ec8e89aa Bump elliptic from 6.6.0 to 6.6.1 in /frontend
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.6.0 to 6.6.1.
- [Commits](https://github.com/indutny/elliptic/compare/v6.6.0...v6.6.1)

---
updated-dependencies:
- dependency-name: elliptic
  dependency-version: 6.6.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 00:56:57 +00:00
dependabot[bot]andGitHub 9a96fbb5f4 Bump vite from 5.4.14 to 5.4.19 in /docs
---
updated-dependencies:
- dependency-name: vite
  dependency-version: 5.4.19
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 00:56:15 +00:00
dependabot[bot]andGitHub a573450bb8 Bump axios from 1.7.7 to 1.10.0 in /test
Bumps [axios](https://github.com/axios/axios) from 1.7.7 to 1.10.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.7.7...v1.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 00:54:38 +00:00
jc21andGitHub 60a25ffbd5 Merge pull request #4560 from spions/patch-1
Added Selectel v2  DNS provider
2025-06-30 10:49:40 +10:00
jc21andGitHub 7d2369b380 Merge pull request #4576 from NginxProxyManager/dependabot/npm_and_yarn/test/brace-expansion-1.1.12
Bump brace-expansion from 1.1.11 to 1.1.12 in /test
2025-06-30 10:49:20 +10:00
jc21andGitHub 64f00e8dba Merge pull request #4577 from h33n0k/develop
Fix Incorrect Api status codes
2025-06-30 10:49:09 +10:00
jc21andGitHub c99143f548 Merge pull request #4596 from NginxProxyManager/dependabot/npm_and_yarn/frontend/pbkdf2-3.1.3
Bump pbkdf2 from 3.1.1 to 3.1.3 in /frontend
2025-06-30 10:48:57 +10:00
jc21andGitHub cc4ee6919a Merge pull request #4597 from aitor422/develop
added CDMon DNS provider
2025-06-30 08:49:03 +10:00
jc21andGitHub 8a69c65b40 Merge pull request #4551 from MinhPho/feature/update-strato-dns-plugin
Update strato dns plugin from 0.2.1 to 0.2.2
2025-06-30 08:09:39 +10:00
jc21andGitHub 95ee5ca958 Merge pull request #4553 from gustavfroding/develop
Added spaceship DNS provider
2025-06-30 08:09:17 +10:00
jc21andGitHub 40f22d30c4 Merge pull request #4574 from tom-kst/develop
Update 'global/certbot-dns-plugins.json' to apply SSL certs for CloudFlare.
2025-06-30 08:08:18 +10:00
aitor422 30dfa9e3de added CDMon DNS provider 2025-06-25 13:32:14 +02:00
dependabot[bot]andGitHub b873499feb Bump pbkdf2 from 3.1.1 to 3.1.3 in /frontend
Bumps [pbkdf2](https://github.com/crypto-browserify/pbkdf2) from 3.1.1 to 3.1.3.
- [Changelog](https://github.com/browserify/pbkdf2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crypto-browserify/pbkdf2/compare/v3.1.1...v3.1.3)

---
updated-dependencies:
- dependency-name: pbkdf2
  dependency-version: 3.1.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-24 09:20:21 +00:00
h33n0k ef69be2036 Fix Incorrect Api status codes
Update Incorrect status code based off the api schema
2025-06-12 08:58:17 +02:00
dependabot[bot]andGitHub 7580e65dd4 Bump brace-expansion from 1.1.11 to 1.1.12 in /test
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.11 to 1.1.12.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-11 21:02:35 +00:00
Tom.KSTandGitHub f11dc5d7c1 Update certbot-dns-plugins.json 2025-06-11 17:36:21 +08:00
Tom.KSTandGitHub 77061a7bd6 Update certbot-dns-plugins.json
I've tried multi times failed, and I found that show be a 'space break' ahead & after the equal mark...
So the correct script should be "dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567", instead of "dns_cloudflare_api_token=0123456789abcdef0123456789abcdef01234567"
2025-06-11 17:33:44 +08:00
OlegandGitHub b6afc19135 Added selectel v2 DNS provider 2025-05-28 18:10:21 +03:00
Lukáš TesařandGitHub 09ba400d09 fix(modal): make textarea font actually monospace
Modal `textarea` element has this class `text-monospace`, but there is actually no CSS definition that sets the monospace font for it (neither in custom SCSS files, nor in included libs). This commit fixes the issue by setting `monospace` `font-family` for the `textarea`, greatly enhancing UX of configuration editing in UI.
2025-05-26 19:40:08 +02:00
gustavfroding 0291cfc270 Added spaceship DNS provider 2025-05-23 13:18:07 +02:00
jelly_moonandGitHub 34267e0af9 Update strato dns plugin from 0.2.1 to 0.2.2 2025-05-23 04:43:52 +02:00
jc21andGitHub f327c1e825 Merge pull request #4406 from chindocaine/fix_domainoffensive_certbot
Fix DomainOffensive certbot plugin
2025-05-21 20:56:12 +10:00
jc21andGitHub 6f539979ec Merge pull request #4411 from henmohr/develop
Update cloudflare dns plugin from 2.19.4 to 4.0.*
2025-05-21 20:52:50 +10:00
jc21andGitHub 3d8079a137 Merge pull request #4426 from foxtrotcz/develop
Updates Active24 plugin to API v2
2025-05-21 20:50:55 +10:00
jc21andGitHub 6d6d83c0d0 Merge pull request #4435 from amateescu/update-gandi-plugin
Update the Gandi plugin.
2025-05-21 20:50:36 +10:00
jc21andGitHub 100a4888d0 Merge pull request #4481 from godsgood33/patch-1
Update certbot-dns-plugins.json
2025-05-21 20:50:14 +10:00
jc21andGitHub 34a46bd733 Merge pull request #4534 from chenghaopeng/develop
add Baidu as DNS provider
2025-05-21 20:48:11 +10:00
jc21andGitHub 7f8adc7e50 Merge pull request #4538 from astamminger/add_dns_ddnss_plugin
Add DDNSS to the list of supported Providers for DNS-01 Challenges
2025-05-21 20:47:29 +10:00
jc21andGitHub 98d118cb74 Merge pull request #4540 from hatharry/develop
Add First Domains DNS Provider
2025-05-21 20:47:02 +10:00
jc21andGitHub 4fb93542c3 Merge pull request #4547 from vzagorovskiy/develop
Added nic.ru DNS provider
2025-05-21 20:46:39 +10:00
vzagorovskiy 4fe305520a Added nic.ru dns provider 2025-05-19 13:18:58 +03:00
A. Stamminger 76be31cf76 Update certbot-dns-plugins.json with dns-ddns plugin
This commit extends the global plugin list with the configuration for
certbot-dns-ddnss (https://pypi.org/project/certbot-dns-ddnss/),
a new plugin providing DNS-01 challenges for ddnss.de
2025-05-12 15:54:10 +02:00
鹏鹏andGitHub 55dadb2004 Merge pull request #1 from chenghaopeng/dns-baidu
add Baidu as DNS provider
2025-05-11 12:46:27 +08:00
鹏鹏andGitHub d9cdb3dc2c add Baidu as DNS provider 2025-05-11 12:45:13 +08:00
Aditya 0cab720f23 fix: optimize certbot ownership script to reduce container startup time
Replace inefficient find/execdir implementation that was causing 3+ minute
startup delays with a more efficient approach that:

1. Uses a flag file to skip redundant operations on container restarts
2. Processes site-packages directories with bulk chown operations instead
   of individual file checks and changes
3. Maintains the same functionality while dramatically improving performance

This change should significantly reduce container startup time while ensuring
all necessary file permissions are still properly set.
2025-04-20 20:38:54 +10:00
Ryan PandGitHub f5879dff6c Update certbot-dns-plugins.json
Fix for bug #4429 add cpanel_api_token entry to credentials check. Will still need to update the documentation that the user will need to retrieve the api token from their cPanel.
2025-04-10 19:56:06 -04:00
Jamie Curnow 5e66d677f1 Adds test for dashboard endpoints 2025-03-24 14:34:45 +10:00
Andrei Mateescu 18830f81b0 Update the Gandi plugin. 2025-03-13 23:47:31 +02:00
FoxtrotCZandGitHub 341ac65587 Updates Active24 plugin to API v2 2025-03-09 19:54:11 +01:00
henmohrandGitHub 078baa255a Update certbot-dns-plugins.json 2025-03-03 16:40:38 -03:00
Michael Heilig bf9d9bd43b Fix DomainOffensive certbot plugin
In https://github.com/NginxProxyManager/nginx-proxy-manager/pull/4235 the certbot plugin for do.de (Domain Offensive) was updated to use the more
official version. One necessary line modification was missed, resulting in an error when creating a new certificate.
2025-02-28 21:00:36 +01:00
cg-zhou a394b25e61 fix eslint error 2025-02-26 19:45:49 +08:00
cg-zhou 1c47fc2ba4 feat: Add IP_RANGES_FETCH_ENABLED environment variable
This change adds a new environment variable to control whether IP ranges
are fetched during application startup. When set to 'false', the initial
fetch will be skipped, which can:

1. Speed up application startup
2. Avoid connectivity issues in environments with restricted internet access
3. Prevent startup failures when CloudFront or CloudFlare services are unreachable
2025-02-26 19:25:50 +08:00
ZeroDengandGitHub 312e2ab80c [fix]Stream List error code 500
Fix stream list page error code 500。
2025-02-21 14:56:00 +08:00
WiseandGitHub d147ccd88d Fixed error in sqlite installation compose file
If people copy and paste the sqlite installation without commenting environment docker compose will throw an error because environment will be null.
2025-02-14 14:44:54 +01:00
MarcandGitHub 03fd292c61 use latest certbot-dns-cpanel version
this allows to use token for authentication
2025-02-09 11:41:30 +01:00
jc21andGitHub 79d28f03d0 Merge pull request #4346 from Sander0542/feature/security-schemes-component
API Schema Improvements
2025-02-07 12:39:49 +10:00
Mordy OvitsandGitHub b09147eca8 Update frontend copyright year to 2025 2025-02-06 19:40:23 -05:00
jc21andGitHub c5a319cb20 Merge pull request #4347 from NginxProxyManager/develop
v2.12.3
2025-02-06 20:25:09 +10:00
Jamie Curnow c4df89df1f Fix dashboard loading loop and freezing the page 2025-02-06 13:38:47 +10:00
jc21andGitHub 34c703f8b4 Merge branch 'master' into develop 2025-02-06 08:52:55 +10:00
Jamie Curnow 0a05d8f0ad Bump version 2025-02-06 08:39:03 +10:00
jc21andGitHub 0a9141fad5 Merge pull request #4208 from badkeyy/feature/add-zone-edit-certbot-plugin
Add ZoneEdit certbot plugin
2025-02-06 08:33:11 +10:00
jc21andGitHub 42836774b7 Merge branch 'develop' into feature/add-zone-edit-certbot-plugin 2025-02-06 08:33:01 +10:00
jc21andGitHub 2a07544f58 Merge pull request #4235 from FabianK3/update-domainoffensive-certbot-plugin
Update DomainOffensive certbot plugin
2025-02-06 08:30:09 +10:00
jc21andGitHub dc9d884743 Merge pull request #4292 from icaksh/patch-1
feat: change htpasswd to openssl
2025-02-06 08:29:15 +10:00
jc21andGitHub 0d5d2b1b7c Merge pull request #4283 from badkeyy/feature/show-active-host-in-cert-list
SSL Certificates: Show if cert is in use on host
2025-02-06 07:43:12 +10:00
Sander JochemsandGitHub df48b835c4 Update order to match others 2025-02-05 22:20:21 +01:00
Sander JochemsandGitHub 8a1557154a Add certificate fields to boolFields 2025-02-05 22:15:12 +01:00
Sander JochemsandGitHub a6af5ec2c7 Remove certificate as required from proxy host 2025-02-05 18:18:50 +01:00
Sander JochemsandGitHub 14d7c35fd7 Fix whitespaces 2025-02-05 17:31:09 +01:00
Sander JochemsandGitHub cfcf78aaee Set bearer auth security component 2025-02-05 17:29:40 +01:00
jc21andGitHub 3a01b2c84f Merge pull request #4334 from nwagenmakers/mijn-host-patch
Update certbot-dns-plugins.json (mijn-host)
2025-02-05 20:36:06 +10:00
jc21andGitHub e1c84a5c10 Merge pull request #4338 from Sander0542/fix/token-expires-type
Fix type for token.expires
2025-02-05 20:35:33 +10:00
jc21andGitHub c56c95a59a Merge pull request #4344 from NginxProxyManager/stream-ssl
SSL for Streams - 2025
2025-02-05 18:22:51 +10:00
Jamie Curnow 6a60627833 Cypress test for Streams
and updated cypress + packages
2025-02-05 16:02:17 +10:00
Jamie Curnow b4793d3c16 Adds testssl.sh and mkcert to cypress stack 2025-02-05 08:10:11 +10:00
Jamie Curnow 68a7803513 Fix api schema after merging latest changes 2025-02-04 17:55:28 +10:00
jbowringandJamie Curnow 2657af97cf Fix stream update not persisting 2025-02-04 17:14:07 +10:00
jbowringandJamie Curnow 4452f014b9 Fix whitespace in nginx stream config 2025-02-04 17:14:07 +10:00
jbowringandJamie Curnow cd80cc8e4d Add certificate to streams database model 2025-02-04 17:14:04 +10:00
jbowringandJamie Curnow ee4250d770 Add SSL column to streams table UI 2025-02-04 17:12:05 +10:00
jbowringandJamie Curnow 3dbc70faa6 Add SSL tab to stream UI 2025-02-04 17:12:04 +10:00
jbowringandJamie Curnow 3091c21cae Add SSL certificate to TCP streams if certificate in database 2025-02-04 17:12:04 +10:00
Sander JochemsandGitHub 57cd2a1919 Fix type for token.expires 2025-02-03 21:47:41 +01:00
nwagenmakersandGitHub ad5936c530 Update certbot-dns-plugins.json (mijn-host)
Updated credentials hint/text in mijn-host plugin entry
2025-02-01 13:10:53 +01:00
jc21andGitHub 498109addb Merge pull request #4310 from NginxProxyManager/dependabot/npm_and_yarn/docs/vite-5.4.14
Bump vite from 5.4.8 to 5.4.14 in /docs
2025-01-28 18:08:46 +10:00
jc21andGitHub 3f3aacd7ec Merge pull request #4274 from Dim145/develop
[Postgres] fix error in access_list get
2025-01-28 14:03:07 +10:00
dependabot[bot]andGitHub bb4ecf812d Bump vite from 5.4.8 to 5.4.14 in /docs
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.8 to 5.4.14.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.14/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.14/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-22 07:09:04 +00:00
Julian GassnerandGitHub c05f9695d0 Merge branch 'develop' into feature/add-zone-edit-certbot-plugin 2025-01-15 15:37:53 +01:00
Julian Gassner 6343b398f0 Add --no-deps 2025-01-15 14:36:38 +00:00
icaksh 59362b7477 feat: change htpasswd to openssl 2025-01-12 19:16:38 +07:00
Julian Gassner aedaaa18e0 Fix whitespace 2025-01-10 05:20:28 +01:00
Julian Gassner 080bd0b749 Added status of certificates to the certificate list and show on which domain names the certificates are in use 2025-01-10 05:15:22 +01:00
Jamie Curnow 9687e9e450 Use previous version of powerdns image, newer version is broken 2025-01-07 10:30:08 +10:00
Jamie Curnow 5a234bb88c Fix incorrect test folder in ci results 2025-01-07 08:13:04 +10:00
jc21andGitHub 4de4b65036 Merge pull request #4252 from GergelyGombai/develop
Add Gcore DNS Provider
2025-01-07 07:54:44 +10:00
dim145 f1c97c7c36 fix: add missing group_by clause for access_list get 2025-01-03 00:39:29 +01:00
ComradeBlin 73110d5e1e Update Gcore apikey format
I managed to mis-write the format in my previous commit
2024-12-22 01:44:52 +01:00
ComradeBlin 356b98bf7e Add Gcore DNS Provider 2024-12-22 01:02:47 +01:00
FabianK3 5d087f1256 Update DomainOffensive certbot plugin 2024-12-15 11:35:58 +01:00
Julian Gassner 1e322804ce Add ZoneEdit certbot plugin 2024-12-04 16:47:36 +01:00
hatharryandGitHub 2cee211fb0 add First Domains plugin 2024-11-13 16:31:59 +13:00
908 changed files with 48739 additions and 29246 deletions
+31
View File
@@ -0,0 +1,31 @@
<!-- WARNING: Any PR without a description will be closed. The title is not enough! -->
<!-- ANOTHER WARNING: Don't go creating a duplicate PR! Check that someone hasn't already created something that tackles your fix and if so, help them first -->
## Why
<!-- Provide a brief description of WHY you are making your changes -->
<!-- Consider if you are changing the API, then go in to detail why and/or if
you change will break API for existing users -->
## Type of Change
<!-- Mark the relevant options with an "x" -->
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation update
- [ ] Code refactoring
- [ ] API changes
- [ ] Performance improvement
- [ ] Test addition or update
## AI Usage
<!-- Mark the relevant options with an "x" -->
- [ ] AI was used to write this
- [ ] AI was used to review this
+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@v10
with:
stale-issue-label: 'stale'
stale-pr-label: 'stale'
+3
View File
@@ -1,5 +1,6 @@
.DS_Store
.idea
.qodo
._*
.vscode
certbot-help.txt
@@ -7,3 +8,5 @@ test/node_modules
*/node_modules
docker/dev/dnsrouter-config.json.tmp
docker/dev/resolv.conf
.claude
+1 -1
View File
@@ -1 +1 @@
2.12.2
2.15.1
Vendored
-280
View File
@@ -1,280 +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: 'testing/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: 'testing/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 $(docker-compose 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: 'testing/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)
as `nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}`
**Note:** ensure you backup your NPM instance before testing this image! Especially if there are database changes
**Note:** this is a different docker image namespace than the official image
""", 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 -28
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.12.2-green.svg?style=for-the-badge">
<img src="https://img.shields.io/badge/version-2.15.1-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
@@ -74,11 +77,7 @@ This is the bare minimum configuration required. See the [documentation](https:/
3. Bring up your stack by running
```bash
docker-compose up -d
# If using docker-compose-plugin
docker compose up -d
```
4. Log in to the Admin UI
@@ -88,14 +87,6 @@ Sometimes this can take a little bit because of the entropy of keys.
[http://127.0.0.1:81](http://127.0.0.1:81)
Default Admin User:
```
Email: admin@example.com
Password: changeme
```
Immediately after logging in with this default user you will be asked to modify your details and change your password.
## Contributing
+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.15.x (latest) | :white_check_mark: |
| < 2.15.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.
-73
View File
@@ -1,73 +0,0 @@
{
"env": {
"node": true,
"es6": true
},
"extends": [
"eslint:recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"align-assignments"
],
"rules": {
"arrow-parens": [
"error",
"always"
],
"indent": [
"error",
"tab"
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"key-spacing": [
"error",
{
"align": "value"
}
],
"comma-spacing": [
"error",
{
"before": false,
"after": true
}
],
"func-call-spacing": [
"error",
"never"
],
"keyword-spacing": [
"error",
{
"before": true
}
],
"no-irregular-whitespace": "error",
"no-unused-expressions": 0,
"align-assignments/align-assignments": [
2,
{
"requiresOnly": false
}
]
}
}
-11
View File
@@ -1,11 +0,0 @@
{
"printWidth": 320,
"tabWidth": 4,
"useTabs": true,
"semi": true,
"singleQuote": true,
"bracketSpacing": true,
"jsxBracketSameLine": true,
"trailingComma": "all",
"proseWrap": "always"
}
+45 -43
View File
@@ -1,9 +1,12 @@
const express = require('express');
const bodyParser = require('body-parser');
const fileUpload = require('express-fileupload');
const compression = require('compression');
const config = require('./lib/config');
const log = require('./logger').express;
import bodyParser from "body-parser";
import compression from "compression";
import express from "express";
import fileUpload from "express-fileupload";
import { isDebugMode } from "./lib/config.js";
import cors from "./lib/express/cors.js";
import jwt from "./lib/express/jwt.js";
import { debug, express as logger } from "./logger.js";
import mainRoutes from "./routes/main.js";
/**
* App
@@ -11,7 +14,7 @@ const log = require('./logger').express;
const app = express();
app.use(fileUpload());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.urlencoded({ extended: true }));
// Gzip
app.use(compression());
@@ -20,71 +23,70 @@ app.use(compression());
* General Logging, BEFORE routes
*/
app.disable('x-powered-by');
app.enable('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
app.enable('strict routing');
app.disable("x-powered-by");
app.enable("trust proxy", ["loopback", "linklocal", "uniquelocal"]);
app.enable("strict routing");
// pretty print JSON when not live
if (config.debug()) {
app.set('json spaces', 2);
if (isDebugMode()) {
app.set("json spaces", 2);
}
// CORS for everything
app.use(require('./lib/express/cors'));
app.use(cors);
// General security/cache related headers + server header
app.use(function (req, res, next) {
let x_frame_options = 'DENY';
app.use((_, res, next) => {
let x_frame_options = "DENY";
if (typeof process.env.X_FRAME_OPTIONS !== 'undefined' && process.env.X_FRAME_OPTIONS) {
if (typeof process.env.X_FRAME_OPTIONS !== "undefined" && process.env.X_FRAME_OPTIONS) {
x_frame_options = process.env.X_FRAME_OPTIONS;
}
res.set({
'X-XSS-Protection': '1; mode=block',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': x_frame_options,
'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
Pragma: 'no-cache',
Expires: 0
"X-XSS-Protection": "1; mode=block",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": x_frame_options,
"Cache-Control": "no-cache, no-store, max-age=0, must-revalidate",
Pragma: "no-cache",
Expires: 0,
});
next();
});
app.use(require('./lib/express/jwt')());
app.use('/', require('./routes/main'));
app.use(jwt());
app.use("/", mainRoutes);
// production error handler
// no stacktraces leaked to user
// eslint-disable-next-line
app.use(function (err, req, res, next) {
let payload = {
app.use((err, req, res, _) => {
const payload = {
error: {
code: err.status,
message: err.public ? err.message : 'Internal Error'
}
code: err.status || 500,
message: err.public ? err.message : "Internal Error",
},
};
if (config.debug() || (req.baseUrl + req.path).includes('nginx/certificates')) {
if (typeof err.message_i18n !== "undefined") {
payload.error.message_i18n = err.message_i18n;
}
if (isDebugMode() || (req.baseUrl + req.path).includes("nginx/certificates")) {
payload.debug = {
stack: typeof err.stack !== 'undefined' && err.stack ? err.stack.split('\n') : null,
previous: err.previous
stack: typeof err.stack !== "undefined" && err.stack ? err.stack.split("\n") : null,
previous: err.previous,
};
}
// Not every error is worth logging - but this is good for now until it gets annoying.
if (typeof err.stack !== 'undefined' && err.stack) {
if (config.debug()) {
log.debug(err.stack);
} else if (typeof err.public == 'undefined' || !err.public) {
log.warn(err.message);
if (typeof err.stack !== "undefined" && err.stack) {
debug(logger, err.stack);
if (typeof err.public === "undefined" || !err.public) {
logger.warn(err.message);
}
}
res
.status(err.status || 500)
.send(payload);
res.status(err.status || 500).send(payload);
});
module.exports = app;
export default app;
+91
View File
@@ -0,0 +1,91 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.15/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"
}
}
}
}
@@ -1,4 +1,4 @@
# certbot-dns-plugins
# Certbot dns-plugins
This file contains info about available Certbot DNS plugins.
This only works for plugins which use the standard argument structure, so:
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"
},
+29 -14
View File
@@ -1,27 +1,42 @@
const config = require('./lib/config');
import knex from "knex";
import {configGet, configHas} from "./lib/config.js";
if (!config.has('database')) {
throw new Error('Database config does not exist! Please read the instructions: https://nginxproxymanager.com/setup/');
}
let instance = null;
function generateDbConfig() {
const cfg = config.get('database');
if (cfg.engine === 'knex-native') {
const generateDbConfig = () => {
if (!configHas("database")) {
throw new Error(
"Database config does not exist! Please read the instructions: https://nginxproxymanager.com/setup/",
);
}
const cfg = configGet("database");
if (cfg.engine === "knex-native") {
return cfg.knex;
}
return {
client: cfg.engine,
client: cfg.engine,
connection: {
host: cfg.host,
user: cfg.user,
host: cfg.host,
user: cfg.user,
password: cfg.password,
database: cfg.name,
port: cfg.port
port: cfg.port,
...(cfg.ssl ? { ssl: cfg.ssl } : {})
},
migrations: {
tableName: 'migrations'
}
tableName: "migrations",
},
};
};
const getInstance = () => {
if (!instance) {
instance = knex(generateDbConfig());
}
return instance;
}
module.exports = require('knex')(generateDbConfig());
export default getInstance;
+27 -18
View File
@@ -1,37 +1,47 @@
#!/usr/bin/env node
const schema = require('./schema');
const logger = require('./logger').global;
import app from "./app.js";
import internalCertificate from "./internal/certificate.js";
import internalIpRanges from "./internal/ip_ranges.js";
import { global as logger } from "./logger.js";
import { migrateUp } from "./migrate.js";
import { getCompiledSchema } from "./schema/index.js";
import setup from "./setup.js";
async function appStart () {
const migrate = require('./migrate');
const setup = require('./setup');
const app = require('./app');
const internalCertificate = require('./internal/certificate');
const internalIpRanges = require('./internal/ip_ranges');
const IP_RANGES_FETCH_ENABLED = process.env.IP_RANGES_FETCH_ENABLED !== "false";
return migrate.latest()
async function appStart() {
return migrateUp()
.then(setup)
.then(schema.getCompiledSchema)
.then(internalIpRanges.fetch)
.then(getCompiledSchema)
.then(() => {
if (!IP_RANGES_FETCH_ENABLED) {
logger.info("IP Ranges fetch is disabled by environment variable");
return;
}
logger.info("IP Ranges fetch is enabled");
return internalIpRanges.fetch().catch((err) => {
logger.error("IP Ranges fetch failed, continuing anyway:", err.message);
});
})
.then(() => {
internalCertificate.initTimer();
internalIpRanges.initTimer();
const server = app.listen(3000, () => {
logger.info('Backend PID ' + process.pid + ' listening on port 3000 ...');
logger.info(`Backend PID ${process.pid} listening on port 3000 ...`);
process.on('SIGTERM', () => {
logger.info('PID ' + process.pid + ' received SIGTERM');
process.on("SIGTERM", () => {
logger.info(`PID ${process.pid} received SIGTERM`);
server.close(() => {
logger.info('Stopping.');
logger.info("Stopping.");
process.exit(0);
});
});
});
})
.catch((err) => {
logger.error(err.message, err);
logger.error(`Startup Error: ${err.message}`, err);
setTimeout(appStart, 1000);
});
}
@@ -39,7 +49,6 @@ async function appStart () {
try {
appStart();
} catch (err) {
logger.error(err.message, err);
logger.fatal(err);
process.exit(1);
}
+305
View File
@@ -0,0 +1,305 @@
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;
+359 -416
View File
@@ -1,103 +1,91 @@
const _ = require('lodash');
const fs = require('fs');
const batchflow = require('batchflow');
const logger = require('../logger').access;
const error = require('../lib/error');
const utils = require('../lib/utils');
const accessListModel = require('../models/access_list');
const accessListAuthModel = require('../models/access_list_auth');
const accessListClientModel = require('../models/access_list_client');
const proxyHostModel = require('../models/proxy_host');
const internalAuditLog = require('./audit-log');
const internalNginx = require('./nginx');
import fs from "node:fs";
import batchflow from "batchflow";
import _ from "lodash";
import errs from "../lib/error.js";
import utils from "../lib/utils.js";
import { access as logger } from "../logger.js";
import accessListModel from "../models/access_list.js";
import accessListAuthModel from "../models/access_list_auth.js";
import accessListClientModel from "../models/access_list_client.js";
import proxyHostModel from "../models/proxy_host.js";
import internalAuditLog from "./audit-log.js";
import internalNginx from "./nginx.js";
function omissions () {
return ['is_deleted'];
}
const omissions = () => {
return ["is_deleted"];
};
const internalAccessList = {
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: (access, data) => {
return access.can('access_lists:create', data)
.then((/*access_data*/) => {
return accessListModel
.query()
.insertAndFetch({
name: data.name,
satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
owner_user_id: access.token.getUserId(1)
})
.then(utils.omitRow(omissions()));
create: async (access, data) => {
await access.can("access_lists:create", data);
const row = await accessListModel
.query()
.insertAndFetch({
name: data.name,
satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
owner_user_id: access.token.getUserId(1),
})
.then((row) => {
data.id = row.id;
.then(utils.omitRow(omissions()));
let promises = [];
data.id = row.id;
// Now add the items
data.items.map((item) => {
promises.push(accessListAuthModel
.query()
.insert({
access_list_id: row.id,
username: item.username,
password: item.password
})
);
});
const promises = [];
// Items
data.items.map((item) => {
promises.push(
accessListAuthModel.query().insert({
access_list_id: row.id,
username: item.username,
password: item.password,
}),
);
return true;
});
// Now add the clients
if (typeof data.clients !== 'undefined' && data.clients) {
data.clients.map((client) => {
promises.push(accessListClientModel
.query()
.insert({
access_list_id: row.id,
address: client.address,
directive: client.directive
})
);
});
}
await Promise.all(promises);
return Promise.all(promises);
})
.then(() => {
// re-fetch with expansions
return internalAccessList.get(access, {
id: data.id,
expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.[clients,items]']
}, true /* <- skip masking */);
})
.then((row) => {
// Audit log
data.meta = _.assign({}, data.meta || {}, row.meta);
return internalAccessList.build(row)
.then(() => {
if (parseInt(row.proxy_host_count, 10)) {
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
}
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'access-list',
object_id: row.id,
meta: internalAccessList.maskItems(data)
});
})
.then(() => {
return internalAccessList.maskItems(row);
});
// 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,
{
id: data.id,
expand: ["owner", "items", "clients", "proxy_hosts.access_list.[clients,items]"],
},
true // skip masking
);
// Audit log
data.meta = _.assign({}, data.meta || {}, freshRow.meta);
await internalAccessList.build(freshRow);
if (Number.parseInt(freshRow.proxy_host_count, 10)) {
await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts);
}
// Add to audit log
await internalAuditLog.add(access, {
action: "created",
object_type: "access-list",
object_id: freshRow.id,
meta: internalAccessList.maskItems(data),
});
return internalAccessList.maskItems(freshRow);
},
/**
@@ -108,129 +96,99 @@ const internalAccessList = {
* @param {String} [data.items]
* @return {Promise}
*/
update: (access, data) => {
return access.can('access_lists:update', data.id)
.then((/*access_data*/) => {
return internalAccessList.get(access, {id: data.id});
})
.then((row) => {
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new error.InternalValidationError('Access List could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
}
})
.then(() => {
// patch name if specified
if (typeof data.name !== 'undefined' && data.name) {
return accessListModel
.query()
.where({id: data.id})
.patch({
name: data.name,
satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
});
}
})
.then(() => {
// Check for items and add/update/remove them
if (typeof data.items !== 'undefined' && data.items) {
let promises = [];
let items_to_keep = [];
update: async (access, data) => {
await access.can("access_lists:update", data.id);
const row = await internalAccessList.get(access, { id: data.id });
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`Access List could not be updated, IDs do not match: ${row.id} !== ${data.id}`,
);
}
data.items.map(function (item) {
if (item.password) {
promises.push(accessListAuthModel
.query()
.insert({
access_list_id: data.id,
username: item.username,
password: item.password
})
);
} else {
// This was supplied with an empty password, which means keep it but don't change the password
items_to_keep.push(item.username);
}
});
let query = accessListAuthModel
.query()
.delete()
.where('access_list_id', data.id);
if (items_to_keep.length) {
query.andWhere('username', 'NOT IN', items_to_keep);
}
return query
.then(() => {
// Add new items
if (promises.length) {
return Promise.all(promises);
}
});
}
})
.then(() => {
// Check for clients and add/update/remove them
if (typeof data.clients !== 'undefined' && data.clients) {
let promises = [];
data.clients.map(function (client) {
if (client.address) {
promises.push(accessListClientModel
.query()
.insert({
access_list_id: data.id,
address: client.address,
directive: client.directive
})
);
}
});
let query = accessListClientModel
.query()
.delete()
.where('access_list_id', data.id);
return query
.then(() => {
// Add new items
if (promises.length) {
return Promise.all(promises);
}
});
}
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'access-list',
object_id: data.id,
meta: internalAccessList.maskItems(data)
});
})
.then(() => {
// re-fetch with expansions
return internalAccessList.get(access, {
id: data.id,
expand: ['owner', 'items', 'clients', 'proxy_hosts.[certificate,access_list.[clients,items]]']
}, true /* <- skip masking */);
})
.then((row) => {
return internalAccessList.build(row)
.then(() => {
if (parseInt(row.proxy_host_count, 10)) {
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
}
}).then(internalNginx.reload)
.then(() => {
return internalAccessList.maskItems(row);
});
// patch name if specified
if (typeof data.name !== "undefined" && data.name) {
await accessListModel.query().where({ id: data.id }).patch({
name: data.name,
satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
});
}
// Check for items and add/update/remove them
if (typeof data.items !== "undefined" && data.items) {
const promises = [];
const itemsToKeep = [];
data.items.map((item) => {
if (item.password) {
promises.push(
accessListAuthModel.query().insert({
access_list_id: data.id,
username: item.username,
password: item.password,
}),
);
} else {
// This was supplied with an empty password, which means keep it but don't change the password
itemsToKeep.push(item.username);
}
return true;
});
const query = accessListAuthModel.query().delete().where("access_list_id", data.id);
if (itemsToKeep.length) {
query.andWhere("username", "NOT IN", itemsToKeep);
}
await query;
// Add new items
if (promises.length) {
await Promise.all(promises);
}
}
// Check for clients and add/update/remove them
if (typeof data.clients !== "undefined" && data.clients) {
const query = accessListClientModel.query().delete().where("access_list_id", data.id);
await query;
for (const client of data.clients) {
if (client.address) {
await accessListClientModel.query().insert({
access_list_id: data.id,
address: client.address,
directive: client.directive,
});
}
}
}
// Add to audit log
await internalAuditLog.add(access, {
action: "updated",
object_type: "access-list",
object_id: data.id,
meta: internalAccessList.maskItems(data),
});
// re-fetch with expansions
const freshRow = await internalAccessList.get(
access,
{
id: data.id,
expand: ["owner", "items", "clients", "proxy_hosts.[certificate,access_list.[clients,items]]"],
},
true // skip masking
);
await internalAccessList.build(freshRow)
if (Number.parseInt(freshRow.proxy_host_count, 10)) {
await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts);
}
await internalNginx.reload();
return internalAccessList.maskItems(freshRow);
},
/**
@@ -239,51 +197,50 @@ const internalAccessList = {
* @param {Integer} data.id
* @param {Array} [data.expand]
* @param {Array} [data.omit]
* @param {Boolean} [skip_masking]
* @param {Boolean} [skipMasking]
* @return {Promise}
*/
get: (access, data, skip_masking) => {
if (typeof data === 'undefined') {
data = {};
get: async (access, data, skipMasking) => {
const thisData = data || {};
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,
);
})
.where("access_list.is_deleted", 0)
.andWhere("access_list.id", thisData.id)
.groupBy("access_list.id")
.allowGraph("[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]")
.first();
if (accessData.permission_visibility !== "all") {
query.andWhere("access_list.owner_user_id", access.token.getUserId(1));
}
return access.can('access_lists:get', data.id)
.then((access_data) => {
let 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);
})
.where('access_list.is_deleted', 0)
.andWhere('access_list.id', data.id)
.allowGraph('[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]')
.first();
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
}
if (access_data.permission_visibility !== 'all') {
query.andWhere('access_list.owner_user_id', access.token.getUserId(1));
}
let row = await query.then(utils.omitRow(omissions()));
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched('[' + data.expand.join(', ') + ']');
}
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
}
if (!skip_masking && typeof row.items !== 'undefined' && row.items) {
row = internalAccessList.maskItems(row);
}
// Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) {
row = _.omit(row, data.omit);
}
return row;
});
if (!row?.id) {
throw new errs.ItemNotFoundError(thisData.id);
}
if (!skipMasking && typeof row.items !== "undefined" && row.items) {
row = internalAccessList.maskItems(row);
}
// Custom omissions
if (typeof data.omit !== "undefined" && data.omit !== null) {
row = _.omit(row, data.omit);
}
return row;
},
/**
@@ -293,73 +250,64 @@ const internalAccessList = {
* @param {String} [data.reason]
* @returns {Promise}
*/
delete: (access, data) => {
return access.can('access_lists:delete', data.id)
.then(() => {
return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items', 'clients']});
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
}
delete: async (access, data) => {
await access.can("access_lists:delete", data.id);
const row = await internalAccessList.get(access, {
id: data.id,
expand: ["proxy_hosts", "items", "clients"],
});
// 1. update row to be deleted
// 2. update any proxy hosts that were using it (ignoring permissions)
// 3. reconfigure those hosts
// 4. audit log
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
// 1. update row to be deleted
return accessListModel
.query()
.where('id', row.id)
.patch({
is_deleted: 1
})
.then(() => {
// 2. update any proxy hosts that were using it (ignoring permissions)
if (row.proxy_hosts) {
return proxyHostModel
.query()
.where('access_list_id', '=', row.id)
.patch({access_list_id: 0})
.then(() => {
// 3. reconfigure those hosts, then reload nginx
// 1. update row to be deleted
// 2. update any proxy hosts that were using it (ignoring permissions)
// 3. reconfigure those hosts
// 4. audit log
// set the access_list_id to zero for these items
row.proxy_hosts.map(function (val, idx) {
row.proxy_hosts[idx].access_list_id = 0;
});
// 1. update row to be deleted
await accessListModel
.query()
.where("id", row.id)
.patch({
is_deleted: 1,
});
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
})
.then(() => {
return internalNginx.reload();
});
}
})
.then(() => {
// delete the htpasswd file
let htpasswd_file = internalAccessList.getFilename(row);
// 2. update any proxy hosts that were using it (ignoring permissions)
if (row.proxy_hosts) {
await proxyHostModel
.query()
.where("access_list_id", "=", row.id)
.patch({ access_list_id: 0 });
try {
fs.unlinkSync(htpasswd_file);
} catch (err) {
// do nothing
}
})
.then(() => {
// 4. audit log
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'access-list',
object_id: row.id,
meta: _.omit(internalAccessList.maskItems(row), ['is_deleted', 'proxy_hosts'])
});
});
})
.then(() => {
// 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);
}
await internalNginx.reload();
// delete the htpasswd file
try {
fs.unlinkSync(internalAccessList.getFilename(row));
} catch (_err) {
// do nothing
}
// 4. audit log
await internalAuditLog.add(access, {
action: "deleted",
object_type: "access-list",
object_id: row.id,
meta: _.omit(internalAccessList.maskItems(row), ["is_deleted", "proxy_hosts"]),
});
return true;
},
/**
@@ -367,75 +315,73 @@ const internalAccessList = {
*
* @param {Access} access
* @param {Array} [expand]
* @param {String} [search_query]
* @param {String} [searchQuery]
* @returns {Promise}
*/
getAll: (access, expand, search_query) => {
return access.can('access_lists:list')
.then((access_data) => {
let 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);
})
.where('access_list.is_deleted', 0)
.groupBy('access_list.id')
.allowGraph('[owner,items,clients]')
.orderBy('access_list.name', 'ASC');
getAll: async (access, expand, searchQuery) => {
const accessData = await access.can("access_lists:list");
if (access_data.permission_visibility !== 'all') {
query.andWhere('access_list.owner_user_id', access.token.getUserId(1));
}
// Query is used for searching
if (typeof search_query === 'string') {
query.where(function () {
this.where('name', 'like', '%' + search_query + '%');
});
}
if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched('[' + expand.join(', ') + ']');
}
return query.then(utils.omitRows(omissions()));
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,
);
})
.then((rows) => {
if (rows) {
rows.map(function (row, idx) {
if (typeof row.items !== 'undefined' && row.items) {
rows[idx] = internalAccessList.maskItems(row);
}
});
}
.where("access_list.is_deleted", 0)
.groupBy("access_list.id")
.allowGraph("[owner,items,clients]")
.orderBy("access_list.name", "ASC");
return rows;
if (accessData.permission_visibility !== "all") {
query.andWhere("access_list.owner_user_id", access.token.getUserId(1));
}
// Query is used for searching
if (typeof searchQuery === "string") {
query.where(function () {
this.where("name", "like", `%${searchQuery}%`);
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
const rows = await query.then(utils.omitRows(omissions()));
if (rows) {
rows.map((row, idx) => {
if (typeof row.items !== "undefined" && row.items) {
rows[idx] = internalAccessList.maskItems(row);
}
return true;
});
}
return rows;
},
/**
* Report use
* Count is used in reports
*
* @param {Integer} user_id
* @param {Integer} userId
* @param {String} visibility
* @returns {Promise}
*/
getCount: (user_id, visibility) => {
let query = accessListModel
getCount: async (userId, visibility) => {
const query = accessListModel
.query()
.count('id as count')
.where('is_deleted', 0);
.count("id as count")
.where("is_deleted", 0);
if (visibility !== 'all') {
query.andWhere('owner_user_id', user_id);
if (visibility !== "all") {
query.andWhere("owner_user_id", userId);
}
return query.first()
.then((row) => {
return parseInt(row.count, 10);
});
const row = await query.first();
return Number.parseInt(row.count, 10);
},
/**
@@ -443,21 +389,21 @@ const internalAccessList = {
* @returns {Object}
*/
maskItems: (list) => {
if (list && typeof list.items !== 'undefined') {
list.items.map(function (val, idx) {
let repeat_for = 8;
let first_char = '*';
if (list && typeof list.items !== "undefined") {
list.items.map((val, idx) => {
let repeatFor = 8;
let firstChar = "*";
if (typeof val.password !== 'undefined' && val.password) {
repeat_for = val.password.length - 1;
first_char = val.password.charAt(0);
if (typeof val.password !== "undefined" && val.password) {
repeatFor = val.password.length - 1;
firstChar = val.password.charAt(0);
}
list.items[idx].hint = first_char + ('*').repeat(repeat_for);
list.items[idx].password = '';
list.items[idx].hint = firstChar + "*".repeat(repeatFor);
list.items[idx].password = "";
return true;
});
}
return list;
},
@@ -467,7 +413,7 @@ const internalAccessList = {
* @returns {String}
*/
getFilename: (list) => {
return '/data/access/' + list.id;
return `/data/access/${list.id}`;
},
/**
@@ -477,58 +423,55 @@ const internalAccessList = {
* @param {Array} list.items
* @returns {Promise}
*/
build: (list) => {
logger.info('Building Access file #' + list.id + ' for: ' + list.name);
build: async (list) => {
logger.info(`Building Access file #${list.id} for: ${list.name}`);
return new Promise((resolve, reject) => {
let htpasswd_file = internalAccessList.getFilename(list);
const htpasswdFile = internalAccessList.getFilename(list);
// 1. remove any existing access file
try {
fs.unlinkSync(htpasswd_file);
} catch (err) {
// do nothing
}
// 1. remove any existing access file
try {
fs.unlinkSync(htpasswdFile);
} catch (_err) {
// do nothing
}
// 2. create empty access file
try {
fs.writeFileSync(htpasswd_file, '', {encoding: 'utf8'});
resolve(htpasswd_file);
} catch (err) {
reject(err);
}
})
.then((htpasswd_file) => {
// 3. generate password for each user
if (list.items.length) {
return new Promise((resolve, reject) => {
batchflow(list.items).sequential()
.each((i, item, next) => {
if (typeof item.password !== 'undefined' && item.password.length) {
logger.info('Adding: ' + item.username);
// 2. create empty access file
fs.writeFileSync(htpasswdFile, '', {encoding: 'utf8'});
utils.execFile('/usr/bin/htpasswd', ['-b', htpasswd_file, item.username, item.password])
.then((/*result*/) => {
next();
})
.catch((err) => {
logger.error(err);
next(err);
});
}
})
.error((err) => {
logger.error(err);
reject(err);
})
.end((results) => {
logger.success('Built Access file #' + list.id + ' for: ' + list.name);
resolve(results);
});
// 3. generate password for each user
if (list.items.length) {
await new Promise((resolve, reject) => {
batchflow(list.items).sequential()
.each((_i, item, next) => {
if (item.password?.length) {
logger.info(`Adding: ${item.username}`);
utils.execFile('openssl', ['passwd', '-apr1', item.password])
.then((res) => {
try {
fs.appendFileSync(htpasswdFile, `${item.username}:${res}\n`, {encoding: 'utf8'});
} catch (err) {
reject(err);
}
next();
})
.catch((err) => {
logger.error(err);
next(err);
});
}
})
.error((err) => {
logger.error(err);
reject(err);
})
.end((results) => {
logger.success(`Built Access file #${list.id} for: ${list.name}`);
resolve(results);
});
}
});
}
}
};
}
module.exports = internalAccessList;
export default internalAccessList;
+69 -46
View File
@@ -1,6 +1,6 @@
const error = require('../lib/error');
const auditLogModel = require('../models/audit-log');
const {castJsonIfNeed} = require('../lib/helpers');
import errs from "../lib/error.js";
import { castJsonIfNeed } from "../lib/helpers.js";
import auditLogModel from "../models/audit-log.js";
const internalAuditLog = {
@@ -9,32 +9,60 @@ const internalAuditLog = {
*
* @param {Access} access
* @param {Array} [expand]
* @param {String} [search_query]
* @param {String} [searchQuery]
* @returns {Promise}
*/
getAll: (access, expand, search_query) => {
return access.can('auditlog:list')
.then(() => {
let query = auditLogModel
.query()
.orderBy('created_on', 'DESC')
.orderBy('id', 'DESC')
.limit(100)
.allowGraph('[user]');
getAll: async (access, expand, searchQuery) => {
await access.can("auditlog:list");
// Query is used for searching
if (typeof search_query === 'string' && search_query.length > 0) {
query.where(function () {
this.where(castJsonIfNeed('meta'), 'like', '%' + search_query + '%');
});
}
const query = auditLogModel
.query()
.orderBy("created_on", "DESC")
.orderBy("id", "DESC")
.limit(100)
.allowGraph("[user]");
if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched('[' + expand.join(', ') + ']');
}
return query;
// Query is used for searching
if (typeof searchQuery === "string" && searchQuery.length > 0) {
query.where(function () {
this.where(castJsonIfNeed("meta"), "like", `%${searchQuery}`);
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
return await query;
},
/**
* @param {Access} access
* @param {Object} [data]
* @param {Integer} [data.id] Defaults to the token user
* @param {Array} [data.expand]
* @return {Promise}
*/
get: async (access, data) => {
await access.can("auditlog:list");
const query = auditLogModel
.query()
.andWhere("id", data.id)
.allowGraph("[user]")
.first();
if (typeof data.expand !== "undefined" && data.expand !== null) {
query.withGraphFetched(`[${data.expand.join(", ")}]`);
}
const row = await query;
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
return row;
},
/**
@@ -51,29 +79,24 @@ const internalAuditLog = {
* @param {Object} [data.meta]
* @returns {Promise}
*/
add: (access, data) => {
return new Promise((resolve, reject) => {
// Default the user id
if (typeof data.user_id === 'undefined' || !data.user_id) {
data.user_id = access.token.getUserId(1);
}
add: async (access, data) => {
if (typeof data.user_id === "undefined" || !data.user_id) {
data.user_id = access.token.getUserId(1);
}
if (typeof data.action === 'undefined' || !data.action) {
reject(new error.InternalValidationError('Audit log entry must contain an Action'));
} else {
// Make sure at least 1 of the IDs are set and action
resolve(auditLogModel
.query()
.insert({
user_id: data.user_id,
action: data.action,
object_type: data.object_type || '',
object_id: data.object_id || 0,
meta: data.meta || {}
}));
}
if (typeof data.action === "undefined" || !data.action) {
throw new errs.InternalValidationError("Audit log entry must contain an Action");
}
// Make sure at least 1 of the IDs are set and action
return await auditLogModel.query().insert({
user_id: data.user_id,
action: data.action,
object_type: data.object_type || "",
object_id: data.object_id || 0,
meta: data.meta || {},
});
}
},
};
module.exports = internalAuditLog;
export default internalAuditLog;
File diff suppressed because it is too large Load Diff
+290 -361
View File
@@ -1,110 +1,96 @@
const _ = require('lodash');
const error = require('../lib/error');
const utils = require('../lib/utils');
const deadHostModel = require('../models/dead_host');
const internalHost = require('./host');
const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log');
const internalCertificate = require('./certificate');
const {castJsonIfNeed} = require('../lib/helpers');
import _ from "lodash";
import errs from "../lib/error.js";
import { castJsonIfNeed } from "../lib/helpers.js";
import utils from "../lib/utils.js";
import deadHostModel from "../models/dead_host.js";
import internalAuditLog from "./audit-log.js";
import internalCertificate from "./certificate.js";
import internalHost from "./host.js";
import internalNginx from "./nginx.js";
function omissions () {
return ['is_deleted'];
}
const omissions = () => {
return ["is_deleted"];
};
const internalDeadHost = {
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: (access, data) => {
let create_certificate = data.certificate_id === 'new';
create: async (access, data) => {
const createCertificate = data.certificate_id === "new";
if (create_certificate) {
if (createCertificate) {
delete data.certificate_id;
}
return access.can('dead_hosts:create', data)
.then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records
let domain_name_check_promises = [];
await access.can("dead_hosts:create", data);
data.domain_names.map(function (domain_name) {
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));
});
// Get a list of the domain names and check each of them against existing records
const domainNameCheckPromises = [];
return Promise.all(domain_name_check_promises)
.then((check_results) => {
check_results.map(function (result) {
if (result.is_taken) {
throw new error.ValidationError(result.hostname + ' is already in use');
}
});
});
})
.then(() => {
// At this point the domains should have been checked
data.owner_user_id = access.token.getUserId(1);
data = internalHost.cleanSslHstsData(data);
data.domain_names.map((domain_name) => {
domainNameCheckPromises.push(internalHost.isHostnameTaken(domain_name));
return true;
});
// Fix for db field not having a default value
// for this optional field.
if (typeof data.advanced_config === 'undefined') {
data.advanced_config = '';
await Promise.all(domainNameCheckPromises).then((check_results) => {
check_results.map((result) => {
if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`);
}
return deadHostModel
.query()
.insertAndFetch(data)
.then(utils.omitRow(omissions()));
})
.then((row) => {
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, data)
.then((cert) => {
// update host with cert id
return internalDeadHost.update(access, {
id: row.id,
certificate_id: cert.id
});
})
.then(() => {
return row;
});
} else {
return row;
}
})
.then((row) => {
// re-fetch with cert
return internalDeadHost.get(access, {
id: row.id,
expand: ['certificate', 'owner']
});
})
.then((row) => {
// Configure nginx
return internalNginx.configure(deadHostModel, 'dead_host', row)
.then(() => {
return row;
});
})
.then((row) => {
data.meta = _.assign({}, data.meta || {}, row.meta);
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'dead-host',
object_id: row.id,
meta: data
})
.then(() => {
return row;
});
return true;
});
});
// At this point the domains should have been checked
data.owner_user_id = access.token.getUserId(1);
const thisData = internalHost.cleanSslHstsData(data);
// Fix for db field not having a default value
// for this optional field.
if (typeof data.advanced_config === "undefined") {
thisData.advanced_config = "";
}
const row = await deadHostModel.query()
.insertAndFetch(thisData)
.then(utils.omitRow(omissions()));
// Add to audit log
await internalAuditLog.add(access, {
action: "created",
object_type: "dead-host",
object_id: row.id,
meta: thisData,
});
if (createCertificate) {
const cert = await internalCertificate.createQuickCertificate(access, data);
// update host with cert id
await internalDeadHost.update(access, {
id: row.id,
certificate_id: cert.id,
});
}
// re-fetch with cert
const freshRow = await internalDeadHost.get(access, {
id: row.id,
expand: ["certificate", "owner"],
});
// Sanity check
if (createCertificate && !freshRow.certificate_id) {
throw new errs.InternalValidationError("The host was created but the Certificate creation failed.");
}
// Configure nginx
await internalNginx.configure(deadHostModel, "dead_host", freshRow);
return freshRow;
},
/**
@@ -113,98 +99,85 @@ const internalDeadHost = {
* @param {Number} data.id
* @return {Promise}
*/
update: (access, data) => {
let create_certificate = data.certificate_id === 'new';
if (create_certificate) {
update: async (access, data) => {
const createCertificate = data.certificate_id === "new";
if (createCertificate) {
delete data.certificate_id;
}
return access.can('dead_hosts:update', data.id)
.then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records
let domain_name_check_promises = [];
await access.can("dead_hosts:update", data.id);
if (typeof data.domain_names !== 'undefined') {
data.domain_names.map(function (domain_name) {
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'dead', data.id));
});
return Promise.all(domain_name_check_promises)
.then((check_results) => {
check_results.map(function (result) {
if (result.is_taken) {
throw new error.ValidationError(result.hostname + ' is already in use');
}
});
});
}
})
.then(() => {
return internalDeadHost.get(access, {id: data.id});
})
.then((row) => {
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new error.InternalValidationError('404 Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
}
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, {
domain_names: data.domain_names || row.domain_names,
meta: _.assign({}, row.meta, data.meta)
})
.then((cert) => {
// update host with cert id
data.certificate_id = cert.id;
})
.then(() => {
return row;
});
} else {
return row;
}
})
.then((row) => {
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
data = _.assign({}, {
domain_names: row.domain_names
}, data);
data = internalHost.cleanSslHstsData(data, row);
return deadHostModel
.query()
.where({id: data.id})
.patch(data)
.then((saved_row) => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'dead-host',
object_id: row.id,
meta: data
})
.then(() => {
return _.omit(saved_row, omissions());
});
});
})
.then(() => {
return internalDeadHost.get(access, {
id: data.id,
expand: ['owner', 'certificate']
})
.then((row) => {
// Configure nginx
return internalNginx.configure(deadHostModel, 'dead_host', row)
.then((new_meta) => {
row.meta = new_meta;
row = internalHost.cleanRowCertificateMeta(row);
return _.omit(row, omissions());
});
});
// Get a list of the domain names and check each of them against existing records
const domainNameCheckPromises = [];
if (typeof data.domain_names !== "undefined") {
data.domain_names.map((domainName) => {
domainNameCheckPromises.push(internalHost.isHostnameTaken(domainName, "dead", data.id));
return true;
});
const checkResults = await Promise.all(domainNameCheckPromises);
checkResults.map((result) => {
if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`);
}
return true;
});
}
const row = await internalDeadHost.get(access, { id: data.id });
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`404 Host could not be updated, IDs do not match: ${row.id} !== ${data.id}`,
);
}
if (createCertificate) {
const cert = await internalCertificate.createQuickCertificate(access, {
domain_names: data.domain_names || row.domain_names,
meta: _.assign({}, row.meta, data.meta),
});
// update host with cert id
data.certificate_id = cert.id;
}
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
let thisData = _.assign(
{},
{
domain_names: row.domain_names,
},
data,
);
thisData = internalHost.cleanSslHstsData(thisData, row);
// do the row update
await deadHostModel
.query()
.where({id: data.id})
.patch(data);
// Add to audit log
await internalAuditLog.add(access, {
action: "updated",
object_type: "dead-host",
object_id: row.id,
meta: thisData,
});
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;
return _.omit(internalHost.cleanRowCertificateMeta(thisRow), omissions());
},
/**
@@ -215,40 +188,32 @@ const internalDeadHost = {
* @param {Array} [data.omit]
* @return {Promise}
*/
get: (access, data) => {
if (typeof data === 'undefined') {
data = {};
get: async (access, data) => {
const accessData = await access.can("dead_hosts:get", data.id);
const query = deadHostModel
.query()
.where("is_deleted", 0)
.andWhere("id", data.id)
.allowGraph(deadHostModel.defaultAllowGraph)
.first();
if (accessData.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
return access.can('dead_hosts:get', data.id)
.then((access_data) => {
let query = deadHostModel
.query()
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowGraph('[owner,certificate]')
.first();
if (typeof data.expand !== "undefined" && data.expand !== null) {
query.withGraphFetched(`[${data.expand.join(", ")}]`);
}
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
}
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched('[' + data.expand.join(', ') + ']');
}
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
}
// Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) {
row = _.omit(row, data.omit);
}
return row;
});
const row = await query.then(utils.omitRow(omissions()));
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
// Custom omissions
if (typeof data.omit !== "undefined" && data.omit !== null) {
return _.omit(row, data.omit);
}
return row;
},
/**
@@ -258,42 +223,32 @@ const internalDeadHost = {
* @param {String} [data.reason]
* @returns {Promise}
*/
delete: (access, data) => {
return access.can('dead_hosts:delete', data.id)
.then(() => {
return internalDeadHost.get(access, {id: data.id});
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
}
delete: async (access, data) => {
await access.can("dead_hosts:delete", data.id)
const row = await internalDeadHost.get(access, { id: data.id });
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
return deadHostModel
.query()
.where('id', row.id)
.patch({
is_deleted: 1
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig('dead_host', row)
.then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'dead-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
.then(() => {
return true;
await deadHostModel
.query()
.where("id", row.id)
.patch({
is_deleted: 1,
});
// Delete Nginx Config
await internalNginx.deleteConfig("dead_host", row);
await internalNginx.reload();
// Add to audit log
await internalAuditLog.add(access, {
action: "deleted",
object_type: "dead-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
return true;
},
/**
@@ -303,46 +258,39 @@ const internalDeadHost = {
* @param {String} [data.reason]
* @returns {Promise}
*/
enable: (access, data) => {
return access.can('dead_hosts:update', data.id)
.then(() => {
return internalDeadHost.get(access, {
id: data.id,
expand: ['certificate', 'owner']
});
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
} else if (row.enabled) {
throw new error.ValidationError('Host is already enabled');
}
enable: async (access, data) => {
await access.can("dead_hosts:update", data.id)
const row = await internalDeadHost.get(access, {
id: data.id,
expand: ["certificate", "owner"],
});
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
throw new errs.ValidationError("Host is already enabled");
}
row.enabled = 1;
row.enabled = 1;
return deadHostModel
.query()
.where('id', row.id)
.patch({
enabled: 1
})
.then(() => {
// Configure nginx
return internalNginx.configure(deadHostModel, 'dead_host', row);
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'enabled',
object_type: 'dead-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
.then(() => {
return true;
await deadHostModel
.query()
.where("id", row.id)
.patch({
enabled: 1,
});
// Configure nginx
await internalNginx.configure(deadHostModel, "dead_host", row);
// Add to audit log
await internalAuditLog.add(access, {
action: "enabled",
object_type: "dead-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
return true;
},
/**
@@ -352,46 +300,37 @@ const internalDeadHost = {
* @param {String} [data.reason]
* @returns {Promise}
*/
disable: (access, data) => {
return access.can('dead_hosts:update', data.id)
.then(() => {
return internalDeadHost.get(access, {id: data.id});
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
} else if (!row.enabled) {
throw new error.ValidationError('Host is already disabled');
}
disable: async (access, data) => {
await access.can("dead_hosts:update", data.id)
const row = await internalDeadHost.get(access, { id: data.id });
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (!row.enabled) {
throw new errs.ValidationError("Host is already disabled");
}
row.enabled = 0;
row.enabled = 0;
return deadHostModel
.query()
.where('id', row.id)
.patch({
enabled: 0
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig('dead_host', row)
.then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'disabled',
object_type: 'dead-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
.then(() => {
return true;
await deadHostModel
.query()
.where("id", row.id)
.patch({
enabled: 0,
});
// Delete Nginx Config
await internalNginx.deleteConfig("dead_host", row);
await internalNginx.reload();
// Add to audit log
await internalAuditLog.add(access, {
action: "disabled",
object_type: "dead-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
return true;
},
/**
@@ -399,43 +338,38 @@ const internalDeadHost = {
*
* @param {Access} access
* @param {Array} [expand]
* @param {String} [search_query]
* @param {String} [searchQuery]
* @returns {Promise}
*/
getAll: (access, expand, search_query) => {
return access.can('dead_hosts:list')
.then((access_data) => {
let query = deadHostModel
.query()
.where('is_deleted', 0)
.groupBy('id')
.allowGraph('[owner,certificate]')
.orderBy(castJsonIfNeed('domain_names'), 'ASC');
getAll: async (access, expand, searchQuery) => {
const accessData = await access.can("dead_hosts:list")
const query = deadHostModel
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph(deadHostModel.defaultAllowGraph)
.orderBy(castJsonIfNeed("domain_names"), "ASC");
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
}
if (accessData.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
// Query is used for searching
if (typeof search_query === 'string' && search_query.length > 0) {
query.where(function () {
this.where(castJsonIfNeed('domain_names'), 'like', '%' + search_query + '%');
});
}
if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched('[' + expand.join(', ') + ']');
}
return query.then(utils.omitRows(omissions()));
})
.then((rows) => {
if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {
return internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
// Query is used for searching
if (typeof searchQuery === "string" && searchQuery.length > 0) {
query.where(function () {
this.where(castJsonIfNeed("domain_names"), "like", `%${searchQuery}%`);
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
const rows = await query.then(utils.omitRows(omissions()));
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
},
/**
@@ -445,21 +379,16 @@ const internalDeadHost = {
* @param {String} visibility
* @returns {Promise}
*/
getCount: (user_id, visibility) => {
let query = deadHostModel
.query()
.count('id as count')
.where('is_deleted', 0);
getCount: async (user_id, visibility) => {
const query = deadHostModel.query().count("id as count").where("is_deleted", 0);
if (visibility !== 'all') {
query.andWhere('owner_user_id', user_id);
if (visibility !== "all") {
query.andWhere("owner_user_id", user_id);
}
return query.first()
.then((row) => {
return parseInt(row.count, 10);
});
}
const row = await query.first();
return Number.parseInt(row.count, 10);
},
};
module.exports = internalDeadHost;
export default internalDeadHost;
+116 -118
View File
@@ -1,11 +1,10 @@
const _ = require('lodash');
const proxyHostModel = require('../models/proxy_host');
const redirectionHostModel = require('../models/redirection_host');
const deadHostModel = require('../models/dead_host');
const {castJsonIfNeed} = require('../lib/helpers');
import _ from "lodash";
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 = {
/**
* Makes sure that the ssl_* and hsts_* fields play nicely together.
* ie: if there is no cert, then force_ssl is off.
@@ -15,25 +14,23 @@ const internalHost = {
* @param {object} [existing_data]
* @returns {object}
*/
cleanSslHstsData: function (data, existing_data) {
existing_data = existing_data === undefined ? {} : existing_data;
cleanSslHstsData: (data, existingData) => {
const combinedData = _.assign({}, existingData || {}, data);
const combined_data = _.assign({}, existing_data, data);
if (!combined_data.certificate_id) {
combined_data.ssl_forced = false;
combined_data.http2_support = false;
if (!combinedData.certificate_id) {
combinedData.ssl_forced = false;
combinedData.http2_support = false;
}
if (!combined_data.ssl_forced) {
combined_data.hsts_enabled = false;
if (!combinedData.ssl_forced) {
combinedData.hsts_enabled = false;
}
if (!combined_data.hsts_enabled) {
combined_data.hsts_subdomains = false;
if (!combinedData.hsts_enabled) {
combinedData.hsts_subdomains = false;
}
return combined_data;
return combinedData;
},
/**
@@ -42,11 +39,12 @@ const internalHost = {
* @param {Array} rows
* @returns {Array}
*/
cleanAllRowsCertificateMeta: function (rows) {
rows.map(function (row, idx) {
if (typeof rows[idx].certificate !== 'undefined' && rows[idx].certificate) {
cleanAllRowsCertificateMeta: (rows) => {
rows.map((_, idx) => {
if (typeof rows[idx].certificate !== "undefined" && rows[idx].certificate) {
rows[idx].certificate.meta = {};
}
return true;
});
return rows;
@@ -58,8 +56,8 @@ const internalHost = {
* @param {Object} row
* @returns {Object}
*/
cleanRowCertificateMeta: function (row) {
if (typeof row.certificate !== 'undefined' && row.certificate) {
cleanRowCertificateMeta: (row) => {
if (typeof row.certificate !== "undefined" && row.certificate) {
row.certificate.meta = {};
}
@@ -67,54 +65,33 @@ const internalHost = {
},
/**
* This returns all the host types with any domain listed in the provided domain_names array.
* This returns all the host types with any domain listed in the provided domainNames array.
* This is used by the certificates to temporarily disable any host that is using the domain
*
* @param {Array} domain_names
* @param {Array} domainNames
* @returns {Promise}
*/
getHostsWithDomains: function (domain_names) {
const promises = [
proxyHostModel
.query()
.where('is_deleted', 0),
redirectionHostModel
.query()
.where('is_deleted', 0),
deadHostModel
.query()
.where('is_deleted', 0)
];
getHostsWithDomains: async (domainNames) => {
const responseObject = {
total_count: 0,
dead_hosts: [],
proxy_hosts: [],
redirection_hosts: [],
};
return Promise.all(promises)
.then((promises_results) => {
let response_object = {
total_count: 0,
dead_hosts: [],
proxy_hosts: [],
redirection_hosts: []
};
const proxyRes = await proxyHostModel.query().where("is_deleted", 0);
responseObject.proxy_hosts = internalHost._getHostsWithDomains(proxyRes, domainNames);
responseObject.total_count += responseObject.proxy_hosts.length;
if (promises_results[0]) {
// Proxy Hosts
response_object.proxy_hosts = internalHost._getHostsWithDomains(promises_results[0], domain_names);
response_object.total_count += response_object.proxy_hosts.length;
}
const redirRes = await redirectionHostModel.query().where("is_deleted", 0);
responseObject.redirection_hosts = internalHost._getHostsWithDomains(redirRes, domainNames);
responseObject.total_count += responseObject.redirection_hosts.length;
if (promises_results[1]) {
// Redirection Hosts
response_object.redirection_hosts = internalHost._getHostsWithDomains(promises_results[1], domain_names);
response_object.total_count += response_object.redirection_hosts.length;
}
const deadRes = await deadHostModel.query().where("is_deleted", 0);
responseObject.dead_hosts = internalHost._getHostsWithDomains(deadRes, domainNames);
responseObject.total_count += responseObject.dead_hosts.length;
if (promises_results[2]) {
// Dead Hosts
response_object.dead_hosts = internalHost._getHostsWithDomains(promises_results[2], domain_names);
response_object.total_count += response_object.dead_hosts.length;
}
return response_object;
});
return responseObject;
},
/**
@@ -125,112 +102,133 @@ const internalHost = {
* @param {Integer} [ignore_id] Must be supplied if type was also supplied
* @returns {Promise}
*/
isHostnameTaken: function (hostname, ignore_type, ignore_id) {
isHostnameTaken: (hostname, ignore_type, ignore_id) => {
const promises = [
proxyHostModel
.query()
.where('is_deleted', 0)
.andWhere(castJsonIfNeed('domain_names'), 'like', '%' + hostname + '%'),
.where("is_deleted", 0)
.andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`),
redirectionHostModel
.query()
.where('is_deleted', 0)
.andWhere(castJsonIfNeed('domain_names'), 'like', '%' + hostname + '%'),
.where("is_deleted", 0)
.andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`),
deadHostModel
.query()
.where('is_deleted', 0)
.andWhere(castJsonIfNeed('domain_names'), 'like', '%' + hostname + '%')
.where("is_deleted", 0)
.andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`),
];
return Promise.all(promises)
.then((promises_results) => {
let is_taken = false;
return Promise.all(promises).then((promises_results) => {
let is_taken = false;
if (promises_results[0]) {
// Proxy Hosts
if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[0], ignore_type === 'proxy' && ignore_id ? ignore_id : 0)) {
is_taken = true;
}
if (promises_results[0]) {
// Proxy Hosts
if (
internalHost._checkHostnameRecordsTaken(
hostname,
promises_results[0],
ignore_type === "proxy" && ignore_id ? ignore_id : 0,
)
) {
is_taken = true;
}
}
if (promises_results[1]) {
// Redirection Hosts
if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[1], ignore_type === 'redirection' && ignore_id ? ignore_id : 0)) {
is_taken = true;
}
if (promises_results[1]) {
// Redirection Hosts
if (
internalHost._checkHostnameRecordsTaken(
hostname,
promises_results[1],
ignore_type === "redirection" && ignore_id ? ignore_id : 0,
)
) {
is_taken = true;
}
}
if (promises_results[2]) {
// Dead Hosts
if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[2], ignore_type === 'dead' && ignore_id ? ignore_id : 0)) {
is_taken = true;
}
if (promises_results[2]) {
// Dead Hosts
if (
internalHost._checkHostnameRecordsTaken(
hostname,
promises_results[2],
ignore_type === "dead" && ignore_id ? ignore_id : 0,
)
) {
is_taken = true;
}
}
return {
hostname: hostname,
is_taken: is_taken
};
});
return {
hostname: hostname,
is_taken: is_taken,
};
});
},
/**
* Private call only
*
* @param {String} hostname
* @param {Array} existing_rows
* @param {Integer} [ignore_id]
* @param {Array} existingRows
* @param {Integer} [ignoreId]
* @returns {Boolean}
*/
_checkHostnameRecordsTaken: function (hostname, existing_rows, ignore_id) {
let is_taken = false;
_checkHostnameRecordsTaken: (hostname, existingRows, ignoreId) => {
let isTaken = false;
if (existing_rows && existing_rows.length) {
existing_rows.map(function (existing_row) {
existing_row.domain_names.map(function (existing_hostname) {
if (existingRows?.length) {
existingRows.map((existingRow) => {
existingRow.domain_names.map((existingHostname) => {
// Does this domain match?
if (existing_hostname.toLowerCase() === hostname.toLowerCase()) {
if (!ignore_id || ignore_id !== existing_row.id) {
is_taken = true;
if (existingHostname.toLowerCase() === hostname.toLowerCase()) {
if (!ignoreId || ignoreId !== existingRow.id) {
isTaken = true;
}
}
return true;
});
return true;
});
}
return is_taken;
return isTaken;
},
/**
* Private call only
*
* @param {Array} hosts
* @param {Array} domain_names
* @param {Array} domainNames
* @returns {Array}
*/
_getHostsWithDomains: function (hosts, domain_names) {
let response = [];
_getHostsWithDomains: (hosts, domainNames) => {
const response = [];
if (hosts && hosts.length) {
hosts.map(function (host) {
let host_matches = false;
if (hosts?.length) {
hosts.map((host) => {
let hostMatches = false;
domain_names.map(function (domain_name) {
host.domain_names.map(function (host_domain_name) {
if (domain_name.toLowerCase() === host_domain_name.toLowerCase()) {
host_matches = true;
domainNames.map((domainName) => {
host.domain_names.map((hostDomainName) => {
if (domainName.toLowerCase() === hostDomainName.toLowerCase()) {
hostMatches = true;
}
return true;
});
return true;
});
if (host_matches) {
if (hostMatches) {
response.push(host);
}
return true;
});
}
return response;
}
},
};
module.exports = internalHost;
export default internalHost;
+66 -55
View File
@@ -1,45 +1,53 @@
const https = require('https');
const fs = require('fs');
const logger = require('../logger').ip_ranges;
const error = require('../lib/error');
const utils = require('../lib/utils');
const internalNginx = require('./nginx');
import fs from "node:fs";
import https from "node:https";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { ProxyAgent } from "proxy-agent";
import errs from "../lib/error.js";
import utils from "../lib/utils.js";
import { ipRanges as logger } from "../logger.js";
import internalNginx from "./nginx.js";
const CLOUDFRONT_URL = 'https://ip-ranges.amazonaws.com/ip-ranges.json';
const CLOUDFARE_V4_URL = 'https://www.cloudflare.com/ips-v4';
const CLOUDFARE_V6_URL = 'https://www.cloudflare.com/ips-v6';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const CLOUDFRONT_URL = "https://ip-ranges.amazonaws.com/ip-ranges.json";
const CLOUDFARE_V4_URL = "https://www.cloudflare.com/ips-v4";
const CLOUDFARE_V6_URL = "https://www.cloudflare.com/ips-v6";
const regIpV4 = /^(\d+\.?){4}\/\d+/;
const regIpV6 = /^(([\da-fA-F]+)?:)+\/\d+/;
const internalIpRanges = {
interval_timeout: 1000 * 60 * 60 * 6, // 6 hours
interval: null,
interval_timeout: 1000 * 60 * 60 * 6, // 6 hours
interval: null,
interval_processing: false,
iteration_count: 0,
iteration_count: 0,
initTimer: () => {
logger.info('IP Ranges Renewal Timer initialized');
logger.info("IP Ranges Renewal Timer initialized");
internalIpRanges.interval = setInterval(internalIpRanges.fetch, internalIpRanges.interval_timeout);
},
fetchUrl: (url) => {
const agent = new ProxyAgent();
return new Promise((resolve, reject) => {
logger.info('Fetching ' + url);
return https.get(url, (res) => {
res.setEncoding('utf8');
let raw_data = '';
res.on('data', (chunk) => {
raw_data += chunk;
});
logger.info(`Fetching ${url}`);
return https
.get(url, { agent }, (res) => {
res.setEncoding("utf8");
let raw_data = "";
res.on("data", (chunk) => {
raw_data += chunk;
});
res.on('end', () => {
resolve(raw_data);
res.on("end", () => {
resolve(raw_data);
});
})
.on("error", (err) => {
reject(err);
});
}).on('error', (err) => {
reject(err);
});
});
},
@@ -49,27 +57,30 @@ const internalIpRanges = {
fetch: () => {
if (!internalIpRanges.interval_processing) {
internalIpRanges.interval_processing = true;
logger.info('Fetching IP Ranges from online services...');
logger.info("Fetching IP Ranges from online services...");
let ip_ranges = [];
return internalIpRanges.fetchUrl(CLOUDFRONT_URL)
return internalIpRanges
.fetchUrl(CLOUDFRONT_URL)
.then((cloudfront_data) => {
let data = JSON.parse(cloudfront_data);
const data = JSON.parse(cloudfront_data);
if (data && typeof data.prefixes !== 'undefined') {
if (data && typeof data.prefixes !== "undefined") {
data.prefixes.map((item) => {
if (item.service === 'CLOUDFRONT') {
if (item.service === "CLOUDFRONT") {
ip_ranges.push(item.ip_prefix);
}
return true;
});
}
if (data && typeof data.ipv6_prefixes !== 'undefined') {
if (data && typeof data.ipv6_prefixes !== "undefined") {
data.ipv6_prefixes.map((item) => {
if (item.service === 'CLOUDFRONT') {
if (item.service === "CLOUDFRONT") {
ip_ranges.push(item.ipv6_prefix);
}
return true;
});
}
})
@@ -77,38 +88,38 @@ const internalIpRanges = {
return internalIpRanges.fetchUrl(CLOUDFARE_V4_URL);
})
.then((cloudfare_data) => {
let items = cloudfare_data.split('\n').filter((line) => regIpV4.test(line));
ip_ranges = [... ip_ranges, ... items];
const items = cloudfare_data.split("\n").filter((line) => regIpV4.test(line));
ip_ranges = [...ip_ranges, ...items];
})
.then(() => {
return internalIpRanges.fetchUrl(CLOUDFARE_V6_URL);
})
.then((cloudfare_data) => {
let items = cloudfare_data.split('\n').filter((line) => regIpV6.test(line));
ip_ranges = [... ip_ranges, ... items];
const items = cloudfare_data.split("\n").filter((line) => regIpV6.test(line));
ip_ranges = [...ip_ranges, ...items];
})
.then(() => {
let clean_ip_ranges = [];
const clean_ip_ranges = [];
ip_ranges.map((range) => {
if (range) {
clean_ip_ranges.push(range);
}
return true;
});
return internalIpRanges.generateConfig(clean_ip_ranges)
.then(() => {
if (internalIpRanges.iteration_count) {
// Reload nginx
return internalNginx.reload();
}
});
return internalIpRanges.generateConfig(clean_ip_ranges).then(() => {
if (internalIpRanges.iteration_count) {
// Reload nginx
return internalNginx.reload();
}
});
})
.then(() => {
internalIpRanges.interval_processing = false;
internalIpRanges.iteration_count++;
})
.catch((err) => {
logger.error(err.message);
logger.fatal(err.message);
internalIpRanges.interval_processing = false;
});
}
@@ -122,26 +133,26 @@ const internalIpRanges = {
const renderEngine = utils.getRenderEngine();
return new Promise((resolve, reject) => {
let template = null;
let filename = '/etc/nginx/conf.d/include/ip_ranges.conf';
const filename = "/etc/nginx/conf.d/include/ip_ranges.conf";
try {
template = fs.readFileSync(__dirname + '/../templates/ip_ranges.conf', {encoding: 'utf8'});
template = fs.readFileSync(`${__dirname}/../templates/ip_ranges.conf`, { encoding: "utf8" });
} catch (err) {
reject(new error.ConfigurationError(err.message));
reject(new errs.ConfigurationError(err.message));
return;
}
renderEngine
.parseAndRender(template, {ip_ranges: ip_ranges})
.parseAndRender(template, { ip_ranges: ip_ranges })
.then((config_text) => {
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
fs.writeFileSync(filename, config_text, { encoding: "utf8" });
resolve(true);
})
.catch((err) => {
logger.warn('Could not write ' + filename + ':', err.message);
reject(new error.ConfigurationError(err.message));
logger.warn(`Could not write ${filename}: ${err.message}`);
reject(new errs.ConfigurationError(err.message));
});
});
}
},
};
module.exports = internalIpRanges;
export default internalIpRanges;
+119 -120
View File
@@ -1,12 +1,15 @@
const _ = require('lodash');
const fs = require('fs');
const logger = require('../logger').nginx;
const config = require('../lib/config');
const utils = require('../lib/utils');
const error = require('../lib/error');
import fs from "node:fs";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import _ from "lodash";
import errs from "../lib/error.js";
import utils from "../lib/utils.js";
import { debug, nginx as logger } from "../logger.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const internalNginx = {
/**
* This will:
* - test the nginx config first to make sure it's OK
@@ -24,7 +27,8 @@ const internalNginx = {
configure: (model, host_type, host) => {
let combined_meta = {};
return internalNginx.test()
return internalNginx
.test()
.then(() => {
// Nginx is OK
// We're deleting this config regardless.
@@ -37,49 +41,46 @@ const internalNginx = {
})
.then(() => {
// Test nginx again and update meta with result
return internalNginx.test()
return internalNginx
.test()
.then(() => {
// nginx is ok
combined_meta = _.assign({}, host.meta, {
nginx_online: true,
nginx_err: null
nginx_err: null,
});
return model
.query()
.where('id', host.id)
.patch({
meta: combined_meta
});
return model.query().where("id", host.id).patch({
meta: combined_meta,
});
})
.catch((err) => {
// Remove the error_log line because it's a docker-ism false positive that doesn't need to be reported.
// It will always look like this:
// nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (6: No such device or address)
let valid_lines = [];
let err_lines = err.message.split('\n');
err_lines.map(function (line) {
if (line.indexOf('/var/log/nginx/error.log') === -1) {
const valid_lines = [];
const err_lines = err.message.split("\n");
err_lines.map((line) => {
if (line.indexOf("/var/log/nginx/error.log") === -1) {
valid_lines.push(line);
}
return true;
});
if (config.debug()) {
logger.error('Nginx test failed:', valid_lines.join('\n'));
}
debug(logger, "Nginx test failed:", valid_lines.join("\n"));
// config is bad, update meta and delete config
combined_meta = _.assign({}, host.meta, {
nginx_online: false,
nginx_err: valid_lines.join('\n')
nginx_err: valid_lines.join("\n"),
});
return model
.query()
.where('id', host.id)
.where("id", host.id)
.patch({
meta: combined_meta
meta: combined_meta,
})
.then(() => {
internalNginx.renameConfigAsError(host_type, host);
@@ -101,22 +102,18 @@ const internalNginx = {
* @returns {Promise}
*/
test: () => {
if (config.debug()) {
logger.info('Testing Nginx configuration');
}
return utils.exec('/usr/sbin/nginx -t -g "error_log off;"');
debug(logger, "Testing Nginx configuration");
return utils.execFile("/usr/sbin/nginx", ["-t", "-g", "error_log off;"]);
},
/**
* @returns {Promise}
*/
reload: () => {
return internalNginx.test()
.then(() => {
logger.info('Reloading Nginx');
return utils.exec('/usr/sbin/nginx -s reload');
});
return internalNginx.test().then(() => {
logger.info("Reloading Nginx");
return utils.execFile("/usr/sbin/nginx", ["-s", "reload"]);
});
},
/**
@@ -125,10 +122,10 @@ const internalNginx = {
* @returns {String}
*/
getConfigName: (host_type, host_id) => {
if (host_type === 'default') {
return '/data/nginx/default_host/site.conf';
if (host_type === "default") {
return "/data/nginx/default_host/site.conf";
}
return '/data/nginx/' + internalNginx.getFileFriendlyHostType(host_type) + '/' + host_id + '.conf';
return `/data/nginx/${internalNginx.getFileFriendlyHostType(host_type)}/${host_id}.conf`;
},
/**
@@ -141,38 +138,45 @@ const internalNginx = {
let template;
try {
template = fs.readFileSync(__dirname + '/../templates/_location.conf', {encoding: 'utf8'});
template = fs.readFileSync(`${__dirname}/../templates/_location.conf`, { encoding: "utf8" });
} catch (err) {
reject(new error.ConfigurationError(err.message));
reject(new errs.ConfigurationError(err.message));
return;
}
const renderEngine = utils.getRenderEngine();
let renderedLocations = '';
const renderEngine = utils.getRenderEngine();
let renderedLocations = "";
const locationRendering = async () => {
for (let i = 0; i < host.locations.length; i++) {
let locationCopy = Object.assign({}, {access_list_id: host.access_list_id}, {certificate_id: host.certificate_id},
{ssl_forced: host.ssl_forced}, {caching_enabled: host.caching_enabled}, {block_exploits: host.block_exploits},
{allow_websocket_upgrade: host.allow_websocket_upgrade}, {http2_support: host.http2_support},
{hsts_enabled: host.hsts_enabled}, {hsts_subdomains: host.hsts_subdomains}, {access_list: host.access_list},
{certificate: host.certificate}, host.locations[i]);
const locationCopy = Object.assign(
{},
{ access_list_id: host.access_list_id },
{ certificate_id: host.certificate_id },
{ ssl_forced: host.ssl_forced },
{ caching_enabled: host.caching_enabled },
{ block_exploits: host.block_exploits },
{ allow_websocket_upgrade: host.allow_websocket_upgrade },
{ http2_support: host.http2_support },
{ hsts_enabled: host.hsts_enabled },
{ hsts_subdomains: host.hsts_subdomains },
{ access_list: host.access_list },
{ certificate: host.certificate },
host.locations[i],
);
if (locationCopy.forward_host.indexOf('/') > -1) {
const splitted = locationCopy.forward_host.split('/');
if (locationCopy.forward_host.indexOf("/") > -1) {
const splitted = locationCopy.forward_host.split("/");
locationCopy.forward_host = splitted.shift();
locationCopy.forward_path = `/${splitted.join('/')}`;
locationCopy.forward_path = `/${splitted.join("/")}`;
}
// eslint-disable-next-line
renderedLocations += await renderEngine.parseAndRender(template, locationCopy);
}
};
locationRendering().then(() => resolve(renderedLocations));
});
},
@@ -183,23 +187,21 @@ const internalNginx = {
*/
generateConfig: (host_type, host_row) => {
// Prevent modifying the original object:
let host = JSON.parse(JSON.stringify(host_row));
const host = JSON.parse(JSON.stringify(host_row));
const nice_host_type = internalNginx.getFileFriendlyHostType(host_type);
if (config.debug()) {
logger.info('Generating ' + nice_host_type + ' Config:', JSON.stringify(host, null, 2));
}
debug(logger, `Generating ${nice_host_type} Config:`, JSON.stringify(host, null, 2));
const renderEngine = utils.getRenderEngine();
return new Promise((resolve, reject) => {
let template = null;
let filename = internalNginx.getConfigName(nice_host_type, host.id);
const filename = internalNginx.getConfigName(nice_host_type, host.id);
try {
template = fs.readFileSync(__dirname + '/../templates/' + nice_host_type + '.conf', {encoding: 'utf8'});
template = fs.readFileSync(`${__dirname}/../templates/${nice_host_type}.conf`, { encoding: "utf8" });
} catch (err) {
reject(new error.ConfigurationError(err.message));
reject(new errs.ConfigurationError(err.message));
return;
}
@@ -207,27 +209,31 @@ const internalNginx = {
let origLocations;
// Manipulate the data a bit before sending it to the template
if (nice_host_type !== 'default') {
if (nice_host_type !== "default") {
host.use_default_location = true;
if (typeof host.advanced_config !== 'undefined' && host.advanced_config) {
if (typeof host.advanced_config !== "undefined" && host.advanced_config) {
host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config);
}
}
// 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";
}
if (host.locations) {
//logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2));
origLocations = [].concat(host.locations);
origLocations = [].concat(host.locations);
locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => {
host.locations = renderedLocations;
});
// Allow someone who is using / custom location path to use it, and skip the default / location
_.map(host.locations, (location) => {
if (location.path === '/') {
if (location.path === "/") {
host.use_default_location = false;
}
});
} else {
locationsPromise = Promise.resolve();
}
@@ -239,11 +245,8 @@ const internalNginx = {
renderEngine
.parseAndRender(template, host)
.then((config_text) => {
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
if (config.debug()) {
logger.success('Wrote config:', filename, config_text);
}
fs.writeFileSync(filename, config_text, { encoding: "utf8" });
debug(logger, "Wrote config:", filename, config_text);
// Restore locations array
host.locations = origLocations;
@@ -251,11 +254,8 @@ const internalNginx = {
resolve(true);
})
.catch((err) => {
if (config.debug()) {
logger.warn('Could not write ' + filename + ':', err.message);
}
reject(new error.ConfigurationError(err.message));
debug(logger, `Could not write ${filename}:`, err.message);
reject(new errs.ConfigurationError(err.message));
});
});
});
@@ -270,20 +270,17 @@ const internalNginx = {
* @returns {Promise}
*/
generateLetsEncryptRequestConfig: (certificate) => {
if (config.debug()) {
logger.info('Generating LetsEncrypt Request Config:', certificate);
}
debug(logger, "Generating LetsEncrypt Request Config:", certificate);
const renderEngine = utils.getRenderEngine();
return new Promise((resolve, reject) => {
let template = null;
let filename = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf';
const filename = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`;
try {
template = fs.readFileSync(__dirname + '/../templates/letsencrypt-request.conf', {encoding: 'utf8'});
template = fs.readFileSync(`${__dirname}/../templates/letsencrypt-request.conf`, { encoding: "utf8" });
} catch (err) {
reject(new error.ConfigurationError(err.message));
reject(new errs.ConfigurationError(err.message));
return;
}
@@ -292,20 +289,13 @@ const internalNginx = {
renderEngine
.parseAndRender(template, certificate)
.then((config_text) => {
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
if (config.debug()) {
logger.success('Wrote config:', filename, config_text);
}
fs.writeFileSync(filename, config_text, { encoding: "utf8" });
debug(logger, "Wrote config:", filename, config_text);
resolve(true);
})
.catch((err) => {
if (config.debug()) {
logger.warn('Could not write ' + filename + ':', err.message);
}
reject(new error.ConfigurationError(err.message));
debug(logger, `Could not write ${filename}:`, err.message);
reject(new errs.ConfigurationError(err.message));
});
});
},
@@ -316,11 +306,14 @@ const internalNginx = {
* @param {String} filename
*/
deleteFile: (filename) => {
logger.debug('Deleting file: ' + filename);
if (!fs.existsSync(filename)) {
return;
}
try {
debug(logger, `Deleting file: ${filename}`);
fs.unlinkSync(filename);
} catch (err) {
logger.debug('Could not delete file:', JSON.stringify(err, null, 2));
debug(logger, "Could not delete file:", JSON.stringify(err, null, 2));
}
},
@@ -330,7 +323,7 @@ const internalNginx = {
* @returns String
*/
getFileFriendlyHostType: (host_type) => {
return host_type.replace(new RegExp('-', 'g'), '_');
return host_type.replace(/-/g, "_");
},
/**
@@ -340,8 +333,8 @@ const internalNginx = {
* @returns {Promise}
*/
deleteLetsEncryptRequestConfig: (certificate) => {
const config_file = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf';
return new Promise((resolve/*, reject*/) => {
const config_file = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`;
return new Promise((resolve /*, reject*/) => {
internalNginx.deleteFile(config_file);
resolve();
});
@@ -354,10 +347,13 @@ const internalNginx = {
* @returns {Promise}
*/
deleteConfig: (host_type, host, delete_err_file) => {
const config_file = internalNginx.getConfigName(internalNginx.getFileFriendlyHostType(host_type), typeof host === 'undefined' ? 0 : host.id);
const config_file_err = config_file + '.err';
const config_file = internalNginx.getConfigName(
internalNginx.getFileFriendlyHostType(host_type),
typeof host === "undefined" ? 0 : host.id,
);
const config_file_err = `${config_file}.err`;
return new Promise((resolve/*, reject*/) => {
return new Promise((resolve /*, reject*/) => {
internalNginx.deleteFile(config_file);
if (delete_err_file) {
internalNginx.deleteFile(config_file_err);
@@ -372,10 +368,13 @@ const internalNginx = {
* @returns {Promise}
*/
renameConfigAsError: (host_type, host) => {
const config_file = internalNginx.getConfigName(internalNginx.getFileFriendlyHostType(host_type), typeof host === 'undefined' ? 0 : host.id);
const config_file_err = config_file + '.err';
const config_file = internalNginx.getConfigName(
internalNginx.getFileFriendlyHostType(host_type),
typeof host === "undefined" ? 0 : host.id,
);
const config_file_err = `${config_file}.err`;
return new Promise((resolve/*, reject*/) => {
return new Promise((resolve /*, reject*/) => {
fs.unlink(config_file, () => {
// ignore result, continue
fs.rename(config_file, config_file_err, () => {
@@ -387,14 +386,15 @@ const internalNginx = {
},
/**
* @param {String} host_type
* @param {String} hostType
* @param {Array} hosts
* @returns {Promise}
*/
bulkGenerateConfigs: (host_type, hosts) => {
let promises = [];
hosts.map(function (host) {
promises.push(internalNginx.generateConfig(host_type, host));
bulkGenerateConfigs: (hostType, hosts) => {
const promises = [];
hosts.map((host) => {
promises.push(internalNginx.generateConfig(hostType, host));
return true;
});
return Promise.all(promises);
@@ -406,9 +406,10 @@ const internalNginx = {
* @returns {Promise}
*/
bulkDeleteConfigs: (host_type, hosts) => {
let promises = [];
hosts.map(function (host) {
const promises = [];
hosts.map((host) => {
promises.push(internalNginx.deleteConfig(host_type, host, true));
return true;
});
return Promise.all(promises);
@@ -418,21 +419,19 @@ const internalNginx = {
* @param {string} config
* @returns {boolean}
*/
advancedConfigHasDefaultLocation: function (cfg) {
return !!cfg.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im);
},
advancedConfigHasDefaultLocation: (cfg) => !!cfg.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im),
/**
* @returns {boolean}
*/
ipv6Enabled: function () {
if (typeof process.env.DISABLE_IPV6 !== 'undefined') {
ipv6Enabled: () => {
if (typeof process.env.DISABLE_IPV6 !== "undefined") {
const disabled = process.env.DISABLE_IPV6.toLowerCase();
return !(disabled === 'on' || disabled === 'true' || disabled === '1' || disabled === 'yes');
return !(disabled === "on" || disabled === "true" || disabled === "1" || disabled === "yes");
}
return true;
}
},
};
module.exports = internalNginx;
export default internalNginx;
+208 -206
View File
@@ -1,107 +1,106 @@
const _ = require('lodash');
const error = require('../lib/error');
const utils = require('../lib/utils');
const proxyHostModel = require('../models/proxy_host');
const internalHost = require('./host');
const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log');
const internalCertificate = require('./certificate');
const {castJsonIfNeed} = require('../lib/helpers');
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 internalAuditLog from "./audit-log.js";
import internalCertificate from "./certificate.js";
import internalHost from "./host.js";
import internalNginx from "./nginx.js";
function omissions () {
return ['is_deleted', 'owner.is_deleted'];
}
const omissions = () => {
return ["is_deleted", "owner.is_deleted"];
};
const internalProxyHost = {
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: (access, data) => {
let create_certificate = data.certificate_id === 'new';
let thisData = data;
const createCertificate = thisData.certificate_id === "new";
if (create_certificate) {
delete data.certificate_id;
if (createCertificate) {
delete thisData.certificate_id;
}
return access.can('proxy_hosts:create', data)
return access
.can("proxy_hosts:create", thisData)
.then(() => {
// Get a list of the domain names and check each of them against existing records
let domain_name_check_promises = [];
const domain_name_check_promises = [];
data.domain_names.map(function (domain_name) {
thisData.domain_names.map((domain_name) => {
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));
return true;
});
return Promise.all(domain_name_check_promises)
.then((check_results) => {
check_results.map(function (result) {
if (result.is_taken) {
throw new error.ValidationError(result.hostname + ' is already in use');
}
});
return Promise.all(domain_name_check_promises).then((check_results) => {
check_results.map((result) => {
if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`);
}
return true;
});
});
})
.then(() => {
// At this point the domains should have been checked
data.owner_user_id = access.token.getUserId(1);
data = internalHost.cleanSslHstsData(data);
thisData.owner_user_id = access.token.getUserId(1);
thisData = internalHost.cleanSslHstsData(thisData);
// 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 = "";
}
return proxyHostModel
.query()
.insertAndFetch(data)
.then(utils.omitRow(omissions()));
return proxyHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
})
.then((row) => {
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, data)
if (createCertificate) {
return internalCertificate
.createQuickCertificate(access, thisData)
.then((cert) => {
// update host with cert id
return internalProxyHost.update(access, {
id: row.id,
certificate_id: cert.id
id: row.id,
certificate_id: cert.id,
});
})
.then(() => {
return row;
});
} else {
return row;
}
return row;
})
.then((row) => {
// re-fetch with cert
return internalProxyHost.get(access, {
id: row.id,
expand: ['certificate', 'owner', 'access_list.[clients,items]']
id: row.id,
expand: ["certificate", "owner", "access_list.[clients,items]"],
});
})
.then((row) => {
// Configure nginx
return internalNginx.configure(proxyHostModel, 'proxy_host', row)
.then(() => {
return row;
});
return internalNginx.configure(proxyHostModel, "proxy_host", row).then(() => {
return row;
});
})
.then((row) => {
// Audit log
data.meta = _.assign({}, data.meta || {}, row.meta);
thisData.meta = _.assign({}, thisData.meta || {}, row.meta);
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'proxy-host',
object_id: row.id,
meta: data
})
return internalAuditLog
.add(access, {
action: "created",
object_type: "proxy-host",
object_id: row.id,
meta: thisData,
})
.then(() => {
return row;
});
@@ -115,100 +114,110 @@ const internalProxyHost = {
* @return {Promise}
*/
update: (access, data) => {
let create_certificate = data.certificate_id === 'new';
let thisData = data;
const createCertificate = thisData.certificate_id === "new";
if (create_certificate) {
delete data.certificate_id;
if (createCertificate) {
delete thisData.certificate_id;
}
return access.can('proxy_hosts:update', data.id)
return access
.can("proxy_hosts:update", thisData.id)
.then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records
let domain_name_check_promises = [];
const domain_name_check_promises = [];
if (typeof data.domain_names !== 'undefined') {
data.domain_names.map(function (domain_name) {
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'proxy', data.id));
if (typeof thisData.domain_names !== "undefined") {
thisData.domain_names.map((domain_name) => {
return domain_name_check_promises.push(
internalHost.isHostnameTaken(domain_name, "proxy", thisData.id),
);
});
return Promise.all(domain_name_check_promises)
.then((check_results) => {
check_results.map(function (result) {
if (result.is_taken) {
throw new error.ValidationError(result.hostname + ' is already in use');
}
});
return Promise.all(domain_name_check_promises).then((check_results) => {
check_results.map((result) => {
if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`);
}
return true;
});
});
}
})
.then(() => {
return internalProxyHost.get(access, {id: data.id});
return internalProxyHost.get(access, { id: thisData.id });
})
.then((row) => {
if (row.id !== data.id) {
if (row.id !== thisData.id) {
// Sanity check that something crazy hasn't happened
throw new error.InternalValidationError('Proxy Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
throw new errs.InternalValidationError(
`Proxy Host could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,
);
}
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, {
domain_names: data.domain_names || row.domain_names,
meta: _.assign({}, row.meta, data.meta)
})
if (createCertificate) {
return internalCertificate
.createQuickCertificate(access, {
domain_names: thisData.domain_names || row.domain_names,
meta: _.assign({}, row.meta, thisData.meta),
})
.then((cert) => {
// update host with cert id
data.certificate_id = cert.id;
thisData.certificate_id = cert.id;
})
.then(() => {
return row;
});
} else {
return row;
}
return row;
})
.then((row) => {
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
data = _.assign({}, {
domain_names: row.domain_names
}, data);
thisData = _.assign(
{},
{
domain_names: row.domain_names,
},
data,
);
data = internalHost.cleanSslHstsData(data, row);
thisData = internalHost.cleanSslHstsData(thisData, row);
return proxyHostModel
.query()
.where({id: data.id})
.patch(data)
.where({ id: thisData.id })
.patch(thisData)
.then(utils.omitRow(omissions()))
.then((saved_row) => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'proxy-host',
object_id: row.id,
meta: data
})
return internalAuditLog
.add(access, {
action: "updated",
object_type: "proxy-host",
object_id: row.id,
meta: thisData,
})
.then(() => {
return saved_row;
});
});
})
.then(() => {
return internalProxyHost.get(access, {
id: data.id,
expand: ['owner', 'certificate', 'access_list.[clients,items]']
})
return internalProxyHost
.get(access, {
id: thisData.id,
expand: ["owner", "certificate", "access_list.[clients,items]"],
})
.then((row) => {
if (!row.enabled) {
// No need to add nginx config if host is disabled
return row;
}
// Configure nginx
return internalNginx.configure(proxyHostModel, 'proxy_host', row)
.then((new_meta) => {
row.meta = new_meta;
row = internalHost.cleanRowCertificateMeta(row);
return _.omit(row, omissions());
});
return internalNginx.configure(proxyHostModel, "proxy_host", row).then((new_meta) => {
row.meta = new_meta;
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions());
});
});
});
},
@@ -222,39 +231,37 @@ const internalProxyHost = {
* @return {Promise}
*/
get: (access, data) => {
if (typeof data === 'undefined') {
data = {};
}
return access.can('proxy_hosts:get', data.id)
const thisData = data || {};
return access
.can("proxy_hosts:get", thisData.id)
.then((access_data) => {
let query = proxyHostModel
const query = proxyHostModel
.query()
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowGraph('[owner,access_list.[clients,items],certificate]')
.where("is_deleted", 0)
.andWhere("id", thisData.id)
.allowGraph(proxyHostModel.defaultAllowGraph)
.first();
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
if (access_data.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched('[' + data.expand.join(', ') + ']');
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
}
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
if (!row?.id) {
throw new errs.ItemNotFoundError(thisData.id);
}
row = internalHost.cleanRowCertificateMeta(row);
const thisRow = internalHost.cleanRowCertificateMeta(row);
// Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) {
row = _.omit(row, data.omit);
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) {
return _.omit(row, thisData.omit);
}
return row;
return thisRow;
});
},
@@ -266,35 +273,35 @@ const internalProxyHost = {
* @returns {Promise}
*/
delete: (access, data) => {
return access.can('proxy_hosts:delete', data.id)
return access
.can("proxy_hosts:delete", data.id)
.then(() => {
return internalProxyHost.get(access, {id: data.id});
return internalProxyHost.get(access, { id: data.id });
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
return proxyHostModel
.query()
.where('id', row.id)
.where("id", row.id)
.patch({
is_deleted: 1
is_deleted: 1,
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig('proxy_host', row)
.then(() => {
return internalNginx.reload();
});
return internalNginx.deleteConfig("proxy_host", row).then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'proxy-host',
object_id: row.id,
meta: _.omit(row, omissions())
action: "deleted",
object_type: "proxy-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
@@ -311,39 +318,41 @@ const internalProxyHost = {
* @returns {Promise}
*/
enable: (access, data) => {
return access.can('proxy_hosts:update', data.id)
return access
.can("proxy_hosts:update", data.id)
.then(() => {
return internalProxyHost.get(access, {
id: data.id,
expand: ['certificate', 'owner', 'access_list']
id: data.id,
expand: ["certificate", "owner", "access_list"],
});
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
} else if (row.enabled) {
throw new error.ValidationError('Host is already enabled');
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
throw new errs.ValidationError("Host is already enabled");
}
row.enabled = 1;
return proxyHostModel
.query()
.where('id', row.id)
.where("id", row.id)
.patch({
enabled: 1
enabled: 1,
})
.then(() => {
// Configure nginx
return internalNginx.configure(proxyHostModel, 'proxy_host', row);
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())
action: "enabled",
object_type: "proxy-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
@@ -360,39 +369,40 @@ const internalProxyHost = {
* @returns {Promise}
*/
disable: (access, data) => {
return access.can('proxy_hosts:update', data.id)
return access
.can("proxy_hosts:update", data.id)
.then(() => {
return internalProxyHost.get(access, {id: data.id});
return internalProxyHost.get(access, { id: data.id });
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
} else if (!row.enabled) {
throw new error.ValidationError('Host is already disabled');
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (!row.enabled) {
throw new errs.ValidationError("Host is already disabled");
}
row.enabled = 0;
return proxyHostModel
.query()
.where('id', row.id)
.where("id", row.id)
.patch({
enabled: 0
enabled: 0,
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig('proxy_host', row)
.then(() => {
return internalNginx.reload();
});
return internalNginx.deleteConfig("proxy_host", row).then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'disabled',
object_type: 'proxy-host',
object_id: row.id,
meta: _.omit(row, omissions())
action: "disabled",
object_type: "proxy-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
@@ -409,40 +419,36 @@ const internalProxyHost = {
* @param {String} [search_query]
* @returns {Promise}
*/
getAll: (access, expand, search_query) => {
return access.can('proxy_hosts:list')
.then((access_data) => {
let query = proxyHostModel
.query()
.where('is_deleted', 0)
.groupBy('id')
.allowGraph('[owner,access_list,certificate]')
.orderBy(castJsonIfNeed('domain_names'), 'ASC');
getAll: async (access, expand, searchQuery) => {
const accessData = await access.can("proxy_hosts:list");
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
}
const query = proxyHostModel
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph(proxyHostModel.defaultAllowGraph)
.orderBy(castJsonIfNeed("domain_names"), "ASC");
// Query is used for searching
if (typeof search_query === 'string' && search_query.length > 0) {
query.where(function () {
this.where(castJsonIfNeed('domain_names'), 'like', `%${search_query}%`);
});
}
if (accessData.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched('[' + expand.join(', ') + ']');
}
return query.then(utils.omitRows(omissions()));
})
.then((rows) => {
if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {
return internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
// Query is used for searching
if (typeof searchQuery === "string" && searchQuery.length > 0) {
query.where(function () {
this.where(castJsonIfNeed("domain_names"), "like", `%${searchQuery}%`);
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
const rows = await query.then(utils.omitRows(omissions()));
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
return internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
},
/**
@@ -453,20 +459,16 @@ const internalProxyHost = {
* @returns {Promise}
*/
getCount: (user_id, visibility) => {
let query = proxyHostModel
.query()
.count('id as count')
.where('is_deleted', 0);
const query = proxyHostModel.query().count("id as count").where("is_deleted", 0);
if (visibility !== 'all') {
query.andWhere('owner_user_id', user_id);
if (visibility !== "all") {
query.andWhere("owner_user_id", user_id);
}
return query.first()
.then((row) => {
return parseInt(row.count, 10);
});
}
return query.first().then((row) => {
return Number.parseInt(row.count, 10);
});
},
};
module.exports = internalProxyHost;
export default internalProxyHost;
+196 -184
View File
@@ -1,73 +1,73 @@
const _ = require('lodash');
const error = require('../lib/error');
const utils = require('../lib/utils');
const redirectionHostModel = require('../models/redirection_host');
const internalHost = require('./host');
const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log');
const internalCertificate = require('./certificate');
const {castJsonIfNeed} = require('../lib/helpers');
import _ from "lodash";
import errs from "../lib/error.js";
import { castJsonIfNeed } from "../lib/helpers.js";
import utils from "../lib/utils.js";
import redirectionHostModel from "../models/redirection_host.js";
import internalAuditLog from "./audit-log.js";
import internalCertificate from "./certificate.js";
import internalHost from "./host.js";
import internalNginx from "./nginx.js";
function omissions () {
return ['is_deleted'];
}
const omissions = () => {
return ["is_deleted"];
};
const internalRedirectionHost = {
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: (access, data) => {
let create_certificate = data.certificate_id === 'new';
let thisData = data || {};
const createCertificate = thisData.certificate_id === "new";
if (create_certificate) {
delete data.certificate_id;
if (createCertificate) {
delete thisData.certificate_id;
}
return access.can('redirection_hosts:create', data)
return access
.can("redirection_hosts:create", thisData)
.then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records
let domain_name_check_promises = [];
const domain_name_check_promises = [];
data.domain_names.map(function (domain_name) {
thisData.domain_names.map((domain_name) => {
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));
return true;
});
return Promise.all(domain_name_check_promises)
.then((check_results) => {
check_results.map(function (result) {
if (result.is_taken) {
throw new error.ValidationError(result.hostname + ' is already in use');
}
});
return Promise.all(domain_name_check_promises).then((check_results) => {
check_results.map((result) => {
if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`);
}
return true;
});
});
})
.then(() => {
// At this point the domains should have been checked
data.owner_user_id = access.token.getUserId(1);
data = internalHost.cleanSslHstsData(data);
thisData.owner_user_id = access.token.getUserId(1);
thisData = internalHost.cleanSslHstsData(thisData);
// Fix for db field not having a default value
// for this optional field.
if (typeof data.advanced_config === 'undefined') {
data.advanced_config = '';
if (typeof data.advanced_config === "undefined") {
data.advanced_config = "";
}
return redirectionHostModel
.query()
.insertAndFetch(data)
.then(utils.omitRow(omissions()));
return redirectionHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
})
.then((row) => {
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, data)
if (createCertificate) {
return internalCertificate
.createQuickCertificate(access, thisData)
.then((cert) => {
// update host with cert id
return internalRedirectionHost.update(access, {
id: row.id,
certificate_id: cert.id
id: row.id,
certificate_id: cert.id,
});
})
.then(() => {
@@ -79,27 +79,27 @@ const internalRedirectionHost = {
.then((row) => {
// re-fetch with cert
return internalRedirectionHost.get(access, {
id: row.id,
expand: ['certificate', 'owner']
id: row.id,
expand: ["certificate", "owner"],
});
})
.then((row) => {
// Configure nginx
return internalNginx.configure(redirectionHostModel, 'redirection_host', row)
.then(() => {
return row;
});
return internalNginx.configure(redirectionHostModel, "redirection_host", row).then(() => {
return row;
});
})
.then((row) => {
data.meta = _.assign({}, data.meta || {}, row.meta);
thisData.meta = _.assign({}, thisData.meta || {}, row.meta);
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'redirection-host',
object_id: row.id,
meta: data
})
return internalAuditLog
.add(access, {
action: "created",
object_type: "redirection-host",
object_id: row.id,
meta: thisData,
})
.then(() => {
return row;
});
@@ -113,94 +113,107 @@ const internalRedirectionHost = {
* @return {Promise}
*/
update: (access, data) => {
let create_certificate = data.certificate_id === 'new';
let thisData = data || {};
const createCertificate = thisData.certificate_id === "new";
if (create_certificate) {
delete data.certificate_id;
if (createCertificate) {
delete thisData.certificate_id;
}
return access.can('redirection_hosts:update', data.id)
return access
.can("redirection_hosts:update", thisData.id)
.then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records
let domain_name_check_promises = [];
const domain_name_check_promises = [];
if (typeof data.domain_names !== 'undefined') {
data.domain_names.map(function (domain_name) {
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'redirection', data.id));
if (typeof thisData.domain_names !== "undefined") {
thisData.domain_names.map((domain_name) => {
domain_name_check_promises.push(
internalHost.isHostnameTaken(domain_name, "redirection", thisData.id),
);
return true;
});
return Promise.all(domain_name_check_promises)
.then((check_results) => {
check_results.map(function (result) {
if (result.is_taken) {
throw new error.ValidationError(result.hostname + ' is already in use');
}
});
return Promise.all(domain_name_check_promises).then((check_results) => {
check_results.map((result) => {
if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`);
}
return true;
});
});
}
})
.then(() => {
return internalRedirectionHost.get(access, {id: data.id});
return internalRedirectionHost.get(access, { id: thisData.id });
})
.then((row) => {
if (row.id !== data.id) {
if (row.id !== thisData.id) {
// Sanity check that something crazy hasn't happened
throw new error.InternalValidationError('Redirection Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
throw new errs.InternalValidationError(
`Redirection Host could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,
);
}
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, {
domain_names: data.domain_names || row.domain_names,
meta: _.assign({}, row.meta, data.meta)
})
if (createCertificate) {
return internalCertificate
.createQuickCertificate(access, {
domain_names: thisData.domain_names || row.domain_names,
meta: _.assign({}, row.meta, thisData.meta),
})
.then((cert) => {
// update host with cert id
data.certificate_id = cert.id;
thisData.certificate_id = cert.id;
})
.then(() => {
return row;
});
} else {
return row;
}
return row;
})
.then((row) => {
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
data = _.assign({}, {
domain_names: row.domain_names
}, data);
thisData = _.assign(
{},
{
domain_names: row.domain_names,
},
thisData,
);
data = internalHost.cleanSslHstsData(data, row);
thisData = internalHost.cleanSslHstsData(thisData, row);
return redirectionHostModel
.query()
.where({id: data.id})
.patch(data)
.where({ id: thisData.id })
.patch(thisData)
.then((saved_row) => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'redirection-host',
object_id: row.id,
meta: data
})
return internalAuditLog
.add(access, {
action: "updated",
object_type: "redirection-host",
object_id: row.id,
meta: thisData,
})
.then(() => {
return _.omit(saved_row, omissions());
});
});
})
.then(() => {
return internalRedirectionHost.get(access, {
id: data.id,
expand: ['owner', 'certificate']
})
return internalRedirectionHost
.get(access, {
id: thisData.id,
expand: ["owner", "certificate"],
})
.then((row) => {
// Configure nginx
return internalNginx.configure(redirectionHostModel, 'redirection_host', row)
return internalNginx
.configure(redirectionHostModel, "redirection_host", row)
.then((new_meta) => {
row.meta = new_meta;
row = internalHost.cleanRowCertificateMeta(row);
return _.omit(row, omissions());
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions());
});
});
});
@@ -215,39 +228,38 @@ const internalRedirectionHost = {
* @return {Promise}
*/
get: (access, data) => {
if (typeof data === 'undefined') {
data = {};
}
return access.can('redirection_hosts:get', data.id)
const thisData = data || {};
return access
.can("redirection_hosts:get", thisData.id)
.then((access_data) => {
let query = redirectionHostModel
const query = redirectionHostModel
.query()
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowGraph('[owner,certificate]')
.where("is_deleted", 0)
.andWhere("id", thisData.id)
.allowGraph(redirectionHostModel.defaultAllowGraph)
.first();
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
if (access_data.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched('[' + data.expand.join(', ') + ']');
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
}
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
let thisRow = row;
if (!thisRow?.id) {
throw new errs.ItemNotFoundError(thisData.id);
}
row = internalHost.cleanRowCertificateMeta(row);
thisRow = internalHost.cleanRowCertificateMeta(thisRow);
// Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) {
row = _.omit(row, data.omit);
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) {
return _.omit(thisRow, thisData.omit);
}
return row;
return thisRow;
});
},
@@ -259,35 +271,35 @@ const internalRedirectionHost = {
* @returns {Promise}
*/
delete: (access, data) => {
return access.can('redirection_hosts:delete', data.id)
return access
.can("redirection_hosts:delete", data.id)
.then(() => {
return internalRedirectionHost.get(access, {id: data.id});
return internalRedirectionHost.get(access, { id: data.id });
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
return redirectionHostModel
.query()
.where('id', row.id)
.where("id", row.id)
.patch({
is_deleted: 1
is_deleted: 1,
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig('redirection_host', row)
.then(() => {
return internalNginx.reload();
});
return internalNginx.deleteConfig("redirection_host", row).then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'redirection-host',
object_id: row.id,
meta: _.omit(row, omissions())
action: "deleted",
object_type: "redirection-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
@@ -304,39 +316,41 @@ const internalRedirectionHost = {
* @returns {Promise}
*/
enable: (access, data) => {
return access.can('redirection_hosts:update', data.id)
return access
.can("redirection_hosts:update", data.id)
.then(() => {
return internalRedirectionHost.get(access, {
id: data.id,
expand: ['certificate', 'owner']
id: data.id,
expand: ["certificate", "owner"],
});
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
} else if (row.enabled) {
throw new error.ValidationError('Host is already enabled');
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
throw new errs.ValidationError("Host is already enabled");
}
row.enabled = 1;
return redirectionHostModel
.query()
.where('id', row.id)
.where("id", row.id)
.patch({
enabled: 1
enabled: 1,
})
.then(() => {
// Configure nginx
return internalNginx.configure(redirectionHostModel, 'redirection_host', row);
return internalNginx.configure(redirectionHostModel, "redirection_host", row);
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'enabled',
object_type: 'redirection-host',
object_id: row.id,
meta: _.omit(row, omissions())
action: "enabled",
object_type: "redirection-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
@@ -353,39 +367,40 @@ const internalRedirectionHost = {
* @returns {Promise}
*/
disable: (access, data) => {
return access.can('redirection_hosts:update', data.id)
return access
.can("redirection_hosts:update", data.id)
.then(() => {
return internalRedirectionHost.get(access, {id: data.id});
return internalRedirectionHost.get(access, { id: data.id });
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
} else if (!row.enabled) {
throw new error.ValidationError('Host is already disabled');
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (!row.enabled) {
throw new errs.ValidationError("Host is already disabled");
}
row.enabled = 0;
return redirectionHostModel
.query()
.where('id', row.id)
.where("id", row.id)
.patch({
enabled: 0
enabled: 0,
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig('redirection_host', row)
.then(() => {
return internalNginx.reload();
});
return internalNginx.deleteConfig("redirection_host", row).then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'disabled',
object_type: 'redirection-host',
object_id: row.id,
meta: _.omit(row, omissions())
action: "disabled",
object_type: "redirection-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
@@ -403,34 +418,35 @@ const internalRedirectionHost = {
* @returns {Promise}
*/
getAll: (access, expand, search_query) => {
return access.can('redirection_hosts:list')
return access
.can("redirection_hosts:list")
.then((access_data) => {
let query = redirectionHostModel
const query = redirectionHostModel
.query()
.where('is_deleted', 0)
.groupBy('id')
.allowGraph('[owner,certificate]')
.orderBy(castJsonIfNeed('domain_names'), 'ASC');
.where("is_deleted", 0)
.groupBy("id")
.allowGraph(redirectionHostModel.defaultAllowGraph)
.orderBy(castJsonIfNeed("domain_names"), "ASC");
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
if (access_data.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
// Query is used for searching
if (typeof search_query === 'string' && search_query.length > 0) {
if (typeof search_query === "string" && search_query.length > 0) {
query.where(function () {
this.where(castJsonIfNeed('domain_names'), 'like', `%${search_query}%`);
this.where(castJsonIfNeed("domain_names"), "like", `%${search_query}%`);
});
}
if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched('[' + expand.join(', ') + ']');
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
return query.then(utils.omitRows(omissions()));
})
.then((rows) => {
if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
return internalHost.cleanAllRowsCertificateMeta(rows);
}
@@ -446,20 +462,16 @@ const internalRedirectionHost = {
* @returns {Promise}
*/
getCount: (user_id, visibility) => {
let query = redirectionHostModel
.query()
.count('id as count')
.where('is_deleted', 0);
const query = redirectionHostModel.query().count("id as count").where("is_deleted", 0);
if (visibility !== 'all') {
query.andWhere('owner_user_id', user_id);
if (visibility !== "all") {
query.andWhere("owner_user_id", user_id);
}
return query.first()
.then((row) => {
return parseInt(row.count, 10);
});
}
return query.first().then((row) => {
return Number.parseInt(row.count, 10);
});
},
};
module.exports = internalRedirectionHost;
export default internalRedirectionHost;
+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;
+17 -18
View File
@@ -1,38 +1,37 @@
const internalProxyHost = require('./proxy-host');
const internalRedirectionHost = require('./redirection-host');
const internalDeadHost = require('./dead-host');
const internalStream = require('./stream');
import internalDeadHost from "./dead-host.js";
import internalProxyHost from "./proxy-host.js";
import internalRedirectionHost from "./redirection-host.js";
import internalStream from "./stream.js";
const internalReport = {
/**
* @param {Access} access
* @return {Promise}
*/
getHostsReport: (access) => {
return access.can('reports:hosts', 1)
return access
.can("reports:hosts", 1)
.then((access_data) => {
let user_id = access.token.getUserId(1);
const userId = access.token.getUserId(1);
let promises = [
internalProxyHost.getCount(user_id, access_data.visibility),
internalRedirectionHost.getCount(user_id, access_data.visibility),
internalStream.getCount(user_id, access_data.visibility),
internalDeadHost.getCount(user_id, access_data.visibility)
const promises = [
internalProxyHost.getCount(userId, access_data.permission_visibility),
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);
})
.then((counts) => {
return {
proxy: counts.shift(),
proxy: counts.shift(),
redirection: counts.shift(),
stream: counts.shift(),
dead: counts.shift()
stream: counts.shift(),
dead: counts.shift(),
};
});
}
},
};
module.exports = internalReport;
export default internalReport;
+35 -43
View File
@@ -1,10 +1,9 @@
const fs = require('fs');
const error = require('../lib/error');
const settingModel = require('../models/setting');
const internalNginx = require('./nginx');
import fs from "node:fs";
import errs from "../lib/error.js";
import settingModel from "../models/setting.js";
import internalNginx from "./nginx.js";
const internalSetting = {
/**
* @param {Access} access
* @param {Object} data
@@ -12,37 +11,38 @@ const internalSetting = {
* @return {Promise}
*/
update: (access, data) => {
return access.can('settings:update', data.id)
return access
.can("settings:update", data.id)
.then((/*access_data*/) => {
return internalSetting.get(access, {id: data.id});
return internalSetting.get(access, { id: data.id });
})
.then((row) => {
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new error.InternalValidationError('Setting could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
throw new errs.InternalValidationError(
`Setting could not be updated, IDs do not match: ${row.id} !== ${data.id}`,
);
}
return settingModel
.query()
.where({id: data.id})
.patch(data);
return settingModel.query().where({ id: data.id }).patch(data);
})
.then(() => {
return internalSetting.get(access, {
id: data.id
id: data.id,
});
})
.then((row) => {
if (row.id === 'default-site') {
if (row.id === "default-site") {
// write the html if we need to
if (row.value === 'html') {
fs.writeFileSync('/data/nginx/default_www/index.html', row.meta.html, {encoding: 'utf8'});
if (row.value === "html") {
fs.writeFileSync("/data/nginx/default_www/index.html", row.meta.html, { encoding: "utf8" });
}
// Configure nginx
return internalNginx.deleteConfig('default')
return internalNginx
.deleteConfig("default")
.then(() => {
return internalNginx.generateConfig('default', row);
return internalNginx.generateConfig("default", row);
})
.then(() => {
return internalNginx.test();
@@ -54,7 +54,8 @@ const internalSetting = {
return row;
})
.catch((/*err*/) => {
internalNginx.deleteConfig('default')
internalNginx
.deleteConfig("default")
.then(() => {
return internalNginx.test();
})
@@ -63,12 +64,11 @@ const internalSetting = {
})
.then(() => {
// I'm being slack here I know..
throw new error.ValidationError('Could not reconfigure Nginx. Please check logs.');
throw new errs.ValidationError("Could not reconfigure Nginx. Please check logs.");
});
});
} else {
return row;
}
return row;
});
},
@@ -79,19 +79,16 @@ const internalSetting = {
* @return {Promise}
*/
get: (access, data) => {
return access.can('settings:get', data.id)
return access
.can("settings:get", data.id)
.then(() => {
return settingModel
.query()
.where('id', data.id)
.first();
return settingModel.query().where("id", data.id).first();
})
.then((row) => {
if (row) {
return row;
} else {
throw new error.ItemNotFoundError(data.id);
}
throw new errs.ItemNotFoundError(data.id);
});
},
@@ -102,15 +99,13 @@ const internalSetting = {
* @returns {*}
*/
getCount: (access) => {
return access.can('settings:list')
return access
.can("settings:list")
.then(() => {
return settingModel
.query()
.count('id as count')
.first();
return settingModel.query().count("id as count").first();
})
.then((row) => {
return parseInt(row.count, 10);
return Number.parseInt(row.count, 10);
});
},
@@ -121,13 +116,10 @@ const internalSetting = {
* @returns {Promise}
*/
getAll: (access) => {
return access.can('settings:list')
.then(() => {
return settingModel
.query()
.orderBy('description', 'ASC');
});
}
return access.can("settings:list").then(() => {
return settingModel.query().orderBy("description", "ASC");
});
},
};
module.exports = internalSetting;
export default internalSetting;
+208 -129
View File
@@ -1,52 +1,85 @@
const _ = require('lodash');
const error = require('../lib/error');
const utils = require('../lib/utils');
const streamModel = require('../models/stream');
const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log');
const {castJsonIfNeed} = require('../lib/helpers');
import _ from "lodash";
import errs from "../lib/error.js";
import { castJsonIfNeed } from "../lib/helpers.js";
import utils from "../lib/utils.js";
import streamModel from "../models/stream.js";
import internalAuditLog from "./audit-log.js";
import internalCertificate from "./certificate.js";
import internalHost from "./host.js";
import internalNginx from "./nginx.js";
function omissions () {
return ['is_deleted'];
}
const omissions = () => {
return ["is_deleted", "owner.is_deleted", "certificate.is_deleted"];
};
const internalStream = {
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: (access, data) => {
return access.can('streams:create', data)
const create_certificate = data.certificate_id === "new";
if (create_certificate) {
delete data.certificate_id;
}
return access
.can("streams:create", data)
.then((/*access_data*/) => {
// TODO: At this point the existing ports should have been checked
data.owner_user_id = access.token.getUserId(1);
if (typeof data.meta === 'undefined') {
if (typeof data.meta === "undefined") {
data.meta = {};
}
return streamModel
.query()
.insertAndFetch(data)
.then(utils.omitRow(omissions()));
// streams aren't routed by domain name so don't store domain names in the DB
const data_no_domains = structuredClone(data);
delete data_no_domains.domain_names;
return streamModel.query().insertAndFetch(data_no_domains).then(utils.omitRow(omissions()));
})
.then((row) => {
if (create_certificate) {
return internalCertificate
.createQuickCertificate(access, data)
.then((cert) => {
// update host with cert id
return internalStream.update(access, {
id: row.id,
certificate_id: cert.id,
});
})
.then(() => {
return row;
});
}
return row;
})
.then((row) => {
// re-fetch with cert
return internalStream.get(access, {
id: row.id,
expand: ["certificate", "owner"],
});
})
.then((row) => {
// Configure nginx
return internalNginx.configure(streamModel, 'stream', row)
.then(() => {
return internalStream.get(access, {id: row.id, expand: ['owner']});
});
return internalNginx.configure(streamModel, "stream", row).then(() => {
return row;
});
})
.then((row) => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'stream',
object_id: row.id,
meta: data
})
return internalAuditLog
.add(access, {
action: "created",
object_type: "stream",
object_id: row.id,
meta: data,
})
.then(() => {
return row;
});
@@ -60,39 +93,78 @@ const internalStream = {
* @return {Promise}
*/
update: (access, data) => {
return access.can('streams:update', data.id)
let thisData = data;
const create_certificate = thisData.certificate_id === "new";
if (create_certificate) {
delete thisData.certificate_id;
}
return access
.can("streams:update", thisData.id)
.then((/*access_data*/) => {
// TODO: at this point the existing streams should have been checked
return internalStream.get(access, {id: data.id});
return internalStream.get(access, { id: thisData.id });
})
.then((row) => {
if (row.id !== data.id) {
if (row.id !== thisData.id) {
// Sanity check that something crazy hasn't happened
throw new error.InternalValidationError('Stream could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
throw new errs.InternalValidationError(
`Stream could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,
);
}
if (create_certificate) {
return internalCertificate
.createQuickCertificate(access, {
domain_names: thisData.domain_names || row.domain_names,
meta: _.assign({}, row.meta, thisData.meta),
})
.then((cert) => {
// update host with cert id
thisData.certificate_id = cert.id;
})
.then(() => {
return row;
});
}
return row;
})
.then((row) => {
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
thisData = _.assign(
{},
{
domain_names: row.domain_names,
},
thisData,
);
return streamModel
.query()
.patchAndFetchById(row.id, data)
.patchAndFetchById(row.id, thisData)
.then(utils.omitRow(omissions()))
.then((saved_row) => {
return internalNginx.configure(streamModel, 'stream', saved_row)
.then(() => {
return internalStream.get(access, {id: row.id, expand: ['owner']});
});
})
.then((saved_row) => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'stream',
object_id: row.id,
meta: data
})
return internalAuditLog
.add(access, {
action: "updated",
object_type: "stream",
object_id: row.id,
meta: thisData,
})
.then(() => {
return saved_row;
});
});
})
.then(() => {
return internalStream.get(access, { id: thisData.id, expand: ["owner", "certificate"] }).then((row) => {
return internalNginx.configure(streamModel, "stream", row).then((new_meta) => {
row.meta = new_meta;
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions());
});
});
});
},
@@ -105,38 +177,38 @@ const internalStream = {
* @return {Promise}
*/
get: (access, data) => {
if (typeof data === 'undefined') {
data = {};
}
return access.can('streams:get', data.id)
const thisData = data || {};
return access
.can("streams:get", thisData.id)
.then((access_data) => {
let query = streamModel
const query = streamModel
.query()
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowGraph('[owner]')
.where("is_deleted", 0)
.andWhere("id", thisData.id)
.allowGraph(streamModel.defaultAllowGraph)
.first();
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
if (access_data.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched('[' + data.expand.join(', ') + ']');
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
}
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
let thisRow = row;
if (!thisRow?.id) {
throw new errs.ItemNotFoundError(thisData.id);
}
thisRow = internalHost.cleanRowCertificateMeta(thisRow);
// Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) {
row = _.omit(row, data.omit);
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) {
return _.omit(thisRow, thisData.omit);
}
return row;
return thisRow;
});
},
@@ -148,35 +220,35 @@ const internalStream = {
* @returns {Promise}
*/
delete: (access, data) => {
return access.can('streams:delete', data.id)
return access
.can("streams:delete", data.id)
.then(() => {
return internalStream.get(access, {id: data.id});
return internalStream.get(access, { id: data.id });
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
return streamModel
.query()
.where('id', row.id)
.where("id", row.id)
.patch({
is_deleted: 1
is_deleted: 1,
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig('stream', row)
.then(() => {
return internalNginx.reload();
});
return internalNginx.deleteConfig("stream", row).then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'stream',
object_id: row.id,
meta: _.omit(row, omissions())
action: "deleted",
object_type: "stream",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
@@ -193,39 +265,41 @@ const internalStream = {
* @returns {Promise}
*/
enable: (access, data) => {
return access.can('streams:update', data.id)
return access
.can("streams:update", data.id)
.then(() => {
return internalStream.get(access, {
id: data.id,
expand: ['owner']
id: data.id,
expand: ["certificate", "owner"],
});
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
} else if (row.enabled) {
throw new error.ValidationError('Host is already enabled');
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
throw new errs.ValidationError("Stream is already enabled");
}
row.enabled = 1;
return streamModel
.query()
.where('id', row.id)
.where("id", row.id)
.patch({
enabled: 1
enabled: 1,
})
.then(() => {
// Configure nginx
return internalNginx.configure(streamModel, 'stream', row);
return internalNginx.configure(streamModel, "stream", row);
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'enabled',
object_type: 'stream',
object_id: row.id,
meta: _.omit(row, omissions())
action: "enabled",
object_type: "stream",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
@@ -242,39 +316,40 @@ const internalStream = {
* @returns {Promise}
*/
disable: (access, data) => {
return access.can('streams:update', data.id)
return access
.can("streams:update", data.id)
.then(() => {
return internalStream.get(access, {id: data.id});
return internalStream.get(access, { id: data.id });
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
} else if (!row.enabled) {
throw new error.ValidationError('Host is already disabled');
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (!row.enabled) {
throw new errs.ValidationError("Stream is already disabled");
}
row.enabled = 0;
return streamModel
.query()
.where('id', row.id)
.where("id", row.id)
.patch({
enabled: 0
enabled: 0,
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig('stream', row)
.then(() => {
return internalNginx.reload();
});
return internalNginx.deleteConfig("stream", row).then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'disabled',
object_type: 'stream-host',
object_id: row.id,
meta: _.omit(row, omissions())
action: "disabled",
object_type: "stream",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
@@ -292,31 +367,39 @@ const internalStream = {
* @returns {Promise}
*/
getAll: (access, expand, search_query) => {
return access.can('streams:list')
return access
.can("streams:list")
.then((access_data) => {
const query = streamModel
.query()
.where('is_deleted', 0)
.groupBy('id')
.allowGraph('[owner]')
.orderByRaw('CAST(incoming_port AS INTEGER) ASC');
.where("is_deleted", 0)
.groupBy("id")
.allowGraph(streamModel.defaultAllowGraph)
.orderBy("incoming_port", "ASC");
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
if (access_data.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
// Query is used for searching
if (typeof search_query === 'string' && search_query.length > 0) {
if (typeof search_query === "string" && search_query.length > 0) {
query.where(function () {
this.where(castJsonIfNeed('incoming_port'), 'like', `%${search_query}%`);
this.where(castJsonIfNeed("incoming_port"), "like", `%${search_query}%`);
});
}
if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched('[' + expand.join(', ') + ']');
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
return query.then(utils.omitRows(omissions()));
})
.then((rows) => {
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
return internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
});
},
@@ -328,20 +411,16 @@ const internalStream = {
* @returns {Promise}
*/
getCount: (user_id, visibility) => {
const query = streamModel
.query()
.count('id AS count')
.where('is_deleted', 0);
const query = streamModel.query().count("id AS count").where("is_deleted", 0);
if (visibility !== 'all') {
query.andWhere('owner_user_id', user_id);
if (visibility !== "all") {
query.andWhere("owner_user_id", user_id);
}
return query.first()
.then((row) => {
return parseInt(row.count, 10);
});
}
return query.first().then((row) => {
return Number.parseInt(row.count, 10);
});
},
};
module.exports = internalStream;
export default internalStream;
+185 -112
View File
@@ -1,14 +1,17 @@
const _ = require('lodash');
const error = require('../lib/error');
const userModel = require('../models/user');
const authModel = require('../models/auth');
const helpers = require('../lib/helpers');
const TokenModel = require('../models/token');
import _ from "lodash";
import errs from "../lib/error.js";
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';
module.exports = {
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 {
/**
* @param {Object} data
* @param {String} data.identity
@@ -18,70 +21,85 @@ module.exports = {
* @param {String} [issuer]
* @returns {Promise}
*/
getTokenFromEmail: (data, issuer) => {
let Token = new TokenModel();
getTokenFromEmail: async (data, issuer) => {
const Token = TokenModel();
data.scope = data.scope || 'user';
data.expiry = data.expiry || '1d';
data.scope = data.scope || "user";
data.expiry = data.expiry || "1d";
return userModel
const user = await userModel
.query()
.where('email', data.identity.toLowerCase().trim())
.andWhere('is_deleted', 0)
.andWhere('is_disabled', 0)
.first()
.then((user) => {
if (user) {
// Get auth
return authModel
.query()
.where('user_id', '=', user.id)
.where('type', '=', 'password')
.first()
.then((auth) => {
if (auth) {
return auth.verifyPassword(data.secret)
.then((valid) => {
if (valid) {
.where("email", data.identity.toLowerCase().trim())
.andWhere("is_deleted", 0)
.andWhere("is_disabled", 0)
.first();
if (data.scope !== 'user' && _.indexOf(user.roles, data.scope) === -1) {
// The scope requested doesn't exist as a role against the user,
// you shall not pass.
throw new error.AuthError('Invalid scope: ' + data.scope);
}
if (!user) {
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
}
// Create a moment of the expiry expression
let expiry = helpers.parseDatePeriod(data.expiry);
if (expiry === null) {
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
}
const auth = await authModel
.query()
.where("user_id", "=", user.id)
.where("type", "=", "password")
.first();
return Token.create({
iss: issuer || 'api',
attrs: {
id: user.id
},
scope: [data.scope],
expiresIn: data.expiry
})
.then((signed) => {
return {
token: signed.token,
expires: expiry.toISOString()
};
});
} else {
throw new error.AuthError(ERROR_MESSAGE_INVALID_AUTH);
}
});
} else {
throw new error.AuthError(ERROR_MESSAGE_INVALID_AUTH);
}
});
} else {
throw new error.AuthError(ERROR_MESSAGE_INVALID_AUTH);
}
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,
);
}
if (data.scope !== "user" && _.indexOf(user.roles, data.scope) === -1) {
// The scope requested doesn't exist as a role against the user,
// you shall not pass.
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) {
throw new errs.AuthError(`Invalid expiry time: ${data.expiry}`);
}
const signed = await Token.create({
iss: issuer || "api",
attrs: {
id: user.id,
},
scope: [data.scope],
expiresIn: data.expiry,
});
return {
token: signed.token,
expires: expiry.toISOString(),
};
},
/**
@@ -91,74 +109,129 @@ module.exports = {
* @param {String} [data.scope] Only considered if existing token scope is admin
* @returns {Promise}
*/
getFreshToken: (access, data) => {
let Token = new TokenModel();
getFreshToken: async (access, data) => {
const Token = TokenModel();
const thisData = data || {};
data = data || {};
data.expiry = data.expiry || '1d';
if (access && access.token.getUserId(0)) {
thisData.expiry = thisData.expiry || "1d";
if (access?.token.getUserId(0)) {
// Create a moment of the expiry expression
let expiry = helpers.parseDatePeriod(data.expiry);
const expiry = parseDatePeriod(thisData.expiry);
if (expiry === null) {
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
throw new errs.AuthError(`Invalid expiry time: ${thisData.expiry}`);
}
let token_attrs = {
id: access.token.getUserId(0)
const token_attrs = {
id: access.token.getUserId(0),
};
// Only admins can request otherwise scoped tokens
let scope = access.token.get('scope');
if (data.scope && access.token.hasScope('admin')) {
scope = [data.scope];
let scope = access.token.get("scope");
if (thisData.scope && access.token.hasScope("admin")) {
scope = [thisData.scope];
if (data.scope === 'job-board' || data.scope === 'worker') {
if (thisData.scope === "job-board" || thisData.scope === "worker") {
token_attrs.id = 0;
}
}
return Token.create({
iss: 'api',
scope: scope,
attrs: token_attrs,
expiresIn: data.expiry
})
.then((signed) => {
return {
token: signed.token,
expires: expiry.toISOString()
};
});
} else {
throw new error.AssertionFailedError('Existing token contained invalid user data');
const signed = await Token.create({
iss: "api",
scope: scope,
attrs: token_attrs,
expiresIn: thisData.expiry,
});
return {
token: signed.token,
expires: expiry.toISOString(),
};
}
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 || 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}
*/
getTokenFromUser: (user) => {
const expire = '1d';
const Token = new TokenModel();
const expiry = helpers.parseDatePeriod(expire);
getTokenFromUser: async (user) => {
const expire = "1d";
const Token = TokenModel();
const expiry = parseDatePeriod(expire);
return Token.create({
iss: 'api',
const signed = await Token.create({
iss: "api",
attrs: {
id: user.id
id: user.id,
},
scope: ['user'],
expiresIn: expire
})
.then((signed) => {
return {
token: signed.token,
expires: expiry.toISOString(),
user: user
};
});
}
scope: ["user"],
expiresIn: expire,
});
return {
token: signed.token,
expires: expiry.toISOString(),
user: user,
};
},
};
+228 -241
View File
@@ -1,93 +1,76 @@
const _ = require('lodash');
const error = require('../lib/error');
const utils = require('../lib/utils');
const userModel = require('../models/user');
const userPermissionModel = require('../models/user_permission');
const authModel = require('../models/auth');
const gravatar = require('gravatar');
const internalToken = require('./token');
const internalAuditLog = require('./audit-log');
import gravatar from "gravatar";
import _ from "lodash";
import errs from "../lib/error.js";
import utils from "../lib/utils.js";
import authModel from "../models/auth.js";
import userModel from "../models/user.js";
import userPermissionModel from "../models/user_permission.js";
import internalAuditLog from "./audit-log.js";
import internalToken from "./token.js";
function omissions () {
return ['is_deleted'];
}
const omissions = () => {
return ["is_deleted", "permissions.id", "permissions.user_id", "permissions.created_on", "permissions.modified_on"];
};
const DEFAULT_AVATAR = gravatar.url("admin@example.com", { default: "mm" });
const internalUser = {
/**
* Create a user can happen unauthenticated only once and only when no active users exist.
* Otherwise, a valid auth method is required.
*
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: (access, data) => {
let auth = data.auth || null;
create: async (access, data) => {
const auth = data.auth || null;
delete data.auth;
data.avatar = data.avatar || '';
data.roles = data.roles || [];
data.avatar = data.avatar || "";
data.roles = data.roles || [];
if (typeof data.is_disabled !== 'undefined') {
if (typeof data.is_disabled !== "undefined") {
data.is_disabled = data.is_disabled ? 1 : 0;
}
return access.can('users:create', data)
.then(() => {
data.avatar = gravatar.url(data.email, {default: 'mm'});
await access.can("users:create", data);
data.avatar = gravatar.url(data.email, { default: "mm" });
return userModel
.query()
.insertAndFetch(data)
.then(utils.omitRow(omissions()));
})
.then((user) => {
if (auth) {
return authModel
.query()
.insert({
user_id: user.id,
type: auth.type,
secret: auth.secret,
meta: {}
})
.then(() => {
return user;
});
} else {
return user;
}
})
.then((user) => {
// Create permissions row as well
let is_admin = data.roles.indexOf('admin') !== -1;
return userPermissionModel
.query()
.insert({
user_id: user.id,
visibility: is_admin ? 'all' : 'user',
proxy_hosts: 'manage',
redirection_hosts: 'manage',
dead_hosts: 'manage',
streams: 'manage',
access_lists: 'manage',
certificates: 'manage'
})
.then(() => {
return internalUser.get(access, {id: user.id, expand: ['permissions']});
});
})
.then((user) => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'user',
object_id: user.id,
meta: user
})
.then(() => {
return user;
});
let user = await userModel.query().insertAndFetch(data).then(utils.omitRow(omissions()));
if (auth) {
user = await authModel.query().insert({
user_id: user.id,
type: auth.type,
secret: auth.secret,
meta: {},
});
}
// Create permissions row as well
const isAdmin = data.roles.indexOf("admin") !== -1;
await userPermissionModel.query().insert({
user_id: user.id,
visibility: isAdmin ? "all" : "user",
proxy_hosts: "manage",
redirection_hosts: "manage",
dead_hosts: "manage",
streams: "manage",
access_lists: "manage",
certificates: "manage",
});
user = await internalUser.get(access, { id: user.id, expand: ["permissions"] });
await internalAuditLog.add(access, {
action: "created",
object_type: "user",
object_id: user.id,
meta: user,
});
return user;
},
/**
@@ -99,62 +82,63 @@ const internalUser = {
* @return {Promise}
*/
update: (access, data) => {
if (typeof data.is_disabled !== 'undefined') {
if (typeof data.is_disabled !== "undefined") {
data.is_disabled = data.is_disabled ? 1 : 0;
}
return access.can('users:update', data.id)
return access
.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
return internalUser.get(access, {id: data.id})
.then((user) => {
return internalUser.get(access, { id: data.id }).then((user) => {
// 2. if email is to be changed, find other users with that email
if (typeof data.email !== "undefined") {
data.email = data.email.toLowerCase().trim();
// 2. if email is to be changed, find other users with that email
if (typeof data.email !== 'undefined') {
data.email = data.email.toLowerCase().trim();
if (user.email !== data.email) {
return internalUser.isEmailAvailable(data.email, data.id)
.then((available) => {
if (!available) {
throw new error.ValidationError('Email address already in use - ' + data.email);
}
return user;
});
}
if (user.email !== data.email) {
return internalUser.isEmailAvailable(data.email, data.id).then((available) => {
if (!available) {
throw new errs.ValidationError(`Email address already in use - ${data.email}`);
}
return user;
});
}
}
// No change to email:
return user;
});
// No change to email:
return user;
});
})
.then((user) => {
if (user.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id);
throw new errs.InternalValidationError(
`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,
);
}
data.avatar = gravatar.url(data.email || user.email, {default: 'mm'});
return userModel
.query()
.patchAndFetchById(user.id, data)
.then(utils.omitRow(omissions()));
data.avatar = gravatar.url(data.email || user.email, { default: "mm" });
return userModel.query().patchAndFetchById(user.id, data).then(utils.omitRow(omissions()));
})
.then(() => {
return internalUser.get(access, {id: data.id});
return internalUser.get(access, { id: data.id });
})
.then((user) => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'user',
object_id: user.id,
meta: data
})
return internalAuditLog
.add(access, {
action: "updated",
object_type: "user",
object_id: user.id,
meta: { ...data, id: user.id, name: user.name },
})
.then(() => {
return user;
});
@@ -170,37 +154,41 @@ const internalUser = {
* @return {Promise}
*/
get: (access, data) => {
if (typeof data === 'undefined') {
data = {};
const thisData = data || {};
if (typeof thisData.id === "undefined" || !thisData.id) {
thisData.id = access.token.getUserId(0);
}
if (typeof data.id === 'undefined' || !data.id) {
data.id = access.token.getUserId(0);
}
return access.can('users:get', data.id)
return access
.can("users:get", thisData.id)
.then(() => {
let query = userModel
const query = userModel
.query()
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowGraph('[permissions]')
.where("is_deleted", 0)
.andWhere("id", thisData.id)
.allowGraph("[permissions]")
.first();
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.withGraphFetched('[' + data.expand.join(', ') + ']');
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
}
return query.then(utils.omitRow(omissions()));
})
.then((row) => {
if (!row || !row.id) {
throw new error.ItemNotFoundError(data.id);
if (!row?.id) {
throw new errs.ItemNotFoundError(thisData.id);
}
// Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) {
row = _.omit(row, data.omit);
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) {
return _.omit(row, thisData.omit);
}
if (row.avatar === "") {
row.avatar = DEFAULT_AVATAR;
}
return row;
});
},
@@ -213,20 +201,15 @@ const internalUser = {
* @param user_id
*/
isEmailAvailable: (email, user_id) => {
let query = userModel
.query()
.where('email', '=', email.toLowerCase().trim())
.where('is_deleted', 0)
.first();
const query = userModel.query().where("email", "=", email.toLowerCase().trim()).where("is_deleted", 0).first();
if (typeof user_id !== 'undefined') {
query.where('id', '!=', user_id);
if (typeof user_id !== "undefined") {
query.where("id", "!=", user_id);
}
return query
.then((user) => {
return !user;
});
return query.then((user) => {
return !user;
});
},
/**
@@ -237,33 +220,34 @@ const internalUser = {
* @returns {Promise}
*/
delete: (access, data) => {
return access.can('users:delete', data.id)
return access
.can("users:delete", data.id)
.then(() => {
return internalUser.get(access, {id: data.id});
return internalUser.get(access, { id: data.id });
})
.then((user) => {
if (!user) {
throw new error.ItemNotFoundError(data.id);
throw new errs.ItemNotFoundError(data.id);
}
// Make sure user can't delete themselves
if (user.id === access.token.getUserId(0)) {
throw new error.PermissionError('You cannot delete yourself.');
throw new errs.PermissionError("You cannot delete yourself.");
}
return userModel
.query()
.where('id', user.id)
.where("id", user.id)
.patch({
is_deleted: 1
is_deleted: 1,
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'user',
object_id: user.id,
meta: _.omit(user, omissions())
action: "deleted",
object_type: "user",
object_id: user.id,
meta: _.omit(user, omissions()),
});
});
})
@@ -272,6 +256,14 @@ const internalUser = {
});
},
deleteAll: async () => {
await userModel
.query()
.patch({
is_deleted: 1,
});
},
/**
* This will only count the users
*
@@ -280,26 +272,26 @@ const internalUser = {
* @returns {*}
*/
getCount: (access, search_query) => {
return access.can('users:list')
return access
.can("users:list")
.then(() => {
let query = userModel
.query()
.count('id as count')
.where('is_deleted', 0)
.first();
const query = userModel.query().count("id as count").where("is_deleted", 0).first();
// Query is used for searching
if (typeof search_query === 'string') {
if (typeof search_query === "string") {
query.where(function () {
this.where('user.name', 'like', '%' + search_query + '%')
.orWhere('user.email', 'like', '%' + search_query + '%');
this.where("user.name", "like", `%${search_query}%`).orWhere(
"user.email",
"like",
`%${search_query}%`,
);
});
}
return query;
})
.then((row) => {
return parseInt(row.count, 10);
return Number.parseInt(row.count, 10);
});
},
@@ -311,30 +303,28 @@ const internalUser = {
* @param {String} [search_query]
* @returns {Promise}
*/
getAll: (access, expand, search_query) => {
return access.can('users:list')
.then(() => {
let query = userModel
.query()
.where('is_deleted', 0)
.groupBy('id')
.allowGraph('[permissions]')
.orderBy('name', 'ASC');
getAll: async (access, expand, search_query) => {
await access.can("users:list");
const query = userModel
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph("[permissions]")
.orderBy("name", "ASC");
// Query is used for searching
if (typeof search_query === 'string') {
query.where(function () {
this.where('name', 'like', '%' + search_query + '%')
.orWhere('email', 'like', '%' + search_query + '%');
});
}
if (typeof expand !== 'undefined' && expand !== null) {
query.withGraphFetched('[' + expand.join(', ') + ']');
}
return query.then(utils.omitRows(omissions()));
// Query is used for searching
if (typeof search_query === "string") {
query.where(function () {
this.where("name", "like", `%${search_query}%`).orWhere("email", "like", `%${search_query}%`);
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
const res = await query;
return utils.omitRows(omissions())(res);
},
/**
@@ -342,11 +332,11 @@ const internalUser = {
* @param {Integer} [id_requested]
* @returns {[String]}
*/
getUserOmisionsByAccess: (access, id_requested) => {
getUserOmisionsByAccess: (access, idRequested) => {
let response = []; // Admin response
if (!access.token.hasScope('admin') && access.token.getUserId(0) !== id_requested) {
response = ['roles', 'is_deleted']; // Restricted response
if (!access.token.hasScope("admin") && access.token.getUserId(0) !== idRequested) {
response = ["is_deleted"]; // Restricted response
}
return response;
@@ -361,26 +351,30 @@ const internalUser = {
* @return {Promise}
*/
setPassword: (access, data) => {
return access.can('users:password', data.id)
return access
.can("users:password", data.id)
.then(() => {
return internalUser.get(access, {id: data.id});
return internalUser.get(access, { id: data.id });
})
.then((user) => {
if (user.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id);
throw new errs.InternalValidationError(
`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,
);
}
if (user.id === access.token.getUserId(0)) {
// they're setting their own password. Make sure their current password is correct
if (typeof data.current === 'undefined' || !data.current) {
throw new error.ValidationError('Current password was not supplied');
if (typeof data.current === "undefined" || !data.current) {
throw new errs.ValidationError("Current password was not supplied");
}
return internalToken.getTokenFromEmail({
identity: user.email,
secret: data.current
})
return internalToken
.getTokenFromEmail({
identity: user.email,
secret: data.current,
})
.then(() => {
return user;
});
@@ -392,43 +386,36 @@ const internalUser = {
// Get auth, patch if it exists
return authModel
.query()
.where('user_id', user.id)
.andWhere('type', data.type)
.where("user_id", user.id)
.andWhere("type", data.type)
.first()
.then((existing_auth) => {
if (existing_auth) {
// patch
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
});
} else {
// insert
return authModel
.query()
.insert({
user_id: user.id,
type: data.type,
secret: data.secret,
meta: {}
});
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,
});
}
// insert
return authModel.query().insert({
user_id: user.id,
type: data.type,
secret: data.secret,
meta: {},
});
})
.then(() => {
// Add to Audit Log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'user',
object_id: user.id,
meta: {
name: user.name,
action: "updated",
object_type: "user",
object_id: user.id,
meta: {
name: user.name,
password_changed: true,
auth_type: data.type
}
auth_type: data.type,
},
});
});
})
@@ -443,14 +430,17 @@ const internalUser = {
* @return {Promise}
*/
setPermissions: (access, data) => {
return access.can('users:permissions', data.id)
return access
.can("users:permissions", data.id)
.then(() => {
return internalUser.get(access, {id: data.id});
return internalUser.get(access, { id: data.id });
})
.then((user) => {
if (user.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id);
throw new errs.InternalValidationError(
`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,
);
}
return user;
@@ -459,34 +449,30 @@ const internalUser = {
// Get perms row, patch if it exists
return userPermissionModel
.query()
.where('user_id', user.id)
.where("user_id", user.id)
.first()
.then((existing_auth) => {
if (existing_auth) {
// patch
return userPermissionModel
.query()
.where('user_id', user.id)
.patchAndFetchById(existing_auth.id, _.assign({user_id: user.id}, data));
} else {
// insert
return userPermissionModel
.query()
.insertAndFetch(_.assign({user_id: user.id}, data));
.where("user_id", user.id)
.patchAndFetchById(existing_auth.id, _.assign({ user_id: user.id }, data));
}
// insert
return userPermissionModel.query().insertAndFetch(_.assign({ user_id: user.id }, data));
})
.then((permissions) => {
// Add to Audit Log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'user',
object_id: user.id,
meta: {
name: user.name,
permissions: permissions
}
action: "updated",
object_type: "user",
object_id: user.id,
meta: {
name: user.name,
permissions: permissions,
},
});
});
})
.then(() => {
@@ -500,14 +486,15 @@ const internalUser = {
* @param {Integer} data.id
*/
loginAs: (access, data) => {
return access.can('users:loginas', data.id)
return access
.can("users:loginas", data.id)
.then(() => {
return internalUser.get(access, data);
})
.then((user) => {
return internalToken.getTokenFromUser(user);
});
}
},
};
module.exports = internalUser;
export default internalUser;
+205 -234
View File
@@ -4,91 +4,90 @@
* "scope" in this file means "where did this token come from and what is using it", so 99% of the time
* the "scope" is going to be "user" because it would be a user token. This is not to be confused with
* the "role" which could be "user" or "admin". The scope in fact, could be "worker" or anything else.
*
*
*/
const _ = require('lodash');
const logger = require('../logger').access;
const Ajv = require('ajv/dist/2020');
const error = require('./error');
const userModel = require('../models/user');
const proxyHostModel = require('../models/proxy_host');
const TokenModel = require('../models/token');
const roleSchema = require('./access/roles.json');
const permsSchema = require('./access/permissions.json');
import fs from "node:fs";
import { dirname } from "node:path";
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 TokenModel from "../models/token.js";
import userModel from "../models/user.js";
import permsSchema from "./access/permissions.json" with { type: "json" };
import roleSchema from "./access/roles.json" with { type: "json" };
import errs from "./error.js";
module.exports = function (token_string) {
let Token = new TokenModel();
let token_data = null;
let initialised = false;
let object_cache = {};
let allow_internal_access = false;
let user_roles = [];
let permissions = {};
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export default function (tokenString) {
const Token = TokenModel();
let tokenData = null;
let initialised = false;
const objectCache = {};
let allowInternalAccess = false;
let userRoles = [];
let permissions = {};
/**
* Loads the Token object from the token string
*
* @returns {Promise}
*/
this.init = () => {
return new Promise((resolve, reject) => {
if (initialised) {
resolve();
} else if (!token_string) {
reject(new error.PermissionError('Permission Denied'));
this.init = async () => {
if (initialised) {
return;
}
if (!tokenString) {
throw new errs.PermissionError("Permission Denied");
}
tokenData = await Token.load(tokenString);
// At this point we need to load the user from the DB and make sure they:
// - exist (and not soft deleted)
// - still have the appropriate scopes for this token
// This is only required when the User ID is supplied or if the token scope has `user`
if (
tokenData.attrs.id ||
(typeof tokenData.scope !== "undefined" && _.indexOf(tokenData.scope, "user") !== -1)
) {
// Has token user id or token user scope
const user = await userModel
.query()
.where("id", tokenData.attrs.id)
.andWhere("is_deleted", 0)
.andWhere("is_disabled", 0)
.allowGraph("[permissions]")
.withGraphFetched("[permissions]")
.first();
if (user) {
// make sure user has all scopes of the token
// The `user` role is not added against the user row, so we have to just add it here to get past this check.
user.roles.push("user");
let ok = true;
_.forEach(tokenData.scope, (scope_item) => {
if (_.indexOf(user.roles, scope_item) === -1) {
ok = false;
}
});
if (!ok) {
throw new errs.AuthError("Invalid token scope for User");
}
initialised = true;
userRoles = user.roles;
permissions = user.permissions;
} else {
resolve(Token.load(token_string)
.then((data) => {
token_data = data;
// At this point we need to load the user from the DB and make sure they:
// - exist (and not soft deleted)
// - still have the appropriate scopes for this token
// This is only required when the User ID is supplied or if the token scope has `user`
if (token_data.attrs.id || (typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, 'user') !== -1)) {
// Has token user id or token user scope
return userModel
.query()
.where('id', token_data.attrs.id)
.andWhere('is_deleted', 0)
.andWhere('is_disabled', 0)
.allowGraph('[permissions]')
.withGraphFetched('[permissions]')
.first()
.then((user) => {
if (user) {
// make sure user has all scopes of the token
// The `user` role is not added against the user row, so we have to just add it here to get past this check.
user.roles.push('user');
let is_ok = true;
_.forEach(token_data.scope, (scope_item) => {
if (_.indexOf(user.roles, scope_item) === -1) {
is_ok = false;
}
});
if (!is_ok) {
throw new error.AuthError('Invalid token scope for User');
} else {
initialised = true;
user_roles = user.roles;
permissions = user.permissions;
}
} else {
throw new error.AuthError('User cannot be loaded for Token');
}
});
} else {
initialised = true;
}
}));
throw new errs.AuthError("User cannot be loaded for Token");
}
});
}
initialised = true;
};
/**
@@ -96,140 +95,121 @@ module.exports = function (token_string) {
* This only applies to USER token scopes, as all other tokens are not really bound
* by object scopes
*
* @param {String} object_type
* @param {String} objectType
* @returns {Promise}
*/
this.loadObjects = (object_type) => {
return new Promise((resolve, reject) => {
if (Token.hasScope('user')) {
if (typeof token_data.attrs.id === 'undefined' || !token_data.attrs.id) {
reject(new error.AuthError('User Token supplied without a User ID'));
} else {
let token_user_id = token_data.attrs.id ? token_data.attrs.id : 0;
let query;
this.loadObjects = async (objectType) => {
let objects = null;
if (typeof object_cache[object_type] === 'undefined') {
switch (object_type) {
if (Token.hasScope("user")) {
if (typeof tokenData.attrs.id === "undefined" || !tokenData.attrs.id) {
throw new errs.AuthError("User Token supplied without a User ID");
}
// USERS - should only return yourself
case 'users':
resolve(token_user_id ? [token_user_id] : []);
break;
const tokenUserId = tokenData.attrs.id ? tokenData.attrs.id : 0;
// Proxy Hosts
case 'proxy_hosts':
query = proxyHostModel
.query()
.select('id')
.andWhere('is_deleted', 0);
if (typeof objectCache[objectType] !== "undefined") {
objects = objectCache[objectType];
} else {
switch (objectType) {
// USERS - should only return yourself
case "users":
objects = tokenUserId ? [tokenUserId] : [];
break;
if (permissions.visibility === 'user') {
query.andWhere('owner_user_id', token_user_id);
}
// Proxy Hosts
case "proxy_hosts": {
const query = proxyHostModel
.query()
.select("id")
.andWhere("is_deleted", 0);
resolve(query
.then((rows) => {
let result = [];
_.forEach(rows, (rule_row) => {
result.push(rule_row.id);
});
// enum should not have less than 1 item
if (!result.length) {
result.push(0);
}
return result;
})
);
break;
// DEFAULT: null
default:
resolve(null);
break;
if (permissions.visibility === "user") {
query.andWhere("owner_user_id", tokenUserId);
}
} else {
resolve(object_cache[object_type]);
const rows = await query;
objects = [];
_.forEach(rows, (ruleRow) => {
objects.push(ruleRow.id);
});
// enum should not have less than 1 item
if (!objects.length) {
objects.push(0);
}
break;
}
}
} else {
resolve(null);
objectCache[objectType] = objects;
}
})
.then((objects) => {
object_cache[object_type] = objects;
return objects;
});
}
return objects;
};
/**
* Creates a schema object on the fly with the IDs and other values required to be checked against the permissionSchema
*
* @param {String} permission_label
* @param {String} permissionLabel
* @returns {Object}
*/
this.getObjectSchema = (permission_label) => {
let base_object_type = permission_label.split(':').shift();
this.getObjectSchema = async (permissionLabel) => {
const baseObjectType = permissionLabel.split(":").shift();
let schema = {
$id: 'objects',
description: 'Actor Properties',
type: 'object',
const schema = {
$id: "objects",
description: "Actor Properties",
type: "object",
additionalProperties: false,
properties: {
properties: {
user_id: {
anyOf: [
{
type: 'number',
enum: [Token.get('attrs').id]
}
]
type: "number",
enum: [Token.get("attrs").id],
},
],
},
scope: {
type: 'string',
pattern: '^' + Token.get('scope') + '$'
}
}
type: "string",
pattern: `^${Token.get("scope")}$`,
},
},
};
return this.loadObjects(base_object_type)
.then((object_result) => {
if (typeof object_result === 'object' && object_result !== null) {
schema.properties[base_object_type] = {
type: 'number',
enum: object_result,
minimum: 1
};
} else {
schema.properties[base_object_type] = {
type: 'number',
minimum: 1
};
}
const result = await this.loadObjects(baseObjectType);
if (typeof result === "object" && result !== null) {
schema.properties[baseObjectType] = {
type: "number",
enum: result,
minimum: 1,
};
} else {
schema.properties[baseObjectType] = {
type: "number",
minimum: 1,
};
}
return schema;
});
return schema;
};
return {
// here:
return {
token: Token,
/**
*
* @param {Boolean} [allow_internal]
* @param {Boolean} [allowInternal]
* @returns {Promise}
*/
load: (allow_internal) => {
return new Promise(function (resolve/*, reject*/) {
if (token_string) {
resolve(Token.load(token_string));
} else {
allow_internal_access = allow_internal;
resolve(allow_internal_access || null);
}
});
load: async (allowInternal) => {
if (tokenString) {
return await Token.load(tokenString);
}
allowInternalAccess = allowInternal;
return allowInternal || null;
},
reloadObjects: this.loadObjects,
@@ -240,68 +220,59 @@ module.exports = function (token_string) {
* @param {*} [data]
* @returns {Promise}
*/
can: (permission, data) => {
if (allow_internal_access === true) {
return Promise.resolve(true);
//return true;
} else {
return this.init()
.then(() => {
// Initialised, token decoded ok
return this.getObjectSchema(permission)
.then((objectSchema) => {
const data_schema = {
[permission]: {
data: data,
scope: Token.get('scope'),
roles: user_roles,
permission_visibility: permissions.visibility,
permission_proxy_hosts: permissions.proxy_hosts,
permission_redirection_hosts: permissions.redirection_hosts,
permission_dead_hosts: permissions.dead_hosts,
permission_streams: permissions.streams,
permission_access_lists: permissions.access_lists,
permission_certificates: permissions.certificates
}
};
let permissionSchema = {
$async: true,
$id: 'permissions',
type: 'object',
additionalProperties: false,
properties: {}
};
permissionSchema.properties[permission] = require('./access/' + permission.replace(/:/gim, '-') + '.json');
const ajv = new Ajv({
verbose: true,
allErrors: true,
breakOnError: true,
coerceTypes: true,
schemas: [
roleSchema,
permsSchema,
objectSchema,
permissionSchema
]
});
return ajv.validate('permissions', data_schema)
.then(() => {
return data_schema[permission];
});
});
})
.catch((err) => {
err.permission = permission;
err.permission_data = data;
logger.error(permission, data, err.message);
throw new error.PermissionError('Permission Denied', err);
});
can: async (permission, data) => {
if (allowInternalAccess === true) {
return true;
}
}
try {
await this.init();
const objectSchema = await this.getObjectSchema(permission);
const dataSchema = {
[permission]: {
data: data,
scope: Token.get("scope"),
roles: userRoles,
permission_visibility: permissions.visibility,
permission_proxy_hosts: permissions.proxy_hosts,
permission_redirection_hosts: permissions.redirection_hosts,
permission_dead_hosts: permissions.dead_hosts,
permission_streams: permissions.streams,
permission_access_lists: permissions.access_lists,
permission_certificates: permissions.certificates,
},
};
const permissionSchema = {
$async: true,
$id: "permissions",
type: "object",
additionalProperties: false,
properties: {},
};
const rawData = fs.readFileSync(`${__dirname}/access/${permission.replace(/:/gim, "-")}.json`, {
encoding: "utf8",
});
permissionSchema.properties[permission] = JSON.parse(rawData);
const ajv = new Ajv({
verbose: true,
allErrors: true,
breakOnError: true,
coerceTypes: true,
schemas: [roleSchema, permsSchema, objectSchema, permissionSchema],
});
const valid = await ajv.validate("permissions", dataSchema);
return valid && dataSchema[permission];
} catch (err) {
err.permission = permission;
err.permission_data = data;
logger.error(permission, data, err.message);
throw errs.PermissionError("Permission Denied", err);
}
},
};
};
}
+82 -70
View File
@@ -1,78 +1,90 @@
const dnsPlugins = require('../global/certbot-dns-plugins.json');
const utils = require('./utils');
const error = require('./error');
const logger = require('../logger').certbot;
const batchflow = require('batchflow');
import batchflow from "batchflow";
import dnsPlugins from "../certbot/dns-plugins.json" with { type: "json" };
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
*
* @param {string} pluginKey
* @returns {Object}
*/
const installPlugin = async (pluginKey) => {
if (typeof dnsPlugins[pluginKey] === "undefined") {
throw new errs.ItemNotFoundError(pluginKey);
}
const certbot = {
const plugin = dnsPlugins[pluginKey];
logger.start(`Installing ${pluginKey}...`);
/**
* @param {array} pluginKeys
*/
installPlugins: async function (pluginKeys) {
let hasErrors = false;
plugin.version = plugin.version.replace(/{{certbot-version}}/g, process.env.CERTBOT_VERSION);
plugin.dependencies = plugin.dependencies.replace(/{{certbot-version}}/g, process.env.CERTBOT_VERSION);
return new Promise((resolve, reject) => {
if (pluginKeys.length === 0) {
resolve();
return;
}
// 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);
}
batchflow(pluginKeys).sequential()
.each((i, pluginKey, next) => {
certbot.installPlugin(pluginKey)
.then(() => {
next();
})
.catch((err) => {
hasErrors = true;
next(err);
});
})
.error((err) => {
logger.error(err.message);
})
.end(() => {
if (hasErrors) {
reject(new error.CommandError('Some plugins failed to install. Please check the logs above', 1));
} else {
resolve();
}
});
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) => {
logger.complete(`Installed ${pluginKey}`);
return result;
})
.catch((err) => {
throw err;
});
},
/**
* Installs a cerbot plugin given the key for the object from
* ../global/certbot-dns-plugins.json
*
* @param {string} pluginKey
* @returns {Object}
*/
installPlugin: async function (pluginKey) {
if (typeof dnsPlugins[pluginKey] === 'undefined') {
// throw Error(`Certbot plugin ${pluginKey} not found`);
throw new error.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);
const cmd = '. /opt/certbot/bin/activate && pip install --no-cache-dir ' + plugin.dependencies + ' ' + plugin.package_name + plugin.version + ' ' + ' && deactivate';
return utils.exec(cmd)
.then((result) => {
logger.complete(`Installed ${pluginKey}`);
return result;
})
.catch((err) => {
throw err;
});
},
};
module.exports = certbot;
/**
* @param {array} pluginKeys
*/
const installPlugins = async (pluginKeys) => {
let hasErrors = false;
return new Promise((resolve, reject) => {
if (pluginKeys.length === 0) {
resolve();
return;
}
batchflow(pluginKeys)
.sequential()
.each((_i, pluginKey, next) => {
installPlugin(pluginKey)
.then(() => {
next();
})
.catch((err) => {
hasErrors = true;
next(err);
});
})
.error((err) => {
logger.error(err.message);
})
.end(() => {
if (hasErrors) {
reject(new errs.CommandError("Some plugins failed to install. Please check the logs above", 1));
} else {
resolve();
}
});
});
};
export { installPlugin, installPlugins };
+173 -148
View File
@@ -1,48 +1,65 @@
const fs = require('fs');
const NodeRSA = require('node-rsa');
const logger = require('../logger').global;
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;
// 1. Load from config file first (not recommended anymore)
// 2. Use config env variables next
const configure = () => {
const filename = (process.env.NODE_CONFIG_DIR || './config') + '/' + (process.env.NODE_ENV || 'default') + '.json';
const filename = `${process.env.NODE_CONFIG_DIR || "./config"}/${process.env.NODE_ENV || "default"}.json`;
if (fs.existsSync(filename)) {
let configData;
try {
configData = require(filename);
// Load this json synchronously
const rawData = fs.readFileSync(filename);
configData = JSON.parse(rawData);
} catch (_) {
// do nothing
}
if (configData && configData.database) {
if (configData?.database) {
logger.info(`Using configuration from file: ${filename}`);
instance = configData;
// Migrate those who have "mysql" engine to "mysql2"
if (configData.database.engine === "mysql") {
configData.database.engine = mysqlEngine;
}
instance = configData;
instance.keys = getKeys();
return;
}
}
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 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);
if (envMysqlHost && envMysqlUser && envMysqlName) {
// we have enough mysql creds to go with mysql
logger.info('Using MySQL configuration');
logger.info("Using MySQL configuration");
instance = {
database: {
engine: mysqlEngine,
host: envMysqlHost,
port: process.env.DB_MYSQL_PORT || 3306,
user: envMysqlUser,
engine: mysqlEngine,
host: envMysqlHost,
port: process.env.DB_MYSQL_PORT || 3306,
user: envMysqlUser,
password: process.env.DB_MYSQL_PASSWORD,
name: envMysqlName,
ssl: envMysqlSSL ? { rejectUnauthorized: envMysqlSSLRejectUnauthorized, verifyIdentity: envMysqlSSLVerifyIdentity } : false,
},
keys: getKeys(),
};
@@ -54,33 +71,34 @@ const configure = () => {
const envPostgresName = process.env.DB_POSTGRES_NAME || null;
if (envPostgresHost && envPostgresUser && envPostgresName) {
// we have enough postgres creds to go with postgres
logger.info('Using Postgres configuration');
logger.info("Using Postgres configuration");
instance = {
database: {
engine: postgresEngine,
host: envPostgresHost,
port: process.env.DB_POSTGRES_PORT || 5432,
user: envPostgresUser,
engine: postgresEngine,
host: envPostgresHost,
port: process.env.DB_POSTGRES_PORT || 5432,
user: envPostgresUser,
password: process.env.DB_POSTGRES_PASSWORD,
name: envPostgresName,
name: envPostgresName,
},
keys: getKeys(),
};
return;
}
const envSqliteFile = process.env.DB_SQLITE_FILE || '/data/database.sqlite';
const envSqliteFile = process.env.DB_SQLITE_FILE || "/data/database.sqlite";
logger.info(`Using Sqlite: ${envSqliteFile}`);
instance = {
database: {
engine: 'knex-native',
knex: {
client: sqliteClientName,
engine: "knex-native",
knex: {
client: sqliteClientName,
connection: {
filename: envSqliteFile
filename: envSqliteFile,
},
useNullAsDefault: true
}
useNullAsDefault: true,
},
},
keys: getKeys(),
};
@@ -88,150 +106,157 @@ const configure = () => {
const getKeys = () => {
// Get keys from file
if (isDebugMode()) {
logger.debug("Checking for keys file:", keysFile);
}
if (!fs.existsSync(keysFile)) {
generateKeys();
} else if (process.env.DEBUG) {
logger.info('Keys file exists OK');
logger.info("Keys file exists OK");
}
try {
return require(keysFile);
// Load this json keysFile synchronously and return the json object
const rawData = fs.readFileSync(keysFile);
return JSON.parse(rawData);
} catch (err) {
logger.error('Could not read JWT key pair from config file: ' + keysFile, err);
logger.error(`Could not read JWT key pair from config file: ${keysFile}`, err);
process.exit(1);
}
};
const generateKeys = () => {
logger.info('Creating a new JWT key pair...');
logger.info("Creating a new JWT key pair...");
// Now create the keys and save them in the config.
const key = new NodeRSA({ b: 2048 });
key.generateKeyPair();
const keys = {
key: key.exportKey('private').toString(),
pub: key.exportKey('public').toString(),
key: key.exportKey("private").toString(),
pub: key.exportKey("public").toString(),
};
// Write keys config
try {
fs.writeFileSync(keysFile, JSON.stringify(keys, null, 2));
} catch (err) {
logger.error('Could not write JWT key pair to config file: ' + keysFile + ': ' + err.message);
logger.error(`Could not write JWT key pair to config file: ${keysFile}: ${err.message}`);
process.exit(1);
}
logger.info('Wrote JWT key pair to config file: ' + keysFile);
logger.info(`Wrote JWT key pair to config file: ${keysFile}`);
};
module.exports = {
/**
*
* @param {string} key ie: 'database' or 'database.engine'
* @returns {boolean}
*/
has: function(key) {
instance === null && configure();
const keys = key.split('.');
let level = instance;
let has = true;
keys.forEach((keyItem) =>{
if (typeof level[keyItem] === 'undefined') {
has = false;
} else {
level = level[keyItem];
}
});
return has;
},
/**
* Gets a specific key from the top level
*
* @param {string} key
* @returns {*}
*/
get: function (key) {
instance === null && configure();
if (key && typeof instance[key] !== 'undefined') {
return instance[key];
/**
*
* @param {string} key ie: 'database' or 'database.engine'
* @returns {boolean}
*/
const configHas = (key) => {
instance === null && configure();
const keys = key.split(".");
let level = instance;
let has = true;
keys.forEach((keyItem) => {
if (typeof level[keyItem] === "undefined") {
has = false;
} else {
level = level[keyItem];
}
return instance;
},
});
/**
* Is this a sqlite configuration?
*
* @returns {boolean}
*/
isSqlite: function () {
instance === null && configure();
return instance.database.knex && instance.database.knex.client === sqliteClientName;
},
return has;
};
/**
* Is this a mysql configuration?
*
* @returns {boolean}
*/
isMysql: function () {
instance === null && configure();
return instance.database.engine === mysqlEngine;
},
/**
* Is this a postgres configuration?
*
* @returns {boolean}
*/
isPostgres: function () {
instance === null && configure();
return instance.database.engine === postgresEngine;
},
/**
* Are we running in debug mdoe?
*
* @returns {boolean}
*/
debug: function () {
return !!process.env.DEBUG;
},
/**
* Returns a public key
*
* @returns {string}
*/
getPublicKey: function () {
instance === null && configure();
return instance.keys.pub;
},
/**
* Returns a private key
*
* @returns {string}
*/
getPrivateKey: function () {
instance === null && configure();
return instance.keys.key;
},
/**
* @returns {boolean}
*/
useLetsencryptStaging: function () {
return !!process.env.LE_STAGING;
},
/**
* @returns {string|null}
*/
useLetsencryptServer: function () {
if (process.env.LE_SERVER) {
return process.env.LE_SERVER;
}
return null;
/**
* Gets a specific key from the top level
*
* @param {string} key
* @returns {*}
*/
const configGet = (key) => {
instance === null && configure();
if (key && typeof instance[key] !== "undefined") {
return instance[key];
}
return instance;
};
/**
* Is this a sqlite configuration?
*
* @returns {boolean}
*/
const isSqlite = () => {
instance === null && configure();
return instance.database.knex && [sqliteClientName, legacySqliteClientName].includes(instance.database.knex.client);
};
/**
* Is this a mysql configuration?
*
* @returns {boolean}
*/
const isMysql = () => {
instance === null && configure();
return instance.database.engine === mysqlEngine;
};
/**
* Is this a postgres configuration?
*
* @returns {boolean}
*/
const isPostgres = () => {
instance === null && configure();
return instance.database.engine === postgresEngine;
};
/**
* Are we running in debug mode?
*
* @returns {boolean}
*/
const isDebugMode = () => !!process.env.DEBUG;
/**
* Are we running in CI?
*
* @returns {boolean}
*/
const isCI = () => process.env.CI === 'true' && process.env.DEBUG === 'true';
/**
* Returns a public key
*
* @returns {string}
*/
const getPublicKey = () => {
instance === null && configure();
return instance.keys.pub;
};
/**
* Returns a private key
*
* @returns {string}
*/
const getPrivateKey = () => {
instance === null && configure();
return instance.keys.key;
};
/**
* @returns {boolean}
*/
const useLetsencryptStaging = () => !!process.env.LE_STAGING;
/**
* @returns {string|null}
*/
const useLetsencryptServer = () => {
if (process.env.LE_SERVER) {
return process.env.LE_SERVER;
}
return null;
};
export { isCI, configHas, configGet, isSqlite, isMysql, isPostgres, isDebugMode, getPrivateKey, getPublicKey, useLetsencryptStaging, useLetsencryptServer };
+52 -48
View File
@@ -1,99 +1,103 @@
const _ = require('lodash');
const util = require('util');
import _ from "lodash";
module.exports = {
PermissionError: function (message, previous) {
const errs = {
PermissionError: function (_, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.name = this.constructor.name;
this.previous = previous;
this.message = 'Permission Denied';
this.public = true;
this.status = 403;
this.message = "Permission Denied";
this.public = true;
this.status = 403;
},
ItemNotFoundError: function (id, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.name = this.constructor.name;
this.previous = previous;
this.message = 'Item Not Found - ' + id;
this.public = true;
this.status = 404;
this.message = "Not Found";
if (id) {
this.message = `Not Found - ${id}`;
}
this.public = true;
this.status = 404;
},
AuthError: function (message, previous) {
AuthError: function (message, messageI18n, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.public = true;
this.status = 401;
this.message = message;
this.message_i18n = messageI18n;
this.public = true;
this.status = 400;
},
InternalError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.status = 500;
this.public = false;
this.message = message;
this.status = 500;
this.public = false;
},
InternalValidationError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.status = 400;
this.public = false;
this.message = message;
this.status = 400;
this.public = false;
},
ConfigurationError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.status = 400;
this.public = true;
this.message = message;
this.status = 400;
this.public = true;
},
CacheError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.message = message;
this.name = this.constructor.name;
this.message = message;
this.previous = previous;
this.status = 500;
this.public = false;
this.status = 500;
this.public = false;
},
ValidationError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.public = true;
this.status = 400;
this.message = message;
this.public = true;
this.status = 400;
},
AssertionFailedError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.public = false;
this.status = 400;
this.message = message;
this.public = false;
this.status = 400;
},
CommandError: function (stdErr, code, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.name = this.constructor.name;
this.previous = previous;
this.message = stdErr;
this.code = code;
this.public = false;
this.message = stdErr;
this.code = code;
this.public = false;
},
};
_.forEach(module.exports, function (error) {
util.inherits(error, Error);
_.forEach(errs, (err) => {
err.prototype = Object.create(Error.prototype);
});
export default errs;
+8 -7
View File
@@ -1,12 +1,13 @@
module.exports = function (req, res, next) {
export default (req, res, next) => {
if (req.headers.origin) {
res.set({
'Access-Control-Allow-Origin': req.headers.origin,
'Access-Control-Allow-Credentials': true,
'Access-Control-Allow-Methods': 'OPTIONS, GET, POST',
'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,
'Access-Control-Expose-Headers': 'X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit'
"Access-Control-Allow-Origin": req.headers.origin,
"Access-Control-Allow-Credentials": true,
"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,
"Access-Control-Expose-Headers": "X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit",
});
next();
} else {
+12 -12
View File
@@ -1,15 +1,15 @@
const Access = require('../access');
import Access from "../access.js";
module.exports = () => {
return function (req, res, next) {
res.locals.access = null;
let access = new Access(res.locals.token || null);
access.load()
.then(() => {
res.locals.access = access;
next();
})
.catch(next);
export default () => {
return async (_, res, next) => {
try {
res.locals.access = null;
const access = new Access(res.locals.token || null);
await access.load();
res.locals.access = access;
next();
} catch (err) {
next(err);
}
};
};
+5 -5
View File
@@ -1,13 +1,13 @@
module.exports = function () {
return function (req, res, next) {
export default function () {
return (req, res, next) => {
if (req.headers.authorization) {
let parts = req.headers.authorization.split(' ');
const parts = req.headers.authorization.split(" ");
if (parts && parts[0] === 'Bearer' && parts[1]) {
if (parts && parts[0] === "Bearer" && parts[1]) {
res.locals.token = parts[1];
}
}
next();
};
};
}
+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();
};
+16 -16
View File
@@ -1,7 +1,6 @@
let _ = require('lodash');
module.exports = function (default_sort, default_offset, default_limit, max_limit) {
import _ from "lodash";
export default (default_sort, default_offset, default_limit, max_limit) => {
/**
* This will setup the req query params with filtered data and defaults
*
@@ -11,34 +10,35 @@ module.exports = function (default_sort, default_offset, default_limit, max_limi
*
*/
return function (req, res, next) {
req.query.offset = typeof req.query.limit === 'undefined' ? default_offset || 0 : parseInt(req.query.offset, 10);
req.query.limit = typeof req.query.limit === 'undefined' ? default_limit || 50 : parseInt(req.query.limit, 10);
return (req, _res, next) => {
req.query.offset =
typeof req.query.limit === "undefined" ? default_offset || 0 : Number.parseInt(req.query.offset, 10);
req.query.limit =
typeof req.query.limit === "undefined" ? default_limit || 50 : Number.parseInt(req.query.limit, 10);
if (max_limit && req.query.limit > max_limit) {
req.query.limit = max_limit;
}
// Sorting
let sort = typeof req.query.sort === 'undefined' ? default_sort : req.query.sort;
let myRegexp = /.*\.(asc|desc)$/ig;
let sort_array = [];
let sort = typeof req.query.sort === "undefined" ? default_sort : req.query.sort;
const myRegexp = /.*\.(asc|desc)$/gi;
const sort_array = [];
sort = sort.split(',');
_.map(sort, function (val) {
let matches = myRegexp.exec(val);
sort = sort.split(",");
_.map(sort, (val) => {
const matches = myRegexp.exec(val);
if (matches !== null) {
let dir = matches[1];
const dir = matches[1];
sort_array.push({
field: val.substr(0, val.length - (dir.length + 1)),
dir: dir.toLowerCase()
dir: dir.toLowerCase(),
});
} else {
sort_array.push({
field: val,
dir: 'asc'
dir: "asc",
});
}
});
+2 -3
View File
@@ -1,9 +1,8 @@
module.exports = (req, res, next) => {
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;
} else {
req.params.user_id = parseInt(req.params.user_id, 10);
req.params.user_id = Number.parseInt(req.params.user_id, 10);
}
next();
};
+54 -58
View File
@@ -1,62 +1,58 @@
const moment = require('moment');
const {isPostgres} = require('./config');
const {ref} = require('objection');
import moment from "moment";
import { ref } from "objection";
import { isPostgres } from "./config.js";
module.exports = {
/**
* Takes an expression such as 30d and returns a moment object of that date in future
*
* Key Shorthand
* ==================
* years y
* quarters Q
* months M
* weeks w
* days d
* hours h
* minutes m
* seconds s
* milliseconds ms
*
* @param {String} expression
* @returns {Object}
*/
parseDatePeriod: function (expression) {
let matches = expression.match(/^([0-9]+)(y|Q|M|w|d|h|m|s|ms)$/m);
if (matches) {
return moment().add(matches[1], matches[2]);
}
return null;
},
convertIntFieldsToBool: function (obj, fields) {
fields.forEach(function (field) {
if (typeof obj[field] !== 'undefined') {
obj[field] = obj[field] === 1;
}
});
return obj;
},
convertBoolFieldsToInt: function (obj, fields) {
fields.forEach(function (field) {
if (typeof obj[field] !== 'undefined') {
obj[field] = obj[field] ? 1 : 0;
}
});
return obj;
},
/**
* Casts a column to json if using postgres
*
* @param {string} colName
* @returns {string|Objection.ReferenceBuilder}
*/
castJsonIfNeed: function (colName) {
return isPostgres() ? ref(colName).castText() : colName;
/**
* Takes an expression such as 30d and returns a moment object of that date in future
*
* Key Shorthand
* ==================
* years y
* quarters Q
* months M
* weeks w
* days d
* hours h
* minutes m
* seconds s
* milliseconds ms
*
* @param {String} expression
* @returns {Object}
*/
const parseDatePeriod = (expression) => {
const matches = expression.match(/^([0-9]+)(y|Q|M|w|d|h|m|s|ms)$/m);
if (matches) {
return moment().add(matches[1], matches[2]);
}
return null;
};
const convertIntFieldsToBool = (obj, fields) => {
fields.forEach((field) => {
if (typeof obj[field] !== "undefined") {
obj[field] = obj[field] === 1;
}
});
return obj;
};
const convertBoolFieldsToInt = (obj, fields) => {
fields.forEach((field) => {
if (typeof obj[field] !== "undefined") {
obj[field] = obj[field] ? 1 : 0;
}
});
return obj;
};
/**
* Casts a column to json if using postgres
*
* @param {string} colName
* @returns {string|Objection.ReferenceBuilder}
*/
const castJsonIfNeed = (colName) => (isPostgres() ? ref(colName).castText() : colName);
export { parseDatePeriod, convertIntFieldsToBool, convertBoolFieldsToInt, castJsonIfNeed };
+25 -21
View File
@@ -1,33 +1,34 @@
const migrate_name = 'identifier_for_migrate';
const logger = require('../logger').migrate;
import { migrate as logger } from "../logger.js";
const migrateName = "identifier_for_migrate";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @param {Object} knex
* @returns {Promise}
*/
exports.up = function (knex, Promise) {
logger.info('[' + migrate_name + '] Migrating Up...');
const up = (_knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
// Create Table example:
/*return knex.schema.createTable('notification', (table) => {
/*
return knex.schema.createTable('notification', (table) => {
table.increments().primary();
table.string('name').notNull();
table.string('type').notNull();
table.integer('created_on').notNull();
table.integer('modified_on').notNull();
})
.then(function () {
logger.info('[' + migrate_name + '] Notification Table created');
});*/
.then(function () {
logger.info('[' + migrateName + '] Notification Table created');
});
*/
logger.info('[' + migrate_name + '] Migrating Up Complete');
logger.info(`[${migrateName}] Migrating Up Complete`);
return Promise.resolve(true);
};
@@ -35,21 +36,24 @@ exports.up = function (knex, Promise) {
/**
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @param {Object} knex
* @returns {Promise}
*/
exports.down = function (knex, Promise) {
logger.info('[' + migrate_name + '] Migrating Down...');
const down = (_knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
// Drop table example:
/*return knex.schema.dropTable('notification')
.then(() => {
logger.info('[' + migrate_name + '] Notification Table dropped');
});*/
/*
return knex.schema.dropTable('notification')
.then(() => {
logger.info(`[${migrateName}] Notification Table dropped`);
});
*/
logger.info('[' + migrate_name + '] Migrating Down Complete');
logger.info(`[${migrateName}] Migrating Down Complete`);
return Promise.resolve(true);
};
export { up, down };
+101 -100
View File
@@ -1,106 +1,107 @@
const _ = require('lodash');
const exec = require('child_process').exec;
const execFile = require('child_process').execFile;
const { Liquid } = require('liquidjs');
const logger = require('../logger').global;
const error = require('./error');
import { exec as nodeExec, execFile as nodeExecFile } from "node:child_process";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { Liquid } from "liquidjs";
import _ from "lodash";
import errs from "./error.js";
module.exports = {
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
exec: async function(cmd, options = {}) {
logger.debug('CMD:', cmd);
const { stdout, stderr } = await new Promise((resolve, reject) => {
const child = exec(cmd, options, (isError, stdout, stderr) => {
if (isError) {
reject(new error.CommandError(stderr, isError));
} else {
resolve({ stdout, stderr });
}
});
child.on('error', (e) => {
reject(new error.CommandError(stderr, 1, e));
});
});
return stdout;
},
/**
* @param {String} cmd
* @param {Array} args
* @returns {Promise}
*/
execFile: function (cmd, args) {
// logger.debug('CMD: ' + cmd + ' ' + (args ? args.join(' ') : ''));
return new Promise((resolve, reject) => {
execFile(cmd, args, function (err, stdout, /*stderr*/) {
if (err && typeof err === 'object') {
reject(err);
} else {
resolve(stdout.trim());
}
});
});
},
/**
* Used in objection query builder
*
* @param {Array} omissions
* @returns {Function}
*/
omitRow: function (omissions) {
/**
* @param {Object} row
* @returns {Object}
*/
return (row) => {
return _.omit(row, omissions);
};
},
/**
* Used in objection query builder
*
* @param {Array} omissions
* @returns {Function}
*/
omitRows: function (omissions) {
/**
* @param {Array} rows
* @returns {Object}
*/
return (rows) => {
rows.forEach((row, idx) => {
rows[idx] = _.omit(row, omissions);
});
return rows;
};
},
/**
* @returns {Object} Liquid render engine
*/
getRenderEngine: function () {
const renderEngine = new Liquid({
root: __dirname + '/../templates/'
});
/**
* nginxAccessRule expects the object given to have 2 properties:
*
* directive string
* address string
*/
renderEngine.registerFilter('nginxAccessRule', (v) => {
if (typeof v.directive !== 'undefined' && typeof v.address !== 'undefined' && v.directive && v.address) {
return `${v.directive} ${v.address};`;
const exec = async (cmd, options = {}) => {
const { stdout, stderr } = await new Promise((resolve, reject) => {
const child = nodeExec(cmd, options, (isError, stdout, stderr) => {
if (isError) {
reject(new errs.CommandError(stderr, isError));
} else {
resolve({ stdout, stderr });
}
return '';
});
return renderEngine;
}
child.on("error", (e) => {
reject(new errs.CommandError(stderr, 1, e));
});
});
return stdout;
};
/**
* @param {String} cmd
* @param {Array} args
* @param {Object|undefined} options
* @returns {Promise}
*/
const execFile = (cmd, args, options) => {
const opts = options || {};
return new Promise((resolve, reject) => {
nodeExecFile(cmd, args, opts, (err, stdout, stderr) => {
if (err && typeof err === "object") {
reject(new errs.CommandError(stderr, 1, err));
} else {
resolve(stdout.trim());
}
});
});
};
/**
* Used in objection query builder
*
* @param {Array} omissions
* @returns {Function}
*/
const omitRow = (omissions) => {
/**
* @param {Object} row
* @returns {Object}
*/
return (row) => {
return _.omit(row, omissions);
};
};
/**
* Used in objection query builder
*
* @param {Array} omissions
* @returns {Function}
*/
const omitRows = (omissions) => {
/**
* @param {Array} rows
* @returns {Object}
*/
return (rows) => {
rows.forEach((row, idx) => {
rows[idx] = _.omit(row, omissions);
});
return rows;
};
};
/**
* @returns {Object} Liquid render engine
*/
const getRenderEngine = () => {
const renderEngine = new Liquid({
root: `${__dirname}/../templates/`,
});
/**
* nginxAccessRule expects the object given to have 2 properties:
*
* directive string
* address string
*/
renderEngine.registerFilter("nginxAccessRule", (v) => {
if (typeof v.directive !== "undefined" && typeof v.address !== "undefined" && v.directive && v.address) {
return `${v.directive} ${v.address};`;
}
return "";
});
return renderEngine;
};
export default { exec, execFile, omitRow, omitRows, getRenderEngine };
+31 -29
View File
@@ -1,12 +1,12 @@
const Ajv = require('ajv/dist/2020');
const error = require('../error');
import Ajv from "ajv/dist/2020.js";
import errs from "../error.js";
const ajv = new Ajv({
verbose: true,
allErrors: true,
verbose: true,
allErrors: true,
allowUnionTypes: true,
strict: false,
coerceTypes: true,
strict: false,
coerceTypes: true,
});
/**
@@ -14,30 +14,32 @@ const ajv = new Ajv({
* @param {Object} payload
* @returns {Promise}
*/
function apiValidator (schema, payload/*, description*/) {
return new Promise(function Promise_apiValidator (resolve, reject) {
if (schema === null) {
reject(new error.ValidationError('Schema is undefined'));
return;
}
const apiValidator = async (schema, payload /*, description*/) => {
if (!schema) {
throw new errs.ValidationError("Schema is undefined");
}
if (typeof payload === 'undefined') {
reject(new error.ValidationError('Payload is undefined'));
return;
}
// Can't use falsy check here as valid payload could be `0` or `false`
if (typeof payload === "undefined") {
throw new errs.ValidationError("Payload is undefined");
}
const validate = ajv.compile(schema);
const valid = validate(payload);
if (valid && !validate.errors) {
resolve(payload);
} else {
let message = ajv.errorsText(validate.errors);
let err = new error.ValidationError(message);
err.debug = [validate.errors, payload];
reject(err);
}
});
}
const validate = ajv.compile(schema);
module.exports = apiValidator;
const valid = validate(payload);
if (valid && !validate.errors) {
return payload;
}
const message = ajv.errorsText(validate.errors);
const err = new errs.ValidationError(message);
err.debug = {validationErrors: validate.errors, payload};
throw err;
};
export default apiValidator;
+18 -18
View File
@@ -1,17 +1,17 @@
const _ = require('lodash');
const Ajv = require('ajv/dist/2020');
const error = require('../error');
const commonDefinitions = require('../../schema/common.json');
import Ajv from 'ajv/dist/2020.js';
import _ from "lodash";
import commonDefinitions from "../../schema/common.json" with { type: "json" };
import errs from "../error.js";
RegExp.prototype.toJSON = RegExp.prototype.toString;
const ajv = new Ajv({
verbose: true,
allErrors: true,
verbose: true,
allErrors: true,
allowUnionTypes: true,
coerceTypes: true,
strict: false,
schemas: [commonDefinitions]
coerceTypes: true,
strict: false,
schemas: [commonDefinitions],
});
/**
@@ -20,26 +20,26 @@ const ajv = new Ajv({
* @param {Object} payload
* @returns {Promise}
*/
function validator (schema, payload) {
return new Promise(function (resolve, reject) {
const validator = (schema, payload) => {
return new Promise((resolve, reject) => {
if (!payload) {
reject(new error.InternalValidationError('Payload is falsy'));
reject(new errs.InternalValidationError("Payload is falsy"));
} else {
try {
let validate = ajv.compile(schema);
let valid = validate(payload);
const validate = ajv.compile(schema);
const valid = validate(payload);
if (valid && !validate.errors) {
resolve(_.cloneDeep(payload));
} else {
let message = ajv.errorsText(validate.errors);
reject(new error.InternalValidationError(message));
const message = ajv.errorsText(validate.errors);
reject(new errs.InternalValidationError(message));
}
} catch (err) {
reject(err);
}
}
});
}
};
module.exports = validator;
export default validator;
+24 -12
View File
@@ -1,14 +1,26 @@
const {Signale} = require('signale');
import signale from "signale";
import { isDebugMode } from "./lib/config.js";
module.exports = {
global: new Signale({scope: 'Global '}),
migrate: new Signale({scope: 'Migrate '}),
express: new Signale({scope: 'Express '}),
access: new Signale({scope: 'Access '}),
nginx: new Signale({scope: 'Nginx '}),
ssl: new Signale({scope: 'SSL '}),
certbot: new Signale({scope: 'Certbot '}),
import: new Signale({scope: 'Importer '}),
setup: new Signale({scope: 'Setup '}),
ip_ranges: new Signale({scope: 'IP Ranges'})
const opts = {
logLevel: "info",
};
const global = new signale.Signale({ scope: "Global ", ...opts });
const migrate = new signale.Signale({ scope: "Migrate ", ...opts });
const express = new signale.Signale({ scope: "Express ", ...opts });
const access = new signale.Signale({ scope: "Access ", ...opts });
const nginx = new signale.Signale({ scope: "Nginx ", ...opts });
const ssl = new signale.Signale({ scope: "SSL ", ...opts });
const certbot = new signale.Signale({ scope: "Certbot ", ...opts });
const importer = new signale.Signale({ scope: "Importer ", ...opts });
const setup = new signale.Signale({ scope: "Setup ", ...opts });
const ipRanges = new signale.Signale({ scope: "IP Ranges", ...opts });
const remoteVersion = new signale.Signale({ scope: "Remote Version", ...opts });
const debug = (logger, ...args) => {
if (isDebugMode()) {
logger.debug(...args);
}
};
export { debug, global, migrate, express, access, nginx, ssl, certbot, importer, setup, ipRanges, remoteVersion };
+11 -13
View File
@@ -1,15 +1,13 @@
const db = require('./db');
const logger = require('./logger').migrate;
import db from "./db.js";
import { migrate as logger } from "./logger.js";
module.exports = {
latest: function () {
return db.migrate.currentVersion()
.then((version) => {
logger.info('Current database version:', version);
return db.migrate.latest({
tableName: 'migrations',
directory: 'migrations'
});
});
}
const migrateUp = async () => {
const version = await db().migrate.currentVersion();
logger.info("Current database version:", version);
return await db().migrate.latest({
tableName: "migrations",
directory: "migrations",
});
};
export { migrateUp };
+134 -133
View File
@@ -1,5 +1,6 @@
const migrate_name = 'initial-schema';
const logger = require('../logger').migrate;
import { migrate as logger } from "../logger.js";
const migrateName = "initial-schema";
/**
* Migrate
@@ -7,199 +8,199 @@ const logger = require('../logger').migrate;
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema.createTable('auth', (table) => {
table.increments().primary();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('user_id').notNull().unsigned();
table.string('type', 30).notNull();
table.string('secret').notNull();
table.json('meta').notNull();
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
})
return knex.schema
.createTable("auth", (table) => {
table.increments().primary();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("user_id").notNull().unsigned();
table.string("type", 30).notNull();
table.string("secret").notNull();
table.json("meta").notNull();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
})
.then(() => {
logger.info('[' + migrate_name + '] auth Table created');
logger.info(`[${migrateName}] auth Table created`);
return knex.schema.createTable('user', (table) => {
return knex.schema.createTable("user", (table) => {
table.increments().primary();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.integer('is_disabled').notNull().unsigned().defaultTo(0);
table.string('email').notNull();
table.string('name').notNull();
table.string('nickname').notNull();
table.string('avatar').notNull();
table.json('roles').notNull();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.integer("is_disabled").notNull().unsigned().defaultTo(0);
table.string("email").notNull();
table.string("name").notNull();
table.string("nickname").notNull();
table.string("avatar").notNull();
table.json("roles").notNull();
});
})
.then(() => {
logger.info('[' + migrate_name + '] user Table created');
logger.info(`[${migrateName}] user Table created`);
return knex.schema.createTable('user_permission', (table) => {
return knex.schema.createTable("user_permission", (table) => {
table.increments().primary();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('user_id').notNull().unsigned();
table.string('visibility').notNull();
table.string('proxy_hosts').notNull();
table.string('redirection_hosts').notNull();
table.string('dead_hosts').notNull();
table.string('streams').notNull();
table.string('access_lists').notNull();
table.string('certificates').notNull();
table.unique('user_id');
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("user_id").notNull().unsigned();
table.string("visibility").notNull();
table.string("proxy_hosts").notNull();
table.string("redirection_hosts").notNull();
table.string("dead_hosts").notNull();
table.string("streams").notNull();
table.string("access_lists").notNull();
table.string("certificates").notNull();
table.unique("user_id");
});
})
.then(() => {
logger.info('[' + migrate_name + '] user_permission Table created');
logger.info(`[${migrateName}] user_permission Table created`);
return knex.schema.createTable('proxy_host', (table) => {
return knex.schema.createTable("proxy_host", (table) => {
table.increments().primary();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('owner_user_id').notNull().unsigned();
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.json('domain_names').notNull();
table.string('forward_ip').notNull();
table.integer('forward_port').notNull().unsigned();
table.integer('access_list_id').notNull().unsigned().defaultTo(0);
table.integer('certificate_id').notNull().unsigned().defaultTo(0);
table.integer('ssl_forced').notNull().unsigned().defaultTo(0);
table.integer('caching_enabled').notNull().unsigned().defaultTo(0);
table.integer('block_exploits').notNull().unsigned().defaultTo(0);
table.text('advanced_config').notNull().defaultTo('');
table.json('meta').notNull();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("owner_user_id").notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.json("domain_names").notNull();
table.string("forward_ip").notNull();
table.integer("forward_port").notNull().unsigned();
table.integer("access_list_id").notNull().unsigned().defaultTo(0);
table.integer("certificate_id").notNull().unsigned().defaultTo(0);
table.integer("ssl_forced").notNull().unsigned().defaultTo(0);
table.integer("caching_enabled").notNull().unsigned().defaultTo(0);
table.integer("block_exploits").notNull().unsigned().defaultTo(0);
table.text("advanced_config").notNull().defaultTo("");
table.json("meta").notNull();
});
})
.then(() => {
logger.info('[' + migrate_name + '] proxy_host Table created');
logger.info(`[${migrateName}] proxy_host Table created`);
return knex.schema.createTable('redirection_host', (table) => {
return knex.schema.createTable("redirection_host", (table) => {
table.increments().primary();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('owner_user_id').notNull().unsigned();
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.json('domain_names').notNull();
table.string('forward_domain_name').notNull();
table.integer('preserve_path').notNull().unsigned().defaultTo(0);
table.integer('certificate_id').notNull().unsigned().defaultTo(0);
table.integer('ssl_forced').notNull().unsigned().defaultTo(0);
table.integer('block_exploits').notNull().unsigned().defaultTo(0);
table.text('advanced_config').notNull().defaultTo('');
table.json('meta').notNull();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("owner_user_id").notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.json("domain_names").notNull();
table.string("forward_domain_name").notNull();
table.integer("preserve_path").notNull().unsigned().defaultTo(0);
table.integer("certificate_id").notNull().unsigned().defaultTo(0);
table.integer("ssl_forced").notNull().unsigned().defaultTo(0);
table.integer("block_exploits").notNull().unsigned().defaultTo(0);
table.text("advanced_config").notNull().defaultTo("");
table.json("meta").notNull();
});
})
.then(() => {
logger.info('[' + migrate_name + '] redirection_host Table created');
logger.info(`[${migrateName}] redirection_host Table created`);
return knex.schema.createTable('dead_host', (table) => {
return knex.schema.createTable("dead_host", (table) => {
table.increments().primary();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('owner_user_id').notNull().unsigned();
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.json('domain_names').notNull();
table.integer('certificate_id').notNull().unsigned().defaultTo(0);
table.integer('ssl_forced').notNull().unsigned().defaultTo(0);
table.text('advanced_config').notNull().defaultTo('');
table.json('meta').notNull();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("owner_user_id").notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.json("domain_names").notNull();
table.integer("certificate_id").notNull().unsigned().defaultTo(0);
table.integer("ssl_forced").notNull().unsigned().defaultTo(0);
table.text("advanced_config").notNull().defaultTo("");
table.json("meta").notNull();
});
})
.then(() => {
logger.info('[' + migrate_name + '] dead_host Table created');
logger.info(`[${migrateName}] dead_host Table created`);
return knex.schema.createTable('stream', (table) => {
return knex.schema.createTable("stream", (table) => {
table.increments().primary();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('owner_user_id').notNull().unsigned();
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.integer('incoming_port').notNull().unsigned();
table.string('forward_ip').notNull();
table.integer('forwarding_port').notNull().unsigned();
table.integer('tcp_forwarding').notNull().unsigned().defaultTo(0);
table.integer('udp_forwarding').notNull().unsigned().defaultTo(0);
table.json('meta').notNull();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("owner_user_id").notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.integer("incoming_port").notNull().unsigned();
table.string("forward_ip").notNull();
table.integer("forwarding_port").notNull().unsigned();
table.integer("tcp_forwarding").notNull().unsigned().defaultTo(0);
table.integer("udp_forwarding").notNull().unsigned().defaultTo(0);
table.json("meta").notNull();
});
})
.then(() => {
logger.info('[' + migrate_name + '] stream Table created');
logger.info(`[${migrateName}] stream Table created`);
return knex.schema.createTable('access_list', (table) => {
return knex.schema.createTable("access_list", (table) => {
table.increments().primary();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('owner_user_id').notNull().unsigned();
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.string('name').notNull();
table.json('meta').notNull();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("owner_user_id").notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.string("name").notNull();
table.json("meta").notNull();
});
})
.then(() => {
logger.info('[' + migrate_name + '] access_list Table created');
logger.info(`[${migrateName}] access_list Table created`);
return knex.schema.createTable('certificate', (table) => {
return knex.schema.createTable("certificate", (table) => {
table.increments().primary();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('owner_user_id').notNull().unsigned();
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.string('provider').notNull();
table.string('nice_name').notNull().defaultTo('');
table.json('domain_names').notNull();
table.dateTime('expires_on').notNull();
table.json('meta').notNull();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("owner_user_id").notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.string("provider").notNull();
table.string("nice_name").notNull().defaultTo("");
table.json("domain_names").notNull();
table.dateTime("expires_on").notNull();
table.json("meta").notNull();
});
})
.then(() => {
logger.info('[' + migrate_name + '] certificate Table created');
logger.info(`[${migrateName}] certificate Table created`);
return knex.schema.createTable('access_list_auth', (table) => {
return knex.schema.createTable("access_list_auth", (table) => {
table.increments().primary();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('access_list_id').notNull().unsigned();
table.string('username').notNull();
table.string('password').notNull();
table.json('meta').notNull();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("access_list_id").notNull().unsigned();
table.string("username").notNull();
table.string("password").notNull();
table.json("meta").notNull();
});
})
.then(() => {
logger.info('[' + migrate_name + '] access_list_auth Table created');
logger.info(`[${migrateName}] access_list_auth Table created`);
return knex.schema.createTable('audit_log', (table) => {
return knex.schema.createTable("audit_log", (table) => {
table.increments().primary();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('user_id').notNull().unsigned();
table.string('object_type').notNull().defaultTo('');
table.integer('object_id').notNull().unsigned().defaultTo(0);
table.string('action').notNull();
table.json('meta').notNull();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("user_id").notNull().unsigned();
table.string("object_type").notNull().defaultTo("");
table.integer("object_id").notNull().unsigned().defaultTo(0);
table.string("action").notNull();
table.json("meta").notNull();
});
})
.then(() => {
logger.info('[' + migrate_name + '] audit_log Table created');
logger.info(`[${migrateName}] audit_log Table created`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down the initial data.');
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down the initial data.`);
return Promise.resolve(true);
};
export { up, down };
+15 -14
View File
@@ -1,5 +1,6 @@
const migrate_name = 'websockets';
const logger = require('../logger').migrate;
import { migrate as logger } from "../logger.js";
const migrateName = "websockets";
/**
* Migrate
@@ -7,29 +8,29 @@ const logger = require('../logger').migrate;
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.integer('allow_websocket_upgrade').notNull().unsigned().defaultTo(0);
})
return knex.schema
.table("proxy_host", (proxy_host) => {
proxy_host.integer("allow_websocket_upgrade").notNull().unsigned().defaultTo(0);
})
.then(() => {
logger.info('[' + migrate_name + '] proxy_host Table altered');
logger.info(`[${migrateName}] proxy_host Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
return Promise.resolve(true);
};
};
export { up, down };
@@ -1,5 +1,6 @@
const migrate_name = 'forward_host';
const logger = require('../logger').migrate;
import { migrate as logger } from "../logger.js";
const migrateName = "forward_host";
/**
* Migrate
@@ -7,17 +8,17 @@ const logger = require('../logger').migrate;
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.renameColumn('forward_ip', 'forward_host');
})
return knex.schema
.table("proxy_host", (proxy_host) => {
proxy_host.renameColumn("forward_ip", "forward_host");
})
.then(() => {
logger.info('[' + migrate_name + '] proxy_host Table altered');
logger.info(`[${migrateName}] proxy_host Table altered`);
});
};
@@ -25,10 +26,11 @@ exports.up = function (knex/*, Promise*/) {
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
return Promise.resolve(true);
};
};
export { up, down };
@@ -1,5 +1,6 @@
const migrate_name = 'http2_support';
const logger = require('../logger').migrate;
import { migrate as logger } from "../logger.js";
const migrateName = "http2_support";
/**
* Migrate
@@ -7,31 +8,31 @@ const logger = require('../logger').migrate;
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.integer('http2_support').notNull().unsigned().defaultTo(0);
})
return knex.schema
.table("proxy_host", (proxy_host) => {
proxy_host.integer("http2_support").notNull().unsigned().defaultTo(0);
})
.then(() => {
logger.info('[' + migrate_name + '] proxy_host Table altered');
logger.info(`[${migrateName}] proxy_host Table altered`);
return knex.schema.table('redirection_host', function (redirection_host) {
redirection_host.integer('http2_support').notNull().unsigned().defaultTo(0);
return knex.schema.table("redirection_host", (redirection_host) => {
redirection_host.integer("http2_support").notNull().unsigned().defaultTo(0);
});
})
.then(() => {
logger.info('[' + migrate_name + '] redirection_host Table altered');
logger.info(`[${migrateName}] redirection_host Table altered`);
return knex.schema.table('dead_host', function (dead_host) {
dead_host.integer('http2_support').notNull().unsigned().defaultTo(0);
return knex.schema.table("dead_host", (dead_host) => {
dead_host.integer("http2_support").notNull().unsigned().defaultTo(0);
});
})
.then(() => {
logger.info('[' + migrate_name + '] dead_host Table altered');
logger.info(`[${migrateName}] dead_host Table altered`);
});
};
@@ -39,11 +40,11 @@ exports.up = function (knex/*, Promise*/) {
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
return Promise.resolve(true);
};
export { up, down };
@@ -1,5 +1,6 @@
const migrate_name = 'forward_scheme';
const logger = require('../logger').migrate;
import { migrate as logger } from "../logger.js";
const migrateName = "forward_scheme";
/**
* Migrate
@@ -7,17 +8,17 @@ const logger = require('../logger').migrate;
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.string('forward_scheme').notNull().defaultTo('http');
})
return knex.schema
.table("proxy_host", (proxy_host) => {
proxy_host.string("forward_scheme").notNull().defaultTo("http");
})
.then(() => {
logger.info('[' + migrate_name + '] proxy_host Table altered');
logger.info(`[${migrateName}] proxy_host Table altered`);
});
};
@@ -25,10 +26,11 @@ exports.up = function (knex/*, Promise*/) {
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
return Promise.resolve(true);
};
export { up, down };
+23 -21
View File
@@ -1,5 +1,6 @@
const migrate_name = 'disabled';
const logger = require('../logger').migrate;
import { migrate as logger } from "../logger.js";
const migrateName = "disabled";
/**
* Migrate
@@ -7,38 +8,38 @@ const logger = require('../logger').migrate;
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.integer('enabled').notNull().unsigned().defaultTo(1);
})
return knex.schema
.table("proxy_host", (proxy_host) => {
proxy_host.integer("enabled").notNull().unsigned().defaultTo(1);
})
.then(() => {
logger.info('[' + migrate_name + '] proxy_host Table altered');
logger.info(`[${migrateName}] proxy_host Table altered`);
return knex.schema.table('redirection_host', function (redirection_host) {
redirection_host.integer('enabled').notNull().unsigned().defaultTo(1);
return knex.schema.table("redirection_host", (redirection_host) => {
redirection_host.integer("enabled").notNull().unsigned().defaultTo(1);
});
})
.then(() => {
logger.info('[' + migrate_name + '] redirection_host Table altered');
logger.info(`[${migrateName}] redirection_host Table altered`);
return knex.schema.table('dead_host', function (dead_host) {
dead_host.integer('enabled').notNull().unsigned().defaultTo(1);
return knex.schema.table("dead_host", (dead_host) => {
dead_host.integer("enabled").notNull().unsigned().defaultTo(1);
});
})
.then(() => {
logger.info('[' + migrate_name + '] dead_host Table altered');
logger.info(`[${migrateName}] dead_host Table altered`);
return knex.schema.table('stream', function (stream) {
stream.integer('enabled').notNull().unsigned().defaultTo(1);
return knex.schema.table("stream", (stream) => {
stream.integer("enabled").notNull().unsigned().defaultTo(1);
});
})
.then(() => {
logger.info('[' + migrate_name + '] stream Table altered');
logger.info(`[${migrateName}] stream Table altered`);
});
};
@@ -46,10 +47,11 @@ exports.up = function (knex/*, Promise*/) {
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
return Promise.resolve(true);
};
export { up, down };
@@ -1,5 +1,6 @@
const migrate_name = 'custom_locations';
const logger = require('../logger').migrate;
import { migrate as logger } from "../logger.js";
const migrateName = "custom_locations";
/**
* Migrate
@@ -8,17 +9,17 @@ const logger = require('../logger').migrate;
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.json('locations');
})
return knex.schema
.table("proxy_host", (proxy_host) => {
proxy_host.json("locations");
})
.then(() => {
logger.info('[' + migrate_name + '] proxy_host Table altered');
logger.info(`[${migrateName}] proxy_host Table altered`);
});
};
@@ -26,10 +27,11 @@ exports.up = function (knex/*, Promise*/) {
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
return Promise.resolve(true);
};
export { up, down };
+23 -21
View File
@@ -1,5 +1,6 @@
const migrate_name = 'hsts';
const logger = require('../logger').migrate;
import { migrate as logger } from "../logger.js";
const migrateName = "hsts";
/**
* Migrate
@@ -7,34 +8,34 @@ const logger = require('../logger').migrate;
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0);
proxy_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0);
})
return knex.schema
.table("proxy_host", (proxy_host) => {
proxy_host.integer("hsts_enabled").notNull().unsigned().defaultTo(0);
proxy_host.integer("hsts_subdomains").notNull().unsigned().defaultTo(0);
})
.then(() => {
logger.info('[' + migrate_name + '] proxy_host Table altered');
logger.info(`[${migrateName}] proxy_host Table altered`);
return knex.schema.table('redirection_host', function (redirection_host) {
redirection_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0);
redirection_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0);
return knex.schema.table("redirection_host", (redirection_host) => {
redirection_host.integer("hsts_enabled").notNull().unsigned().defaultTo(0);
redirection_host.integer("hsts_subdomains").notNull().unsigned().defaultTo(0);
});
})
.then(() => {
logger.info('[' + migrate_name + '] redirection_host Table altered');
logger.info(`[${migrateName}] redirection_host Table altered`);
return knex.schema.table('dead_host', function (dead_host) {
dead_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0);
dead_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0);
return knex.schema.table("dead_host", (dead_host) => {
dead_host.integer("hsts_enabled").notNull().unsigned().defaultTo(0);
dead_host.integer("hsts_subdomains").notNull().unsigned().defaultTo(0);
});
})
.then(() => {
logger.info('[' + migrate_name + '] dead_host Table altered');
logger.info(`[${migrateName}] dead_host Table altered`);
});
};
@@ -42,10 +43,11 @@ exports.up = function (knex/*, Promise*/) {
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
return Promise.resolve(true);
};
export { up, down };
+10 -9
View File
@@ -1,5 +1,6 @@
const migrate_name = 'settings';
const logger = require('../logger').migrate;
import { migrate as logger } from "../logger.js";
const migrateName = "settings";
/**
* Migrate
@@ -7,11 +8,10 @@ const logger = require('../logger').migrate;
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema.createTable('setting', (table) => {
table.string('id').notNull().primary();
@@ -21,7 +21,7 @@ exports.up = function (knex/*, Promise*/) {
table.json('meta').notNull();
})
.then(() => {
logger.info('[' + migrate_name + '] setting Table created');
logger.info(`[${migrateName}] setting Table created`);
});
};
@@ -29,10 +29,11 @@ exports.up = function (knex/*, Promise*/) {
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down the initial data.');
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down the initial data.`);
return Promise.resolve(true);
};
export { up, down };
@@ -1,5 +1,6 @@
const migrate_name = 'access_list_client';
const logger = require('../logger').migrate;
import { migrate as logger } from "../logger.js";
const migrateName = "access_list_client";
/**
* Migrate
@@ -7,32 +8,30 @@ const logger = require('../logger').migrate;
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema
.createTable("access_list_client", (table) => {
table.increments().primary();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("access_list_id").notNull().unsigned();
table.string("address").notNull();
table.string("directive").notNull();
table.json("meta").notNull();
})
.then(() => {
logger.info(`[${migrateName}] access_list_client Table created`);
return knex.schema.createTable('access_list_client', (table) => {
table.increments().primary();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('access_list_id').notNull().unsigned();
table.string('address').notNull();
table.string('directive').notNull();
table.json('meta').notNull();
})
.then(function () {
logger.info('[' + migrate_name + '] access_list_client Table created');
return knex.schema.table('access_list', function (access_list) {
access_list.integer('satify_any').notNull().defaultTo(0);
return knex.schema.table("access_list", (access_list) => {
access_list.integer("satify_any").notNull().defaultTo(0);
});
})
.then(() => {
logger.info('[' + migrate_name + '] access_list Table altered');
logger.info(`[${migrateName}] access_list Table altered`);
});
};
@@ -40,14 +39,14 @@ exports.up = function (knex/*, Promise*/) {
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Down...');
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema.dropTable('access_list_client')
.then(() => {
logger.info('[' + migrate_name + '] access_list_client Table dropped');
});
return knex.schema.dropTable("access_list_client").then(() => {
logger.info(`[${migrateName}] access_list_client Table dropped`);
});
};
export { up, down };
@@ -1,5 +1,6 @@
const migrate_name = 'access_list_client_fix';
const logger = require('../logger').migrate;
import { migrate as logger } from "../logger.js";
const migrateName = "access_list_client_fix";
/**
* Migrate
@@ -7,17 +8,17 @@ const logger = require('../logger').migrate;
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema.table('access_list', function (access_list) {
access_list.renameColumn('satify_any', 'satisfy_any');
})
return knex.schema
.table("access_list", (access_list) => {
access_list.renameColumn("satify_any", "satisfy_any");
})
.then(() => {
logger.info('[' + migrate_name + '] access_list Table altered');
logger.info(`[${migrateName}] access_list Table altered`);
});
};
@@ -25,10 +26,11 @@ exports.up = function (knex/*, Promise*/) {
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
return Promise.resolve(true);
};
export { up, down };
+19 -17
View File
@@ -1,5 +1,6 @@
const migrate_name = 'pass_auth';
const logger = require('../logger').migrate;
import { migrate as logger } from "../logger.js";
const migrateName = "pass_auth";
/**
* Migrate
@@ -7,18 +8,17 @@ const logger = require('../logger').migrate;
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema.table('access_list', function (access_list) {
access_list.integer('pass_auth').notNull().defaultTo(1);
})
return knex.schema
.table("access_list", (access_list) => {
access_list.integer("pass_auth").notNull().defaultTo(1);
})
.then(() => {
logger.info('[' + migrate_name + '] access_list Table altered');
logger.info(`[${migrateName}] access_list Table altered`);
});
};
@@ -26,16 +26,18 @@ exports.up = function (knex/*, Promise*/) {
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Down...');
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema.table('access_list', function (access_list) {
access_list.dropColumn('pass_auth');
})
return knex.schema
.table("access_list", (access_list) => {
access_list.dropColumn("pass_auth");
})
.then(() => {
logger.info('[' + migrate_name + '] access_list pass_auth Column dropped');
logger.info(`[${migrateName}] access_list pass_auth Column dropped`);
});
};
export { up, down };
@@ -1,5 +1,6 @@
const migrate_name = 'redirection_scheme';
const logger = require('../logger').migrate;
import { migrate as logger } from "../logger.js";
const migrateName = "redirection_scheme";
/**
* Migrate
@@ -7,18 +8,17 @@ const logger = require('../logger').migrate;
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema.table('redirection_host', (table) => {
table.string('forward_scheme').notNull().defaultTo('$scheme');
})
.then(function () {
logger.info('[' + migrate_name + '] redirection_host Table altered');
return knex.schema
.table("redirection_host", (table) => {
table.string("forward_scheme").notNull().defaultTo("$scheme");
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
});
};
@@ -26,16 +26,18 @@ exports.up = function (knex/*, Promise*/) {
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Down...');
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema.table('redirection_host', (table) => {
table.dropColumn('forward_scheme');
})
.then(function () {
logger.info('[' + migrate_name + '] redirection_host Table altered');
return knex.schema
.table("redirection_host", (table) => {
table.dropColumn("forward_scheme");
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
});
};
export { up, down };
@@ -1,5 +1,6 @@
const migrate_name = 'redirection_status_code';
const logger = require('../logger').migrate;
import { migrate as logger } from "../logger.js";
const migrateName = "redirection_status_code";
/**
* Migrate
@@ -7,18 +8,17 @@ const logger = require('../logger').migrate;
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema.table('redirection_host', (table) => {
table.integer('forward_http_code').notNull().unsigned().defaultTo(302);
})
.then(function () {
logger.info('[' + migrate_name + '] redirection_host Table altered');
return knex.schema
.table("redirection_host", (table) => {
table.integer("forward_http_code").notNull().unsigned().defaultTo(302);
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
});
};
@@ -26,16 +26,18 @@ exports.up = function (knex/*, Promise*/) {
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Down...');
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema.table('redirection_host', (table) => {
table.dropColumn('forward_http_code');
})
.then(function () {
logger.info('[' + migrate_name + '] redirection_host Table altered');
return knex.schema
.table("redirection_host", (table) => {
table.dropColumn("forward_http_code");
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
});
};
export { up, down };
@@ -1,40 +1,43 @@
const migrate_name = 'stream_domain';
const logger = require('../logger').migrate;
import { migrate as logger } from "../logger.js";
const migrateName = "stream_domain";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema.table('stream', (table) => {
table.renameColumn('forward_ip', 'forwarding_host');
})
.then(function () {
logger.info('[' + migrate_name + '] stream Table altered');
return knex.schema
.table("stream", (table) => {
table.renameColumn("forward_ip", "forwarding_host");
})
.then(() => {
logger.info(`[${migrateName}] stream Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Down...');
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema.table('stream', (table) => {
table.renameColumn('forwarding_host', 'forward_ip');
})
.then(function () {
logger.info('[' + migrate_name + '] stream Table altered');
return knex.schema
.table("stream", (table) => {
table.renameColumn("forwarding_host", "forward_ip");
})
.then(() => {
logger.info(`[${migrateName}] stream Table altered`);
});
};
export { up, down };
@@ -1,17 +1,19 @@
const migrate_name = 'stream_domain';
const logger = require('../logger').migrate;
const internalNginx = require('../internal/nginx');
import internalNginx from "../internal/nginx.js";
import { migrate as logger } from "../logger.js";
const migrateName = "stream_domain";
async function regenerateDefaultHost(knex) {
const row = await knex('setting').select('*').where('id', 'default-site').first();
const row = await knex("setting").select("*").where("id", "default-site").first();
if (!row) {
return Promise.resolve();
}
return internalNginx.deleteConfig('default')
return internalNginx
.deleteConfig("default")
.then(() => {
return internalNginx.generateConfig('default', row);
return internalNginx.generateConfig("default", row);
})
.then(() => {
return internalNginx.test();
@@ -22,29 +24,29 @@ async function regenerateDefaultHost(knex) {
}
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex) {
logger.info('[' + migrate_name + '] Migrating Up...');
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return regenerateDefaultHost(knex);
};
/**
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex) {
logger.info('[' + migrate_name + '] Migrating Down...');
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return regenerateDefaultHost(knex);
};
};
export { up, down };
@@ -0,0 +1,43 @@
import { migrate as logger } from "../logger.js";
const migrateName = "stream_ssl";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.table("stream", (table) => {
table.integer("certificate_id").notNull().unsigned().defaultTo(0);
})
.then(() => {
logger.info(`[${migrateName}] stream Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema
.table("stream", (table) => {
table.dropColumn("certificate_id");
})
.then(() => {
logger.info(`[${migrateName}] stream Table altered`);
});
};
export { up, down };
@@ -0,0 +1,50 @@
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 };
+52 -57
View File
@@ -1,103 +1,98 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
const db = require('../db');
const helpers = require('../lib/helpers');
const Model = require('objection').Model;
const User = require('./user');
const AccessListAuth = require('./access_list_auth');
const AccessListClient = require('./access_list_client');
const now = require('./now_helper');
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import AccessListAuth from "./access_list_auth.js";
import AccessListClient from "./access_list_client.js";
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',
];
const boolFields = ["is_deleted", "satisfy_any", "pass_auth"];
class AccessList extends Model {
$beforeInsert () {
this.created_on = now();
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === 'undefined') {
if (typeof this.meta === "undefined") {
this.meta = {};
}
}
$beforeUpdate () {
$beforeUpdate() {
this.modified_on = now();
}
$parseDatabaseJson(json) {
json = super.$parseDatabaseJson(json);
return helpers.convertIntFieldsToBool(json, boolFields);
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
}
$formatDatabaseJson(json) {
json = helpers.convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(json);
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
}
static get name () {
return 'AccessList';
static get name() {
return "AccessList";
}
static get tableName () {
return 'access_list';
static get tableName() {
return "access_list";
}
static get jsonAttributes () {
return ['meta'];
static get jsonAttributes() {
return ["meta"];
}
static get relationMappings () {
const ProxyHost = require('./proxy_host');
static get relationMappings() {
return {
owner: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: 'access_list.owner_user_id',
to: 'user.id'
join: {
from: "access_list.owner_user_id",
to: "user.id",
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
},
modify: function (qb) {
qb.where('user.is_deleted', 0);
}
},
items: {
relation: Model.HasManyRelation,
relation: Model.HasManyRelation,
modelClass: AccessListAuth,
join: {
from: 'access_list.id',
to: 'access_list_auth.access_list_id'
}
join: {
from: "access_list.id",
to: "access_list_auth.access_list_id",
},
},
clients: {
relation: Model.HasManyRelation,
relation: Model.HasManyRelation,
modelClass: AccessListClient,
join: {
from: 'access_list.id',
to: 'access_list_client.access_list_id'
}
join: {
from: "access_list.id",
to: "access_list_client.access_list_id",
},
},
proxy_hosts: {
relation: Model.HasManyRelation,
modelClass: ProxyHost,
join: {
from: 'access_list.id',
to: 'proxy_host.access_list_id'
relation: Model.HasManyRelation,
modelClass: ProxyHostModel,
join: {
from: "access_list.id",
to: "proxy_host.access_list_id",
},
modify: function (qb) {
qb.where('proxy_host.is_deleted', 0);
}
}
modify: (qb) => {
qb.where("proxy_host.is_deleted", 0);
},
},
};
}
}
module.exports = AccessList;
export default AccessList;
+26 -25
View File
@@ -1,54 +1,55 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
const db = require('../db');
const Model = require('objection').Model;
const now = require('./now_helper');
import { Model } from "objection";
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 () {
this.created_on = now();
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === 'undefined') {
if (typeof this.meta === "undefined") {
this.meta = {};
}
}
$beforeUpdate () {
$beforeUpdate() {
this.modified_on = now();
}
static get name () {
return 'AccessListAuth';
static get name() {
return "AccessListAuth";
}
static get tableName () {
return 'access_list_auth';
static get tableName() {
return "access_list_auth";
}
static get jsonAttributes () {
return ['meta'];
static get jsonAttributes() {
return ["meta"];
}
static get relationMappings () {
static get relationMappings() {
return {
access_list: {
relation: Model.HasOneRelation,
modelClass: require('./access_list'),
join: {
from: 'access_list_auth.access_list_id',
to: 'access_list.id'
relation: Model.HasOneRelation,
modelClass: accessListModel,
join: {
from: "access_list_auth.access_list_id",
to: "access_list.id",
},
modify: function (qb) {
qb.where('access_list.is_deleted', 0);
}
}
modify: (qb) => {
qb.where("access_list.is_deleted", 0);
},
},
};
}
}
module.exports = AccessListAuth;
export default AccessListAuth;
+26 -25
View File
@@ -1,54 +1,55 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
const db = require('../db');
const Model = require('objection').Model;
const now = require('./now_helper');
import { Model } from "objection";
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 () {
this.created_on = now();
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === 'undefined') {
if (typeof this.meta === "undefined") {
this.meta = {};
}
}
$beforeUpdate () {
$beforeUpdate() {
this.modified_on = now();
}
static get name () {
return 'AccessListClient';
static get name() {
return "AccessListClient";
}
static get tableName () {
return 'access_list_client';
static get tableName() {
return "access_list_client";
}
static get jsonAttributes () {
return ['meta'];
static get jsonAttributes() {
return ["meta"];
}
static get relationMappings () {
static get relationMappings() {
return {
access_list: {
relation: Model.HasOneRelation,
modelClass: require('./access_list'),
join: {
from: 'access_list_client.access_list_id',
to: 'access_list.id'
relation: Model.HasOneRelation,
modelClass: accessListModel,
join: {
from: "access_list_client.access_list_id",
to: "access_list.id",
},
modify: function (qb) {
qb.where('access_list.is_deleted', 0);
}
}
modify: (qb) => {
qb.where("access_list.is_deleted", 0);
},
},
};
}
}
module.exports = AccessListClient;
export default AccessListClient;
+23 -23
View File
@@ -1,52 +1,52 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
const db = require('../db');
const Model = require('objection').Model;
const User = require('./user');
const now = require('./now_helper');
import { Model } from "objection";
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 () {
this.created_on = now();
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === 'undefined') {
if (typeof this.meta === "undefined") {
this.meta = {};
}
}
$beforeUpdate () {
$beforeUpdate() {
this.modified_on = now();
}
static get name () {
return 'AuditLog';
static get name() {
return "AuditLog";
}
static get tableName () {
return 'audit_log';
static get tableName() {
return "audit_log";
}
static get jsonAttributes () {
return ['meta'];
static get jsonAttributes() {
return ["meta"];
}
static get relationMappings () {
static get relationMappings() {
return {
user: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: 'audit_log.user_id',
to: 'user.id'
}
}
join: {
from: "audit_log.user_id",
to: "user.id",
},
},
};
}
}
module.exports = AuditLog;
export default AuditLog;
+37 -43
View File
@@ -1,59 +1,53 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
const bcrypt = require('bcrypt');
const db = require('../db');
const helpers = require('../lib/helpers');
const Model = require('objection').Model;
const User = require('./user');
const now = require('./now_helper');
import bcrypt from "bcrypt";
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import now from "./now_helper.js";
import User from "./user.js";
Model.knex(db);
Model.knex(db());
const boolFields = [
'is_deleted',
];
const boolFields = ["is_deleted"];
function encryptPassword () {
/* jshint -W040 */
let _this = this;
if (_this.type === 'password' && _this.secret) {
return bcrypt.hash(_this.secret, 13)
.then(function (hash) {
_this.secret = hash;
});
function encryptPassword() {
if (this.type === "password" && this.secret) {
return bcrypt.hash(this.secret, 13).then((hash) => {
this.secret = hash;
});
}
return null;
}
class Auth extends Model {
$beforeInsert (queryContext) {
this.created_on = now();
$beforeInsert(queryContext) {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === 'undefined') {
if (typeof this.meta === "undefined") {
this.meta = {};
}
return encryptPassword.apply(this, queryContext);
}
$beforeUpdate (queryContext) {
$beforeUpdate(queryContext) {
this.modified_on = now();
return encryptPassword.apply(this, queryContext);
}
$parseDatabaseJson(json) {
json = super.$parseDatabaseJson(json);
return helpers.convertIntFieldsToBool(json, boolFields);
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
}
$formatDatabaseJson(json) {
json = helpers.convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(json);
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
}
/**
@@ -62,37 +56,37 @@ class Auth extends Model {
* @param {String} password
* @returns {Promise}
*/
verifyPassword (password) {
verifyPassword(password) {
return bcrypt.compare(password, this.secret);
}
static get name () {
return 'Auth';
static get name() {
return "Auth";
}
static get tableName () {
return 'auth';
static get tableName() {
return "auth";
}
static get jsonAttributes () {
return ['meta'];
static get jsonAttributes() {
return ["meta"];
}
static get relationMappings () {
static get relationMappings() {
return {
user: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: 'auth.user_id',
to: 'user.id'
join: {
from: "auth.user_id",
to: "user.id",
},
filter: {
is_deleted: 0
}
}
is_deleted: 0,
},
},
};
}
}
module.exports = Auth;
export default Auth;
+96 -42
View File
@@ -1,87 +1,141 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
const db = require('../db');
const helpers = require('../lib/helpers');
const Model = require('objection').Model;
const User = require('./user');
const now = require('./now_helper');
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import deadHostModel from "./dead_host.js";
import now from "./now_helper.js";
import proxyHostModel from "./proxy_host.js";
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 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();
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for expires_on
if (typeof this.expires_on === 'undefined') {
if (typeof this.expires_on === "undefined") {
this.expires_on = now();
}
// 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 = [];
}
// Default for meta
if (typeof this.meta === 'undefined') {
if (typeof this.meta === "undefined") {
this.meta = {};
}
this.domain_names.sort();
}
$beforeUpdate () {
$beforeUpdate() {
this.modified_on = now();
// Sort domain_names
if (typeof this.domain_names !== 'undefined') {
this.domain_names.sort();
if (typeof this.domain_names !== "undefined") {
this.domain_names = cleanDomainNames(this.domain_names);
}
}
$parseDatabaseJson(json) {
json = super.$parseDatabaseJson(json);
return helpers.convertIntFieldsToBool(json, boolFields);
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
}
$formatDatabaseJson(json) {
json = helpers.convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(json);
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
}
static get name () {
return 'Certificate';
static get name() {
return "Certificate";
}
static get tableName () {
return 'certificate';
static get tableName() {
return "certificate";
}
static get jsonAttributes () {
return ['domain_names', 'meta'];
static get jsonAttributes() {
return ["domain_names", "meta"];
}
static get relationMappings () {
static get relationMappings() {
return {
owner: {
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: 'certificate.owner_user_id',
to: 'user.id'
relation: Model.HasOneRelation,
modelClass: userModel,
join: {
from: "certificate.owner_user_id",
to: "user.id",
},
modify: function (qb) {
qb.where('user.is_deleted', 0);
}
}
modify: (qb) => {
qb.where("user.is_deleted", 0);
},
},
proxy_hosts: {
relation: Model.HasManyRelation,
modelClass: proxyHostModel,
join: {
from: "certificate.id",
to: "proxy_host.certificate_id",
},
modify: (qb) => {
qb.where("proxy_host.is_deleted", 0);
},
},
dead_hosts: {
relation: Model.HasManyRelation,
modelClass: deadHostModel,
join: {
from: "certificate.id",
to: "dead_host.certificate_id",
},
modify: (qb) => {
qb.where("dead_host.is_deleted", 0);
},
},
redirection_hosts: {
relation: Model.HasManyRelation,
modelClass: redirectionHostModel,
join: {
from: "certificate.id",
to: "redirection_host.certificate_id",
},
modify: (qb) => {
qb.where("redirection_host.is_deleted", 0);
},
},
streams: {
relation: Model.HasManyRelation,
modelClass: streamModel,
join: {
from: "certificate.id",
to: "stream.certificate_id",
},
modify: (qb) => {
qb.where("stream.is_deleted", 0);
},
},
};
}
}
module.exports = Certificate;
export default Certificate;
+53 -44
View File
@@ -1,95 +1,104 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
const db = require('../db');
const helpers = require('../lib/helpers');
const Model = require('objection').Model;
const User = require('./user');
const Certificate = require('./certificate');
const now = require('./now_helper');
import { Model } from "objection";
import db from "../db.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',
];
const boolFields = ["is_deleted", "ssl_forced", "http2_support", "enabled", "hsts_enabled", "hsts_subdomains"];
class DeadHost extends Model {
$beforeInsert () {
this.created_on = now();
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for domain_names
if (typeof this.domain_names === 'undefined') {
if (typeof this.domain_names === "undefined") {
this.domain_names = [];
}
// Default for meta
if (typeof this.meta === 'undefined') {
if (typeof this.meta === "undefined") {
this.meta = {};
}
this.domain_names.sort();
}
$beforeUpdate () {
$beforeUpdate() {
this.modified_on = now();
// Sort domain_names
if (typeof this.domain_names !== 'undefined') {
if (typeof this.domain_names !== "undefined") {
this.domain_names.sort();
}
}
$parseDatabaseJson(json) {
json = super.$parseDatabaseJson(json);
return helpers.convertIntFieldsToBool(json, boolFields);
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
}
$formatDatabaseJson(json) {
json = helpers.convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(json);
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
}
static get name () {
return 'DeadHost';
static get name() {
return "DeadHost";
}
static get tableName () {
return 'dead_host';
static get tableName() {
return "dead_host";
}
static get jsonAttributes () {
return ['domain_names', 'meta'];
static get jsonAttributes() {
return ["domain_names", "meta"];
}
static get relationMappings () {
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: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: 'dead_host.owner_user_id',
to: 'user.id'
join: {
from: "dead_host.owner_user_id",
to: "user.id",
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
},
modify: function (qb) {
qb.where('user.is_deleted', 0);
}
},
certificate: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: Certificate,
join: {
from: 'dead_host.certificate_id',
to: 'certificate.id'
join: {
from: "dead_host.certificate_id",
to: "certificate.id",
},
modify: function (qb) {
qb.where('certificate.is_deleted', 0);
}
}
modify: (qb) => {
qb.where("certificate.is_deleted", 0);
},
},
};
}
}
module.exports = DeadHost;
export default DeadHost;
+7 -8
View File
@@ -1,13 +1,12 @@
const db = require('../db');
const config = require('../lib/config');
const Model = require('objection').Model;
import { Model } from "objection";
import db from "../db.js";
import { isSqlite } from "../lib/config.js";
Model.knex(db);
Model.knex(db());
module.exports = function () {
if (config.isSqlite()) {
// eslint-disable-next-line
export default () => {
if (isSqlite()) {
return Model.raw("datetime('now','localtime')");
}
return Model.raw('NOW()');
return Model.raw("NOW()");
};
+70 -57
View File
@@ -1,114 +1,127 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
const db = require('../db');
const helpers = require('../lib/helpers');
const Model = require('objection').Model;
const User = require('./user');
const AccessList = require('./access_list');
const Certificate = require('./certificate');
const now = require('./now_helper');
import { Model } from "objection";
import db from "../db.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',
'ssl_forced',
'caching_enabled',
'block_exploits',
'allow_websocket_upgrade',
'http2_support',
'enabled',
'hsts_enabled',
'hsts_subdomains',
"is_deleted",
"ssl_forced",
"caching_enabled",
"block_exploits",
"allow_websocket_upgrade",
"http2_support",
"enabled",
"hsts_enabled",
"hsts_subdomains",
"trust_forwarded_proto",
];
class ProxyHost extends Model {
$beforeInsert () {
this.created_on = now();
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for domain_names
if (typeof this.domain_names === 'undefined') {
if (typeof this.domain_names === "undefined") {
this.domain_names = [];
}
// Default for meta
if (typeof this.meta === 'undefined') {
if (typeof this.meta === "undefined") {
this.meta = {};
}
this.domain_names.sort();
}
$beforeUpdate () {
$beforeUpdate() {
this.modified_on = now();
// Sort domain_names
if (typeof this.domain_names !== 'undefined') {
if (typeof this.domain_names !== "undefined") {
this.domain_names.sort();
}
}
$parseDatabaseJson(json) {
json = super.$parseDatabaseJson(json);
return helpers.convertIntFieldsToBool(json, boolFields);
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
}
$formatDatabaseJson(json) {
json = helpers.convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(json);
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
}
static get name () {
return 'ProxyHost';
static get name() {
return "ProxyHost";
}
static get tableName () {
return 'proxy_host';
static get tableName() {
return "proxy_host";
}
static get jsonAttributes () {
return ['domain_names', 'meta', 'locations'];
static get jsonAttributes() {
return ["domain_names", "meta", "locations"];
}
static get relationMappings () {
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: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: 'proxy_host.owner_user_id',
to: 'user.id'
join: {
from: "proxy_host.owner_user_id",
to: "user.id",
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
},
modify: function (qb) {
qb.where('user.is_deleted', 0);
}
},
access_list: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: AccessList,
join: {
from: 'proxy_host.access_list_id',
to: 'access_list.id'
join: {
from: "proxy_host.access_list_id",
to: "access_list.id",
},
modify: (qb) => {
qb.where("access_list.is_deleted", 0);
},
modify: function (qb) {
qb.where('access_list.is_deleted', 0);
}
},
certificate: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: Certificate,
join: {
from: 'proxy_host.certificate_id',
to: 'certificate.id'
join: {
from: "proxy_host.certificate_id",
to: "certificate.id",
},
modify: function (qb) {
qb.where('certificate.is_deleted', 0);
}
}
modify: (qb) => {
qb.where("certificate.is_deleted", 0);
},
},
};
}
}
module.exports = ProxyHost;
export default ProxyHost;
+60 -49
View File
@@ -1,102 +1,113 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
const db = require('../db');
const helpers = require('../lib/helpers');
const Model = require('objection').Model;
const User = require('./user');
const Certificate = require('./certificate');
const now = require('./now_helper');
import { Model } from "objection";
import db from "../db.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',
'preserve_path',
'ssl_forced',
'block_exploits',
'hsts_enabled',
'hsts_subdomains',
'http2_support',
"is_deleted",
"enabled",
"preserve_path",
"ssl_forced",
"block_exploits",
"hsts_enabled",
"hsts_subdomains",
"http2_support",
];
class RedirectionHost extends Model {
$beforeInsert () {
this.created_on = now();
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for domain_names
if (typeof this.domain_names === 'undefined') {
if (typeof this.domain_names === "undefined") {
this.domain_names = [];
}
// Default for meta
if (typeof this.meta === 'undefined') {
if (typeof this.meta === "undefined") {
this.meta = {};
}
this.domain_names.sort();
}
$beforeUpdate () {
$beforeUpdate() {
this.modified_on = now();
// Sort domain_names
if (typeof this.domain_names !== 'undefined') {
if (typeof this.domain_names !== "undefined") {
this.domain_names.sort();
}
}
$parseDatabaseJson(json) {
json = super.$parseDatabaseJson(json);
return helpers.convertIntFieldsToBool(json, boolFields);
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
}
$formatDatabaseJson(json) {
json = helpers.convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(json);
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
}
static get name () {
return 'RedirectionHost';
static get name() {
return "RedirectionHost";
}
static get tableName () {
return 'redirection_host';
static get tableName() {
return "redirection_host";
}
static get jsonAttributes () {
return ['domain_names', 'meta'];
static get jsonAttributes() {
return ["domain_names", "meta"];
}
static get relationMappings () {
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: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: 'redirection_host.owner_user_id',
to: 'user.id'
join: {
from: "redirection_host.owner_user_id",
to: "user.id",
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
},
modify: function (qb) {
qb.where('user.is_deleted', 0);
}
},
certificate: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: Certificate,
join: {
from: 'redirection_host.certificate_id',
to: 'certificate.id'
join: {
from: "redirection_host.certificate_id",
to: "certificate.id",
},
modify: function (qb) {
qb.where('certificate.is_deleted', 0);
}
}
modify: (qb) => {
qb.where("certificate.is_deleted", 0);
},
},
};
}
}
module.exports = RedirectionHost;
export default RedirectionHost;
+4 -4
View File
@@ -1,10 +1,10 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
const db = require('../db');
const Model = require('objection').Model;
import { Model } from "objection";
import db from "../db.js";
Model.knex(db);
Model.knex(db());
class Setting extends Model {
$beforeInsert () {
@@ -27,4 +27,4 @@ class Setting extends Model {
}
}
module.exports = Setting;
export default Setting;
+55 -38
View File
@@ -1,72 +1,89 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.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";
const db = require('../db');
const helpers = require('../lib/helpers');
const Model = require('objection').Model;
const User = require('./user');
const now = require('./now_helper');
Model.knex(db());
Model.knex(db);
const boolFields = [
'is_deleted',
'tcp_forwarding',
'udp_forwarding',
];
const boolFields = ["is_deleted", "enabled", "tcp_forwarding", "udp_forwarding"];
class Stream extends Model {
$beforeInsert () {
this.created_on = now();
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === 'undefined') {
if (typeof this.meta === "undefined") {
this.meta = {};
}
}
$beforeUpdate () {
$beforeUpdate() {
this.modified_on = now();
}
$parseDatabaseJson(json) {
json = super.$parseDatabaseJson(json);
return helpers.convertIntFieldsToBool(json, boolFields);
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
}
$formatDatabaseJson(json) {
json = helpers.convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(json);
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
}
static get name () {
return 'Stream';
static get name() {
return "Stream";
}
static get tableName () {
return 'stream';
static get tableName() {
return "stream";
}
static get jsonAttributes () {
return ['meta'];
static get jsonAttributes() {
return ["meta"];
}
static get relationMappings () {
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: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: 'stream.owner_user_id',
to: 'user.id'
join: {
from: "stream.owner_user_id",
to: "user.id",
},
modify: function (qb) {
qb.where('user.is_deleted', 0);
}
}
modify: (qb) => {
qb.where("user.is_deleted", 0);
},
},
certificate: {
relation: Model.HasOneRelation,
modelClass: Certificate,
join: {
from: "stream.certificate_id",
to: "certificate.id",
},
modify: (qb) => {
qb.where("certificate.is_deleted", 0);
},
},
};
}
}
module.exports = Stream;
export default Stream;
+59 -58
View File
@@ -3,17 +3,17 @@
and then has abilities after that.
*/
const _ = require('lodash');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const config = require('../lib/config');
const error = require('../lib/error');
const logger = require('../logger').global;
const ALGO = 'RS256';
import crypto from "node:crypto";
import jwt from "jsonwebtoken";
import _ from "lodash";
import { getPrivateKey, getPublicKey } from "../lib/config.js";
import errs from "../lib/error.js";
import { global as logger } from "../logger.js";
module.exports = function () {
const ALGO = "RS256";
let token_data = {};
export default () => {
let tokenData = {};
const self = {
/**
@@ -21,28 +21,26 @@ module.exports = function () {
* @returns {Promise}
*/
create: (payload) => {
if (!config.getPrivateKey()) {
logger.error('Private key is empty!');
if (!getPrivateKey()) {
logger.error("Private key is empty!");
}
// sign with RSA SHA256
const options = {
algorithm: ALGO,
expiresIn: payload.expiresIn || '1d'
expiresIn: payload.expiresIn || "1d",
};
payload.jti = crypto.randomBytes(12)
.toString('base64')
.substring(-8);
payload.jti = crypto.randomBytes(12).toString("base64").substring(-8);
return new Promise((resolve, reject) => {
jwt.sign(payload, config.getPrivateKey(), options, (err, token) => {
jwt.sign(payload, getPrivateKey(), options, (err, token) => {
if (err) {
reject(err);
} else {
token_data = payload;
tokenData = payload;
resolve({
token: token,
payload: payload
token: token,
payload: payload,
});
}
});
@@ -53,42 +51,47 @@ module.exports = function () {
* @param {String} token
* @returns {Promise}
*/
load: function (token) {
if (!config.getPublicKey()) {
logger.error('Public key is empty!');
load: (token) => {
if (!getPublicKey()) {
logger.error("Public key is empty!");
}
return new Promise((resolve, reject) => {
try {
if (!token || token === null || token === 'null') {
reject(new error.AuthError('Empty token'));
if (!token || token === null || token === "null") {
reject(new errs.AuthError("Empty token"));
} else {
jwt.verify(token, config.getPublicKey(), {ignoreExpiration: false, algorithms: [ALGO]}, (err, result) => {
if (err) {
if (err.name === 'TokenExpiredError') {
reject(new error.AuthError('Token has expired', err));
jwt.verify(
token,
getPublicKey(),
{ ignoreExpiration: false, algorithms: [ALGO] },
(err, result) => {
if (err) {
if (err.name === "TokenExpiredError") {
reject(new errs.AuthError("Token has expired", err));
} else {
reject(err);
}
} else {
reject(err);
tokenData = result;
// Hack: some tokens out in the wild have a scope of 'all' instead of 'user'.
// For 30 days at least, we need to replace 'all' with user.
if (
typeof tokenData.scope !== "undefined" &&
_.indexOf(tokenData.scope, "all") !== -1
) {
tokenData.scope = ["user"];
}
resolve(tokenData);
}
} else {
token_data = result;
// Hack: some tokens out in the wild have a scope of 'all' instead of 'user'.
// For 30 days at least, we need to replace 'all' with user.
if ((typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, 'all') !== -1)) {
token_data.scope = ['user'];
}
resolve(token_data);
}
});
},
);
}
} catch (err) {
reject(err);
}
});
},
/**
@@ -97,17 +100,15 @@ module.exports = function () {
* @param {String} scope
* @returns {Boolean}
*/
hasScope: function (scope) {
return typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, scope) !== -1;
},
hasScope: (scope) => typeof tokenData.scope !== "undefined" && _.indexOf(tokenData.scope, scope) !== -1,
/**
* @param {String} key
* @return {*}
*/
get: function (key) {
if (typeof token_data[key] !== 'undefined') {
return token_data[key];
get: (key) => {
if (typeof tokenData[key] !== "undefined") {
return tokenData[key];
}
return null;
@@ -117,22 +118,22 @@ module.exports = function () {
* @param {String} key
* @param {*} value
*/
set: function (key, value) {
token_data[key] = value;
set: (key, value) => {
tokenData[key] = value;
},
/**
* @param [default_value]
* @param [defaultValue]
* @returns {Integer}
*/
getUserId: (default_value) => {
const attrs = self.get('attrs');
if (attrs && typeof attrs.id !== 'undefined' && attrs.id) {
getUserId: (defaultValue) => {
const attrs = self.get("attrs");
if (attrs?.id) {
return attrs.id;
}
return default_value || 0;
}
return defaultValue || 0;
},
};
return self;
+29 -33
View File
@@ -1,69 +1,65 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
const db = require('../db');
const helpers = require('../lib/helpers');
const Model = require('objection').Model;
const UserPermission = require('./user_permission');
const now = require('./now_helper');
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import now from "./now_helper.js";
import UserPermission from "./user_permission.js";
Model.knex(db);
Model.knex(db());
const boolFields = [
'is_deleted',
'is_disabled',
];
const boolFields = ["is_deleted", "is_disabled"];
class User extends Model {
$beforeInsert () {
this.created_on = now();
$beforeInsert() {
this.created_on = now();
this.modified_on = now();
// Default for roles
if (typeof this.roles === 'undefined') {
if (typeof this.roles === "undefined") {
this.roles = [];
}
}
$beforeUpdate () {
$beforeUpdate() {
this.modified_on = now();
}
$parseDatabaseJson(json) {
json = super.$parseDatabaseJson(json);
return helpers.convertIntFieldsToBool(json, boolFields);
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
}
$formatDatabaseJson(json) {
json = helpers.convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(json);
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
}
static get name () {
return 'User';
static get name() {
return "User";
}
static get tableName () {
return 'user';
static get tableName() {
return "user";
}
static get jsonAttributes () {
return ['roles'];
static get jsonAttributes() {
return ["roles"];
}
static get relationMappings () {
static get relationMappings() {
return {
permissions: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: UserPermission,
join: {
from: 'user.id',
to: 'user_permission.user_id'
}
}
join: {
from: "user.id",
to: "user_permission.user_id",
},
},
};
}
}
module.exports = User;
export default User;
+5 -5
View File
@@ -1,11 +1,11 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
const db = require('../db');
const Model = require('objection').Model;
const now = require('./now_helper');
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 () {
@@ -26,4 +26,4 @@ class UserPermission extends Model {
}
}
module.exports = UserPermission;
export default UserPermission;
+1 -1
View File
@@ -3,5 +3,5 @@
"ignore": [
"data"
],
"ext": "js json ejs"
"ext": "js json ejs cjs"
}
+39 -34
View File
@@ -1,48 +1,53 @@
{
"name": "nginx-proxy-manager",
"version": "0.0.0",
"version": "2.0.0",
"description": "A beautiful interface for creating Nginx endpoints",
"author": "Jamie Curnow <jc@jc21.com>",
"license": "MIT",
"main": "index.js",
"type": "module",
"scripts": {
"lint": "biome lint",
"prettier": "biome format --write .",
"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": "^15.3.5",
"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.1.9",
"gravatar": "^1.8.0",
"jsonwebtoken": "^9.0.0",
"knex": "2.4.2",
"liquidjs": "10.6.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"mysql2": "^3.11.1",
"node-rsa": "^1.0.8",
"objection": "3.0.1",
"bcrypt": "^6.0.0",
"better-sqlite3": "^12.10.0",
"body-parser": "^2.2.2",
"chalk": "5.6.2",
"compression": "^1.8.1",
"express": "^5.2.1",
"express-fileupload": "^1.5.2",
"gravatar": "^1.8.2",
"jsonwebtoken": "^9.0.3",
"knex": "3.2.10",
"liquidjs": "10.27.0",
"lodash": "^4.18.1",
"moment": "^2.30.1",
"mysql2": "^3.22.3",
"node-rsa": "^1.1.1",
"objection": "3.1.5",
"otplib": "^13.4.0",
"path": "^0.12.7",
"pg": "^8.13.1",
"pg": "^8.21.0",
"proxy-agent": "^8.0.1",
"signale": "1.4.0",
"sqlite3": "5.1.6",
"temp-write": "^4.0.0"
"sqlite3": "^6.0.1",
"temp-write": "^6.0.1"
},
"devDependencies": {
"@apidevtools/swagger-parser": "^12.1.0",
"@biomejs/biome": "^2.4.15",
"nodemon": "^3.1.14"
},
"signale": {
"displayDate": true,
"displayTimestamp": true
},
"author": "Jamie Curnow <jc@jc21.com>",
"license": "MIT",
"devDependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"chalk": "4.1.2",
"eslint": "^8.36.0",
"eslint-plugin-align-assignments": "^1.1.2",
"nodemon": "^2.0.2",
"prettier": "^2.0.4"
},
"scripts": {
"validate-schema": "node validate-schema.js"
}
}
+86 -31
View File
@@ -1,19 +1,20 @@
const express = require('express');
const validator = require('../lib/validator');
const jwtdecode = require('../lib/express/jwt-decode');
const internalAuditLog = require('../internal/audit-log');
import express from "express";
import internalAuditLog from "../internal/audit-log.js";
import jwtdecode from "../lib/express/jwt-decode.js";
import validator from "../lib/validator/index.js";
import { debug, express as logger } from "../logger.js";
let router = express.Router({
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
strict: true,
mergeParams: true,
});
/**
* /api/audit-log
*/
router
.route('/')
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
@@ -24,29 +25,83 @@ router
*
* Retrieve all logs
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'common#/properties/expand'
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
query: {
$ref: "common#/properties/query",
},
},
},
query: {
$ref: 'common#/properties/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalAuditLog.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
{
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
query: typeof req.query.query === "string" ? req.query.query : null,
},
);
const rows = await internalAuditLog.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
module.exports = router;
/**
* Specific audit log entry
*
* /api/audit-log/123
*/
router
.route("/:event_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/audit-log/123
*
* Retrieve a specific entry
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["event_id"],
additionalProperties: false,
properties: {
event_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
{
event_id: req.params.event_id,
expand:
typeof req.query.expand === "string"
? req.query.expand.split(",")
: null,
},
);
const item = await internalAuditLog.get(res.locals.access, {
id: data.event_id,
expand: data.expand,
});
res.status(200).send(item);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;
+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;
+56 -29
View File
@@ -1,51 +1,78 @@
const express = require('express');
const pjson = require('../package.json');
const error = require('../lib/error');
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 accessListsRoutes from "./nginx/access_lists.js";
import certificatesHostsRoutes from "./nginx/certificates.js";
import deadHostsRoutes from "./nginx/dead_hosts.js";
import proxyHostsRoutes from "./nginx/proxy_hosts.js";
import redirectionHostsRoutes from "./nginx/redirection_hosts.js";
import streamsRoutes from "./nginx/streams.js";
import reportsRoutes from "./reports.js";
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";
let router = express.Router({
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
strict: true,
mergeParams: true,
});
router.use(logRequest);
/**
* Health Check
* GET /api
*/
router.get('/', (req, res/*, next*/) => {
let version = pjson.version.split('-').shift().split('.');
router.get("/", async (_, res /*, next*/) => {
const version = pjson.version.split("-").shift().split(".");
const setup = await isSetup();
res.status(200).send({
status: 'OK',
status: "OK",
setup,
version: {
major: parseInt(version.shift(), 10),
minor: parseInt(version.shift(), 10),
revision: parseInt(version.shift(), 10)
}
major: Number.parseInt(version.shift(), 10),
minor: Number.parseInt(version.shift(), 10),
revision: Number.parseInt(version.shift(), 10),
},
});
});
router.use('/schema', require('./schema'));
router.use('/tokens', require('./tokens'));
router.use('/users', require('./users'));
router.use('/audit-log', require('./audit-log'));
router.use('/reports', require('./reports'));
router.use('/settings', require('./settings'));
router.use('/nginx/proxy-hosts', require('./nginx/proxy_hosts'));
router.use('/nginx/redirection-hosts', require('./nginx/redirection_hosts'));
router.use('/nginx/dead-hosts', require('./nginx/dead_hosts'));
router.use('/nginx/streams', require('./nginx/streams'));
router.use('/nginx/access-lists', require('./nginx/access_lists'));
router.use('/nginx/certificates', require('./nginx/certificates'));
router.use("/schema", schemaRoutes);
router.use("/tokens", tokensRoutes);
router.use("/users", usersRoutes);
router.use("/audit-log", auditLogRoutes);
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);
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
*
* ALL /api/*
*/
router.all(/(.+)/, function (req, _, next) {
req.params.page = req.params['0'];
next(new error.ItemNotFoundError(req.params.page));
router.all(/(.+)/, (req, _, next) => {
req.params.page = req.params["0"];
next(new errs.ItemNotFoundError(req.params.page));
});
module.exports = router;
export default router;
+95 -89
View File
@@ -1,22 +1,23 @@
const express = require('express');
const validator = require('../../lib/validator');
const jwtdecode = require('../../lib/express/jwt-decode');
const apiValidator = require('../../lib/validator/api');
const internalAccessList = require('../../internal/access-list');
const schema = require('../../schema');
import express from "express";
import internalAccessList from "../../internal/access-list.js";
import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js";
import { debug, express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js";
let router = express.Router({
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
strict: true,
mergeParams: true,
});
/**
* /api/nginx/access-lists
*/
router
.route('/')
.options((req, res) => {
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
@@ -26,29 +27,31 @@ router
*
* Retrieve all access-lists
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'common#/properties/expand'
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
query: {
$ref: "common#/properties/query",
},
},
},
query: {
$ref: 'common#/properties/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalAccessList.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
{
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
query: typeof req.query.query === "string" ? req.query.query : null,
},
);
const rows = await internalAccessList.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -56,16 +59,15 @@ router
*
* Create a new access-list
*/
.post((req, res, next) => {
apiValidator(schema.getValidationSchema('/nginx/access-lists', 'post'), req.body)
.then((payload) => {
return internalAccessList.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
.post(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/access-lists", "post"), req.body);
const result = await internalAccessList.create(res.locals.access, payload);
res.status(201).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
@@ -74,7 +76,7 @@ router
* /api/nginx/access-lists/123
*/
router
.route('/:list_id')
.route("/:list_id")
.options((_, res) => {
res.sendStatus(204);
})
@@ -85,33 +87,35 @@ router
*
* Retrieve a specific access-list
*/
.get((req, res, next) => {
validator({
required: ['list_id'],
additionalProperties: false,
properties: {
list_id: {
$ref: 'common#/properties/id'
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["list_id"],
additionalProperties: false,
properties: {
list_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
expand: {
$ref: 'common#/properties/expand'
}
}
}, {
list_id: req.params.list_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalAccessList.get(res.locals.access, {
id: parseInt(data.list_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
{
list_id: req.params.list_id,
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
},
);
const row = await internalAccessList.get(res.locals.access, {
id: Number.parseInt(data.list_id, 10),
expand: data.expand,
});
res.status(200).send(row);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -119,17 +123,16 @@ router
*
* Update and existing access-list
*/
.put((req, res, next) => {
apiValidator(schema.getValidationSchema('/nginx/access-lists/{listID}', 'put'), req.body)
.then((payload) => {
payload.id = parseInt(req.params.list_id, 10);
return internalAccessList.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.put(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/access-lists/{listID}", "put"), req.body);
payload.id = Number.parseInt(req.params.list_id, 10);
const result = await internalAccessList.update(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -137,13 +140,16 @@ router
*
* Delete and existing access-list
*/
.delete((req, res, next) => {
internalAccessList.delete(res.locals.access, {id: parseInt(req.params.list_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.delete(async (req, res, next) => {
try {
const result = await internalAccessList.delete(res.locals.access, {
id: Number.parseInt(req.params.list_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
module.exports = router;
export default router;
+289 -222
View File
@@ -1,22 +1,24 @@
const express = require('express');
const error = require('../../lib/error');
const validator = require('../../lib/validator');
const jwtdecode = require('../../lib/express/jwt-decode');
const apiValidator = require('../../lib/validator/api');
const internalCertificate = require('../../internal/certificate');
const schema = require('../../schema');
import express from "express";
import dnsPlugins from "../../certbot/dns-plugins.json" with { type: "json" };
import internalCertificate from "../../internal/certificate.js";
import errs from "../../lib/error.js";
import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js";
import { debug, express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js";
let router = express.Router({
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
strict: true,
mergeParams: true,
});
/**
* /api/nginx/certificates
*/
router
.route('/')
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
@@ -27,29 +29,38 @@ router
*
* Retrieve all certificates
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'common#/properties/expand'
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
query: {
$ref: "common#/properties/query",
},
},
},
query: {
$ref: 'common#/properties/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalCertificate.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
{
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,
);
res.status(200).send(rows);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -57,17 +68,56 @@ router
*
* Create a new certificate
*/
.post((req, res, next) => {
apiValidator(schema.getValidationSchema('/nginx/certificates', 'post'), req.body)
.then((payload) => {
req.setTimeout(900000); // 15 minutes timeout
return internalCertificate.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
.post(async (req, res, next) => {
try {
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,
);
res.status(201).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* /api/nginx/certificates/dns-providers
*/
router
.route("/dns-providers")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/dns-providers
*
* Get list of all supported DNS providers
*/
.get(async (req, res, next) => {
try {
if (!res.locals.access.token.getUserId()) {
throw new errs.PermissionError("Login required");
}
const clean = Object.keys(dnsPlugins).map((key) => ({
id: key,
name: dnsPlugins[key].name,
credentials: dnsPlugins[key].credentials,
}));
clean.sort((a, b) => a.name.localeCompare(b.name));
res.status(200).send(clean);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
@@ -76,180 +126,34 @@ router
* /api/nginx/certificates/test-http
*/
router
.route('/test-http')
.route("/test-http")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/test-http
* POST /api/nginx/certificates/test-http
*
* Test HTTP challenge for domains
*/
.get((req, res, next) => {
if (req.query.domains === undefined) {
next(new error.ValidationError('Domains are required as query parameters'));
return;
.post(async (req, res, next) => {
try {
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,
);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
internalCertificate.testHttpsChallenge(res.locals.access, JSON.parse(req.query.domains))
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Specific certificate
*
* /api/nginx/certificates/123
*/
router
.route('/:certificate_id')
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/123
*
* Retrieve a specific certificate
*/
.get((req, res, next) => {
validator({
required: ['certificate_id'],
additionalProperties: false,
properties: {
certificate_id: {
$ref: 'common#/properties/id'
},
expand: {
$ref: 'common#/properties/expand'
}
}
}, {
certificate_id: req.params.certificate_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalCertificate.get(res.locals.access, {
id: parseInt(data.certificate_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
})
/**
* DELETE /api/nginx/certificates/123
*
* Update and existing certificate
*/
.delete((req, res, next) => {
internalCertificate.delete(res.locals.access, {id: parseInt(req.params.certificate_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Upload Certs
*
* /api/nginx/certificates/123/upload
*/
router
.route('/:certificate_id/upload')
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/certificates/123/upload
*
* Upload certificates
*/
.post((req, res, next) => {
if (!req.files) {
res.status(400)
.send({error: 'No files were uploaded'});
} else {
internalCertificate.upload(res.locals.access, {
id: parseInt(req.params.certificate_id, 10),
files: req.files
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
}
});
/**
* Renew LE Certs
*
* /api/nginx/certificates/123/renew
*/
router
.route('/:certificate_id/renew')
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/certificates/123/renew
*
* Renew certificate
*/
.post((req, res, next) => {
req.setTimeout(900000); // 15 minutes timeout
internalCertificate.renew(res.locals.access, {
id: parseInt(req.params.certificate_id, 10)
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Download LE Certs
*
* /api/nginx/certificates/123/download
*/
router
.route('/:certificate_id/download')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/123/download
*
* Renew certificate
*/
.get((req, res, next) => {
internalCertificate.download(res.locals.access, {
id: parseInt(req.params.certificate_id, 10)
})
.then((result) => {
res.status(200)
.download(result.fileName);
})
.catch(next);
});
/**
@@ -258,7 +162,7 @@ router
* /api/nginx/certificates/validate
*/
router
.route('/validate')
.route("/validate")
.options((_, res) => {
res.sendStatus(204);
})
@@ -269,20 +173,183 @@ router
*
* Validate certificates
*/
.post((req, res, next) => {
.post(async (req, res, next) => {
if (!req.files) {
res.status(400)
.send({error: 'No files were uploaded'});
} else {
internalCertificate.validate({
files: req.files
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
res.status(400).send({ error: "No files were uploaded" });
return;
}
try {
const result = await internalCertificate.validate({
files: req.files,
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
module.exports = router;
/**
* Specific certificate
*
* /api/nginx/certificates/123
*/
router
.route("/:certificate_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/123
*
* Retrieve a specific certificate
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["certificate_id"],
additionalProperties: false,
properties: {
certificate_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
{
certificate_id: req.params.certificate_id,
expand:
typeof req.query.expand === "string"
? req.query.expand.split(",")
: null,
},
);
const row = await internalCertificate.get(res.locals.access, {
id: Number.parseInt(data.certificate_id, 10),
expand: data.expand,
});
res.status(200).send(row);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/nginx/certificates/123
*
* Update and existing certificate
*/
.delete(async (req, res, next) => {
try {
const result = await internalCertificate.delete(res.locals.access, {
id: Number.parseInt(req.params.certificate_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Upload Certs
*
* /api/nginx/certificates/123/upload
*/
router
.route("/:certificate_id/upload")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/certificates/123/upload
*
* Upload certificates
*/
.post(async (req, res, next) => {
if (!req.files) {
res.status(400).send({ error: "No files were uploaded" });
return;
}
try {
const result = await internalCertificate.upload(res.locals.access, {
id: Number.parseInt(req.params.certificate_id, 10),
files: req.files,
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Renew LE Certs
*
* /api/nginx/certificates/123/renew
*/
router
.route("/:certificate_id/renew")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/certificates/123/renew
*
* Renew certificate
*/
.post(async (req, res, next) => {
req.setTimeout(900000); // 15 minutes timeout
try {
const result = await internalCertificate.renew(res.locals.access, {
id: Number.parseInt(req.params.certificate_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Download LE Certs
*
* /api/nginx/certificates/123/download
*/
router
.route("/:certificate_id/download")
.options((_req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/123/download
*
* Renew certificate
*/
.get(async (req, res, next) => {
try {
const result = await internalCertificate.download(res.locals.access, {
id: Number.parseInt(req.params.certificate_id, 10),
});
res.status(200).download(result.fileName);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;
+116 -106
View File
@@ -1,21 +1,22 @@
const express = require('express');
const validator = require('../../lib/validator');
const jwtdecode = require('../../lib/express/jwt-decode');
const apiValidator = require('../../lib/validator/api');
const internalDeadHost = require('../../internal/dead-host');
const schema = require('../../schema');
import express from "express";
import internalDeadHost from "../../internal/dead-host.js";
import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js";
import { debug, express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js";
let router = express.Router({
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
strict: true,
mergeParams: true,
});
/**
* /api/nginx/dead-hosts
*/
router
.route('/')
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
@@ -26,29 +27,31 @@ router
*
* Retrieve all dead-hosts
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'common#/properties/expand'
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
query: {
$ref: "common#/properties/query",
},
},
},
query: {
$ref: 'common#/properties/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalDeadHost.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
{
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
query: typeof req.query.query === "string" ? req.query.query : null,
},
);
const rows = await internalDeadHost.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -56,16 +59,15 @@ router
*
* Create a new dead-host
*/
.post((req, res, next) => {
apiValidator(schema.getValidationSchema('/nginx/dead-hosts', 'post'), req.body)
.then((payload) => {
return internalDeadHost.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
.post(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/dead-hosts", "post"), req.body);
const result = await internalDeadHost.create(res.locals.access, payload);
res.status(201).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
@@ -74,8 +76,8 @@ router
* /api/nginx/dead-hosts/123
*/
router
.route('/:host_id')
.options((req, res) => {
.route("/:host_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
@@ -85,65 +87,69 @@ router
*
* Retrieve a specific dead-host
*/
.get((req, res, next) => {
validator({
required: ['host_id'],
additionalProperties: false,
properties: {
host_id: {
$ref: 'common#/properties/id'
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["host_id"],
additionalProperties: false,
properties: {
host_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
expand: {
$ref: 'common#/properties/expand'
}
}
}, {
host_id: req.params.host_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalDeadHost.get(res.locals.access, {
id: parseInt(data.host_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
{
host_id: req.params.host_id,
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
},
);
const row = await internalDeadHost.get(res.locals.access, {
id: Number.parseInt(data.host_id, 10),
expand: data.expand,
});
res.status(200).send(row);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* PUT /api/nginx/dead-hosts/123
*
* Update and existing dead-host
* Update an existing dead-host
*/
.put((req, res, next) => {
apiValidator(schema.getValidationSchema('/nginx/dead-hosts/{hostID}', 'put'), req.body)
.then((payload) => {
payload.id = parseInt(req.params.host_id, 10);
return internalDeadHost.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.put(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/dead-hosts/{hostID}", "put"), req.body);
payload.id = Number.parseInt(req.params.host_id, 10);
const result = await internalDeadHost.update(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/nginx/dead-hosts/123
*
* Update and existing dead-host
* Delete a dead-host
*/
.delete((req, res, next) => {
internalDeadHost.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.delete(async (req, res, next) => {
try {
const result = await internalDeadHost.delete(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}`);
next(err);
}
});
/**
@@ -152,7 +158,7 @@ router
* /api/nginx/dead-hosts/123/enable
*/
router
.route('/:host_id/enable')
.route("/:host_id/enable")
.options((_, res) => {
res.sendStatus(204);
})
@@ -161,13 +167,16 @@ router
/**
* POST /api/nginx/dead-hosts/123/enable
*/
.post((req, res, next) => {
internalDeadHost.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.post(async (req, res, next) => {
try {
const result = await internalDeadHost.enable(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}`);
next(err);
}
});
/**
@@ -176,7 +185,7 @@ router
* /api/nginx/dead-hosts/123/disable
*/
router
.route('/:host_id/disable')
.route("/:host_id/disable")
.options((_, res) => {
res.sendStatus(204);
})
@@ -186,12 +195,13 @@ router
* POST /api/nginx/dead-hosts/123/disable
*/
.post((req, res, next) => {
internalDeadHost.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
try {
const result = 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}`);
next(err);
}
});
module.exports = router;
export default router;
+118 -106
View File
@@ -1,22 +1,23 @@
const express = require('express');
const validator = require('../../lib/validator');
const jwtdecode = require('../../lib/express/jwt-decode');
const apiValidator = require('../../lib/validator/api');
const internalProxyHost = require('../../internal/proxy-host');
const schema = require('../../schema');
import express from "express";
import internalProxyHost from "../../internal/proxy-host.js";
import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js";
import { debug, express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js";
let router = express.Router({
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
strict: true,
mergeParams: true,
});
/**
* /api/nginx/proxy-hosts
*/
router
.route('/')
.options((req, res) => {
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
@@ -26,29 +27,31 @@ router
*
* Retrieve all proxy-hosts
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'common#/properties/expand'
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
query: {
$ref: "common#/properties/query",
},
},
},
query: {
$ref: 'common#/properties/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalProxyHost.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
{
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
query: typeof req.query.query === "string" ? req.query.query : null,
},
);
const rows = await internalProxyHost.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -56,16 +59,15 @@ router
*
* Create a new proxy-host
*/
.post((req, res, next) => {
apiValidator(schema.getValidationSchema('/nginx/proxy-hosts', 'post'), req.body)
.then((payload) => {
return internalProxyHost.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
.post(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/proxy-hosts", "post"), req.body);
const result = await internalProxyHost.create(res.locals.access, payload);
res.status(201).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err} ${JSON.stringify(err.debug, null, 2)}`);
next(err);
}
});
/**
@@ -74,8 +76,8 @@ router
* /api/nginx/proxy-hosts/123
*/
router
.route('/:host_id')
.options((req, res) => {
.route("/:host_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
@@ -85,33 +87,35 @@ router
*
* Retrieve a specific proxy-host
*/
.get((req, res, next) => {
validator({
required: ['host_id'],
additionalProperties: false,
properties: {
host_id: {
$ref: 'common#/properties/id'
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["host_id"],
additionalProperties: false,
properties: {
host_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
expand: {
$ref: 'common#/properties/expand'
}
}
}, {
host_id: req.params.host_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalProxyHost.get(res.locals.access, {
id: parseInt(data.host_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
{
host_id: req.params.host_id,
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
},
);
const row = await internalProxyHost.get(res.locals.access, {
id: Number.parseInt(data.host_id, 10),
expand: data.expand,
});
res.status(200).send(row);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -119,17 +123,16 @@ router
*
* Update and existing proxy-host
*/
.put((req, res, next) => {
apiValidator(schema.getValidationSchema('/nginx/proxy-hosts/{hostID}', 'put'), req.body)
.then((payload) => {
payload.id = parseInt(req.params.host_id, 10);
return internalProxyHost.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.put(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/proxy-hosts/{hostID}", "put"), req.body);
payload.id = Number.parseInt(req.params.host_id, 10);
const result = await internalProxyHost.update(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -137,13 +140,16 @@ router
*
* Update and existing proxy-host
*/
.delete((req, res, next) => {
internalProxyHost.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.delete(async (req, res, next) => {
try {
const result = await internalProxyHost.delete(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}`);
next(err);
}
});
/**
@@ -152,7 +158,7 @@ router
* /api/nginx/proxy-hosts/123/enable
*/
router
.route('/:host_id/enable')
.route("/:host_id/enable")
.options((_, res) => {
res.sendStatus(204);
})
@@ -161,13 +167,16 @@ router
/**
* POST /api/nginx/proxy-hosts/123/enable
*/
.post((req, res, next) => {
internalProxyHost.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.post(async (req, res, next) => {
try {
const result = await internalProxyHost.enable(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}`);
next(err);
}
});
/**
@@ -176,7 +185,7 @@ router
* /api/nginx/proxy-hosts/123/disable
*/
router
.route('/:host_id/disable')
.route("/:host_id/disable")
.options((_, res) => {
res.sendStatus(204);
})
@@ -185,13 +194,16 @@ router
/**
* POST /api/nginx/proxy-hosts/123/disable
*/
.post((req, res, next) => {
internalProxyHost.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.post(async (req, res, next) => {
try {
const result = await internalProxyHost.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}`);
next(err);
}
});
module.exports = router;
export default router;
+123 -108
View File
@@ -1,22 +1,23 @@
const express = require('express');
const validator = require('../../lib/validator');
const jwtdecode = require('../../lib/express/jwt-decode');
const apiValidator = require('../../lib/validator/api');
const internalRedirectionHost = require('../../internal/redirection-host');
const schema = require('../../schema');
import express from "express";
import internalRedirectionHost from "../../internal/redirection-host.js";
import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js";
import { debug, express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js";
let router = express.Router({
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
strict: true,
mergeParams: true,
});
/**
* /api/nginx/redirection-hosts
*/
router
.route('/')
.options((req, res) => {
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
@@ -26,29 +27,31 @@ router
*
* Retrieve all redirection-hosts
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'common#/properties/expand'
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
query: {
$ref: "common#/properties/query",
},
},
},
query: {
$ref: 'common#/properties/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalRedirectionHost.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
{
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
query: typeof req.query.query === "string" ? req.query.query : null,
},
);
const rows = await internalRedirectionHost.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -56,16 +59,15 @@ router
*
* Create a new redirection-host
*/
.post((req, res, next) => {
apiValidator(schema.getValidationSchema('/nginx/redirection-hosts', 'post'), req.body)
.then((payload) => {
return internalRedirectionHost.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
.post(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/redirection-hosts", "post"), req.body);
const result = await internalRedirectionHost.create(res.locals.access, payload);
res.status(201).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
@@ -74,8 +76,8 @@ router
* /api/nginx/redirection-hosts/123
*/
router
.route('/:host_id')
.options((req, res) => {
.route("/:host_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
@@ -85,33 +87,35 @@ router
*
* Retrieve a specific redirection-host
*/
.get((req, res, next) => {
validator({
required: ['host_id'],
additionalProperties: false,
properties: {
host_id: {
$ref: 'common#/properties/id'
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["host_id"],
additionalProperties: false,
properties: {
host_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
expand: {
$ref: 'common#/properties/expand'
}
}
}, {
host_id: req.params.host_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalRedirectionHost.get(res.locals.access, {
id: parseInt(data.host_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
{
host_id: req.params.host_id,
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
},
);
const row = await internalRedirectionHost.get(res.locals.access, {
id: Number.parseInt(data.host_id, 10),
expand: data.expand,
});
res.status(200).send(row);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -119,17 +123,19 @@ router
*
* Update and existing redirection-host
*/
.put((req, res, next) => {
apiValidator(schema.getValidationSchema('/nginx/redirection-hosts/{hostID}', 'put'), req.body)
.then((payload) => {
payload.id = parseInt(req.params.host_id, 10);
return internalRedirectionHost.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.put(async (req, res, next) => {
try {
const payload = await apiValidator(
getValidationSchema("/nginx/redirection-hosts/{hostID}", "put"),
req.body,
);
payload.id = Number.parseInt(req.params.host_id, 10);
const result = await internalRedirectionHost.update(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -137,13 +143,16 @@ router
*
* Update and existing redirection-host
*/
.delete((req, res, next) => {
internalRedirectionHost.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.delete(async (req, res, next) => {
try {
const result = await internalRedirectionHost.delete(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}`);
next(err);
}
});
/**
@@ -152,8 +161,8 @@ router
* /api/nginx/redirection-hosts/123/enable
*/
router
.route('/:host_id/enable')
.options((req, res) => {
.route("/:host_id/enable")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
@@ -161,13 +170,16 @@ router
/**
* POST /api/nginx/redirection-hosts/123/enable
*/
.post((req, res, next) => {
internalRedirectionHost.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.post(async (req, res, next) => {
try {
const result = await internalRedirectionHost.enable(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}`);
next(err);
}
});
/**
@@ -176,8 +188,8 @@ router
* /api/nginx/redirection-hosts/123/disable
*/
router
.route('/:host_id/disable')
.options((req, res) => {
.route("/:host_id/disable")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
@@ -185,13 +197,16 @@ router
/**
* POST /api/nginx/redirection-hosts/123/disable
*/
.post((req, res, next) => {
internalRedirectionHost.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.post(async (req, res, next) => {
try {
const result = await internalRedirectionHost.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}`);
next(err);
}
});
module.exports = router;
export default router;
+118 -106
View File
@@ -1,22 +1,23 @@
const express = require('express');
const validator = require('../../lib/validator');
const jwtdecode = require('../../lib/express/jwt-decode');
const apiValidator = require('../../lib/validator/api');
const internalStream = require('../../internal/stream');
const schema = require('../../schema');
import express from "express";
import internalStream from "../../internal/stream.js";
import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js";
import { debug, express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js";
let router = express.Router({
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
strict: true,
mergeParams: true,
});
/**
* /api/nginx/streams
*/
router
.route('/')
.options((req, res) => {
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes
@@ -26,29 +27,31 @@ router
*
* Retrieve all streams
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'common#/properties/expand'
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
query: {
$ref: "common#/properties/query",
},
},
},
query: {
$ref: 'common#/properties/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalStream.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
{
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
query: typeof req.query.query === "string" ? req.query.query : null,
},
);
const rows = await internalStream.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -56,16 +59,15 @@ router
*
* Create a new stream
*/
.post((req, res, next) => {
apiValidator(schema.getValidationSchema('/nginx/streams', 'post'), req.body)
.then((payload) => {
return internalStream.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
.post(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/streams", "post"), req.body);
const result = await internalStream.create(res.locals.access, payload);
res.status(201).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
@@ -74,8 +76,8 @@ router
* /api/nginx/streams/123
*/
router
.route('/:stream_id')
.options((req, res) => {
.route("/:stream_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes
@@ -85,33 +87,35 @@ router
*
* Retrieve a specific stream
*/
.get((req, res, next) => {
validator({
required: ['stream_id'],
additionalProperties: false,
properties: {
stream_id: {
$ref: 'common#/properties/id'
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["stream_id"],
additionalProperties: false,
properties: {
stream_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
expand: {
$ref: 'common#/properties/expand'
}
}
}, {
stream_id: req.params.stream_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalStream.get(res.locals.access, {
id: parseInt(data.stream_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
{
stream_id: req.params.stream_id,
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
},
);
const row = await internalStream.get(res.locals.access, {
id: Number.parseInt(data.stream_id, 10),
expand: data.expand,
});
res.status(200).send(row);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -119,17 +123,16 @@ router
*
* Update and existing stream
*/
.put((req, res, next) => {
apiValidator(schema.getValidationSchema('/nginx/streams/{streamID}', 'put'), req.body)
.then((payload) => {
payload.id = parseInt(req.params.stream_id, 10);
return internalStream.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.put(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/streams/{streamID}", "put"), req.body);
payload.id = Number.parseInt(req.params.stream_id, 10);
const result = await internalStream.update(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -137,13 +140,16 @@ router
*
* Update and existing stream
*/
.delete((req, res, next) => {
internalStream.delete(res.locals.access, {id: parseInt(req.params.stream_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.delete(async (req, res, next) => {
try {
const result = await internalStream.delete(res.locals.access, {
id: Number.parseInt(req.params.stream_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
@@ -152,7 +158,7 @@ router
* /api/nginx/streams/123/enable
*/
router
.route('/:host_id/enable')
.route("/:host_id/enable")
.options((_, res) => {
res.sendStatus(204);
})
@@ -161,13 +167,16 @@ router
/**
* POST /api/nginx/streams/123/enable
*/
.post((req, res, next) => {
internalStream.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.post(async (req, res, next) => {
try {
const result = await internalStream.enable(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}`);
next(err);
}
});
/**
@@ -176,7 +185,7 @@ router
* /api/nginx/streams/123/disable
*/
router
.route('/:host_id/disable')
.route("/:host_id/disable")
.options((_, res) => {
res.sendStatus(204);
})
@@ -185,13 +194,16 @@ router
/**
* POST /api/nginx/streams/123/disable
*/
.post((req, res, next) => {
internalStream.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.post(async (req, res, next) => {
try {
const result = await internalStream.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}`);
next(err);
}
});
module.exports = router;
export default router;
+18 -15
View File
@@ -1,29 +1,32 @@
const express = require('express');
const jwtdecode = require('../lib/express/jwt-decode');
const internalReport = require('../internal/report');
import express from "express";
import internalReport from "../internal/report.js";
import jwtdecode from "../lib/express/jwt-decode.js";
import { debug, express as logger } from "../logger.js";
let router = express.Router({
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
strict: true,
mergeParams: true,
});
router
.route('/hosts')
.route("/hosts")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /reports/hosts
*/
.get(jwtdecode(), (_, res, next) => {
internalReport.getHostsReport(res.locals.access)
.then((data) => {
res.status(200)
.send(data);
})
.catch(next);
.get(async (req, res, next) => {
try {
const data = await internalReport.getHostsReport(res.locals.access);
res.status(200).send(data);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
module.exports = router;
export default router;
+27 -21
View File
@@ -1,15 +1,16 @@
const express = require('express');
const schema = require('../schema');
const PACKAGE = require('../package.json');
import express from "express";
import { debug, express as logger } from "../logger.js";
import PACKAGE from "../package.json" with { type: "json" };
import { getCompiledSchema } from "../schema/index.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
strict: true,
mergeParams: true,
});
router
.route('/')
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
@@ -17,22 +18,27 @@ router
/**
* GET /schema
*/
.get(async (req, res) => {
let swaggerJSON = await schema.getCompiledSchema();
.get(async (req, res, next) => {
try {
const swaggerJSON = await getCompiledSchema();
let proto = req.protocol;
if (typeof req.headers['x-forwarded-proto'] !== 'undefined' && req.headers['x-forwarded-proto']) {
proto = req.headers['x-forwarded-proto'];
let proto = req.protocol;
if (typeof req.headers["x-forwarded-proto"] !== "undefined" && req.headers["x-forwarded-proto"]) {
proto = req.headers["x-forwarded-proto"];
}
let origin = `${proto}://${req.hostname}`;
if (typeof req.headers.origin !== "undefined" && req.headers.origin) {
origin = req.headers.origin;
}
swaggerJSON.info.version = PACKAGE.version;
swaggerJSON.servers[0].url = `${origin}/api`;
res.status(200).send(swaggerJSON);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
let origin = proto + '://' + req.hostname;
if (typeof req.headers.origin !== 'undefined' && req.headers.origin) {
origin = req.headers.origin;
}
swaggerJSON.info.version = PACKAGE.version;
swaggerJSON.servers[0].url = origin + '/api';
res.status(200).send(swaggerJSON);
});
module.exports = router;
export default router;
+56 -53
View File
@@ -1,21 +1,22 @@
const express = require('express');
const validator = require('../lib/validator');
const jwtdecode = require('../lib/express/jwt-decode');
const apiValidator = require('../lib/validator/api');
const internalSetting = require('../internal/setting');
const schema = require('../schema');
import express from "express";
import internalSetting from "../internal/setting.js";
import jwtdecode from "../lib/express/jwt-decode.js";
import apiValidator from "../lib/validator/api.js";
import validator from "../lib/validator/index.js";
import { debug, express as logger } from "../logger.js";
import { getValidationSchema } from "../schema/index.js";
let router = express.Router({
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
strict: true,
mergeParams: true,
});
/**
* /api/settings
*/
router
.route('/')
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
@@ -26,13 +27,14 @@ router
*
* Retrieve all settings
*/
.get((_, res, next) => {
internalSetting.getAll(res.locals.access)
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
.get(async (req, res, next) => {
try {
const rows = await internalSetting.getAll(res.locals.access);
res.status(200).send(rows);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
@@ -41,7 +43,7 @@ router
* /api/settings/something
*/
router
.route('/:setting_id')
.route("/:setting_id")
.options((_, res) => {
res.sendStatus(204);
})
@@ -52,29 +54,31 @@ router
*
* Retrieve a specific setting
*/
.get((req, res, next) => {
validator({
required: ['setting_id'],
additionalProperties: false,
properties: {
setting_id: {
type: 'string',
minLength: 1
}
}
}, {
setting_id: req.params.setting_id
})
.then((data) => {
return internalSetting.get(res.locals.access, {
id: data.setting_id
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["setting_id"],
additionalProperties: false,
properties: {
setting_id: {
type: "string",
minLength: 1,
},
},
},
{
setting_id: req.params.setting_id,
},
);
const row = await internalSetting.get(res.locals.access, {
id: data.setting_id,
});
res.status(200).send(row);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -82,17 +86,16 @@ router
*
* Update and existing setting
*/
.put((req, res, next) => {
apiValidator(schema.getValidationSchema('/settings/{settingID}', 'put'), req.body)
.then((payload) => {
payload.id = req.params.setting_id;
return internalSetting.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.put(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/settings/{settingID}", "put"), req.body);
payload.id = req.params.setting_id;
const result = await internalSetting.update(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
module.exports = router;
export default router;
+52 -27
View File
@@ -1,17 +1,18 @@
const express = require('express');
const jwtdecode = require('../lib/express/jwt-decode');
const apiValidator = require('../lib/validator/api');
const internalToken = require('../internal/token');
const schema = require('../schema');
import express from "express";
import internalToken from "../internal/token.js";
import jwtdecode from "../lib/express/jwt-decode.js";
import apiValidator from "../lib/validator/api.js";
import { debug, express as logger } from "../logger.js";
import { getValidationSchema } from "../schema/index.js";
let router = express.Router({
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
strict: true,
mergeParams: true,
});
router
.route('/')
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
@@ -23,16 +24,17 @@ router
* We also piggy back on to this method, allowing admins to get tokens
* for services like Job board and Worker.
*/
.get(jwtdecode(), (req, res, next) => {
internalToken.getFreshToken(res.locals.access, {
expiry: (typeof req.query.expiry !== 'undefined' ? req.query.expiry : null),
scope: (typeof req.query.scope !== 'undefined' ? req.query.scope : null)
})
.then((data) => {
res.status(200)
.send(data);
})
.catch(next);
.get(jwtdecode(), async (req, res, next) => {
try {
const data = await internalToken.getFreshToken(res.locals.access, {
expiry: typeof req.query.expiry !== "undefined" ? req.query.expiry : null,
scope: typeof req.query.scope !== "undefined" ? req.query.scope : null,
});
res.status(200).send(data);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -41,13 +43,36 @@ router
* Create a new Token
*/
.post(async (req, res, next) => {
apiValidator(schema.getValidationSchema('/tokens', 'post'), req.body)
.then(internalToken.getTokenFromEmail)
.then((data) => {
res.status(200)
.send(data);
})
.catch(next);
try {
const data = await apiValidator(getValidationSchema("/tokens", "post"), req.body);
const result = await internalToken.getTokenFromEmail(data);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
module.exports = router;
router
.route("/2fa")
.options((_, res) => {
res.sendStatus(204);
})
/**
* POST /tokens/2fa
*
* Verify 2FA code and get full token
*/
.post(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;
+338 -130
View File
@@ -1,22 +1,28 @@
const express = require('express');
const validator = require('../lib/validator');
const jwtdecode = require('../lib/express/jwt-decode');
const userIdFromMe = require('../lib/express/user-id-from-me');
const internalUser = require('../internal/user');
const apiValidator = require('../lib/validator/api');
const schema = require('../schema');
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";
import errs from "../lib/error.js";
import jwtdecode from "../lib/express/jwt-decode.js";
import userIdFromMe from "../lib/express/user-id-from-me.js";
import apiValidator from "../lib/validator/api.js";
import validator from "../lib/validator/index.js";
import { debug, express as logger } from "../logger.js";
import { getValidationSchema } from "../schema/index.js";
import { isSetup } from "../setup.js";
let router = express.Router({
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
strict: true,
mergeParams: true,
});
/**
* /api/users
*/
router
.route('/')
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
@@ -27,33 +33,38 @@ router
*
* Retrieve all users
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'common#/properties/expand'
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
query: {
$ref: "common#/properties/query",
},
},
},
query: {
$ref: 'common#/properties/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalUser.getAll(res.locals.access, data.expand, data.query);
})
.then((users) => {
res.status(200)
.send(users);
})
.catch((err) => {
console.log(err);
next(err);
});
//.catch(next);
{
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,
);
res.status(200).send(users);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -61,16 +72,66 @@ router
*
* Create a new User
*/
.post((req, res, next) => {
apiValidator(schema.getValidationSchema('/users', 'post'), req.body)
.then((payload) => {
return internalUser.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
.post(async (req, res, next) => {
const body = req.body;
try {
// If we are in setup mode, we don't check access for current user
const setup = await isSetup();
if (!setup) {
logger.info("Creating a new user in setup mode");
const access = new Access(null);
await access.load(true);
res.locals.access = access;
// We are in setup mode, set some defaults for this first new user, such as making
// them an admin.
body.is_disabled = false;
if (typeof body.roles !== "object" || body.roles === null) {
body.roles = [];
}
if (body.roles.indexOf("admin") === -1) {
body.roles.push("admin");
}
}
const payload = await apiValidator(
getValidationSchema("/users", "post"),
body,
);
const user = await internalUser.create(res.locals.access, payload);
res.status(201).send(user);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/users
*
* Deletes ALL users. This is NOT GENERALLY AVAILABLE!
* (!) It is NOT an authenticated endpoint.
* (!) Only CI should be able to call this endpoint. As a result,
*
* it will only work when the env vars DEBUG=true and CI=true
*
* Do NOT set those env vars in a production environment!
*/
.delete(async (_, res, next) => {
if (isCI()) {
try {
logger.warn("Deleting all users - CI environment detected, allowing this operation");
await internalUser.deleteAll();
res.status(200).send(true);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
return;
}
next(new errs.ItemNotFoundError());
});
/**
@@ -79,7 +140,7 @@ router
* /api/users/123
*/
router
.route('/:user_id')
.route("/:user_id")
.options((_, res) => {
res.sendStatus(204);
})
@@ -91,37 +152,43 @@ router
*
* Retrieve a specific user
*/
.get((req, res, next) => {
validator({
required: ['user_id'],
additionalProperties: false,
properties: {
user_id: {
$ref: 'common#/properties/id'
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["user_id"],
additionalProperties: false,
properties: {
user_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
expand: {
$ref: 'common#/properties/expand'
}
}
}, {
user_id: req.params.user_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalUser.get(res.locals.access, {
id: data.user_id,
expand: data.expand,
omit: internalUser.getUserOmisionsByAccess(res.locals.access, data.user_id)
});
})
.then((user) => {
res.status(200)
.send(user);
})
.catch((err) => {
console.log(err);
next(err);
{
user_id: req.params.user_id,
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,
),
});
res.status(200).send(user);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -129,17 +196,19 @@ router
*
* Update and existing user
*/
.put((req, res, next) => {
apiValidator(schema.getValidationSchema('/users/{userID}', 'put'), req.body)
.then((payload) => {
payload.id = req.params.user_id;
return internalUser.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.put(async (req, res, next) => {
try {
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);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
@@ -147,13 +216,16 @@ router
*
* Update and existing user
*/
.delete((req, res, next) => {
internalUser.delete(res.locals.access, {id: req.params.user_id})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
.delete(async (req, res, next) => {
try {
const result = await internalUser.delete(res.locals.access, {
id: req.params.user_id,
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
@@ -162,8 +234,8 @@ router
* /api/users/123/auth
*/
router
.route('/:user_id/auth')
.options((req, res) => {
.route("/:user_id/auth")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
@@ -174,17 +246,19 @@ router
*
* Update password for a user
*/
.put((req, res, next) => {
apiValidator(schema.getValidationSchema('/users/{userID}/auth', 'put'), req.body)
.then((payload) => {
payload.id = req.params.user_id;
return internalUser.setPassword(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
.put(async (req, res, next) => {
try {
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);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
@@ -193,8 +267,8 @@ router
* /api/users/123/permissions
*/
router
.route('/:user_id/permissions')
.options((req, res) => {
.route("/:user_id/permissions")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
@@ -205,17 +279,22 @@ router
*
* Set some or all permissions for a user
*/
.put((req, res, next) => {
apiValidator(schema.getValidationSchema('/users/{userID}/permissions', 'put'), req.body)
.then((payload) => {
payload.id = req.params.user_id;
return internalUser.setPermissions(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
.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,
);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
@@ -224,7 +303,7 @@ router
* /api/users/123/login
*/
router
.route('/:user_id/login')
.route("/:user_id/login")
.options((_, res) => {
res.sendStatus(204);
})
@@ -235,13 +314,142 @@ router
*
* Log in as a user
*/
.post((req, res, next) => {
internalUser.loginAs(res.locals.access, {id: parseInt(req.params.user_id, 10)})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
.post(async (req, res, next) => {
try {
const result = await internalUser.loginAs(res.locals.access, {
id: Number.parseInt(req.params.user_id, 10),
});
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
module.exports = 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;

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