Compare commits

..

54 Commits

Author SHA1 Message Date
b4f49969d6 Merge pull request #4261 from NginxProxyManager/develop
v2.12.2
2024-12-29 14:40:05 +10:00
ec12d8f9bf Merge pull request #4148 from Medan-rfz/develop
Added certbot plugin for Beget DNS service
2024-12-29 14:00:51 +10:00
e50e3def9d Merge pull request #4169 from andrew-codechimp/bump-porkbun
Bump certbot-dns-porkbun
2024-12-29 14:00:18 +10:00
6415f284f9 Merge pull request #4256 from bigcat26/develop
upgrade certbot-dns-aliyun plugin from 0.38.1 to 2.0.0
2024-12-29 13:52:03 +10:00
98e5997f0a upgrade certbot-dns-aliyun plugin from 0.38.1 to 2.0.0 2024-12-26 09:51:28 +08:00
fc30a92bd4 Open port for authentik in dev
All checks were successful
Close stale issues and PRs / stale (push) Successful in 4s
2024-12-24 18:19:52 +10:00
e2011ee45c Bump version 2024-12-24 17:51:25 +10:00
1406e75c2c Merge pull request #4254 from NginxProxyManager/postgres
Postgres
2024-12-24 17:24:05 +10:00
ca3ee98c68 Postgres Support
- Combines #4086 and #4087 PRs
- Adds authentik in CI stack
2024-12-24 16:48:48 +10:00
f90d839ebe Merge pull request #4246 from JanzenJohn/develop
Remove infinite requests loop
2024-12-24 08:16:48 +10:00
be5278f31e Merge pull request #4247 from miguelangel-nubla/patch-1
Add custom configuration to 404 hosts
2024-12-24 08:15:55 +10:00
3eecf7a38b Add custom configuration to 404 hosts 2024-12-20 01:03:21 +01:00
7f9240dda7 Add custom configuration to dead_host.conf 2024-12-20 00:59:26 +01:00
f537619ffe Revert "Change onRender function to always update the dashboard stats"
This reverts commit d26e8c1d0c.

This reopens #4204 (which i can't reproduce sadly)

The reverted commit is responsible for an infinite loop of requests to /hosts, which makes buttons unresponsive on the main page
another way to invalidate the cache needs to be found

this infinite requests loop happens on d26e8c1d0c
and on the docker image
`nginxproxymanager/nginx-proxy-manager-dev:pr-4206`

the docker image is attaced to the pr #4206 which merges the commit
2024-12-19 16:16:03 +01:00
805968aac6 Merge pull request #4185 from muescha/patch-1
All checks were successful
Close stale issues and PRs / stale (push) Successful in 4s
Update index.md: add link to Proxmox VE Helper-Scripts
2024-12-17 07:59:45 +10:00
2a4093c1b8 Merge pull request #4215 from TECH7Fox/patch-1
Add hostingnl DNS Challenge provider
2024-12-17 07:57:43 +10:00
ae2ac8a733 Merge pull request #4230 from NginxProxyManager/dependabot/npm_and_yarn/docs/nanoid-3.3.8
Bump nanoid from 3.3.7 to 3.3.8 in /docs
2024-12-17 07:52:24 +10:00
c6eca2578e Bump nanoid from 3.3.7 to 3.3.8 in /docs
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-14 10:02:55 +00:00
56033bee9c Add hostingnl 2024-12-08 15:23:37 +01:00
c6630e87bb Update version 'certbot-beget-plugin' & fix credentials content 2024-12-07 15:01:57 +04:00
d6b98f51b0 Merge branch 'NginxProxyManager:develop' into develop 2024-12-07 14:27:29 +04:00
b3de76c945 Merge pull request #4192 from badkeyy/bugfix/fix-user-edit-email-format-check
All checks were successful
Close stale issues and PRs / stale (push) Successful in 4s
Enforce email format when editing user
2024-12-04 14:50:42 +10:00
fcf4117f8e Merge pull request #4206 from badkeyy/bugfix/update-dashboard-stats-on-change
Update the dashboard stats every time the dashboard is shown
2024-12-04 13:08:21 +10:00
d26e8c1d0c Change onRender function to always update the dashboard stats 2024-12-04 03:45:56 +01:00
19ed4c1212 Change click to submit 2024-12-04 03:08:49 +01:00
03018d252b Merge branch 'NginxProxyManager:develop' into bugfix/fix-user-edit-email-format-check 2024-12-04 01:58:08 +01:00
8351dd41f6 Merge pull request #4199 from NginxProxyManager/dependabot/npm_and_yarn/test/cross-spawn-7.0.6
All checks were successful
Close stale issues and PRs / stale (push) Successful in 4s
Bump cross-spawn from 7.0.3 to 7.0.6 in /test
2024-12-02 10:45:00 +10:00
97212f2686 Merge pull request #4123 from NginxProxyManager/dependabot/npm_and_yarn/frontend/elliptic-6.6.0
Bump elliptic from 6.5.7 to 6.6.0 in /frontend
2024-12-02 10:44:20 +10:00
fe068a8b51 Bump cross-spawn from 7.0.3 to 7.0.6 in /test
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-01 22:49:09 +00:00
61e2bde98f Merge pull request #4184 from NginxProxyManager/dependabot/npm_and_yarn/backend/cross-spawn-7.0.6
Bump cross-spawn from 7.0.3 to 7.0.6 in /backend
2024-12-02 08:48:08 +10:00
81c9038929 Refactor user form structure 2024-11-27 18:27:11 +01:00
4ea50ca40c Merge pull request #4126 from jonasrdl/remove-deprecated-version-line
All checks were successful
Close stale issues and PRs / stale (push) Successful in 4s
docs(setup): Remove deprecated version from docker-compose.yml
2024-11-26 07:37:41 +10:00
53ed12bcf2 Merge pull request #4163 from Jasparigus/stream_error_correction
Fix Container Bootloop if Stream is used for http/https ports
2024-11-26 07:37:14 +10:00
cb3e4ed59c Merge pull request #4137 from irexyc/add-woff2-asset
Add woff2 format to assets.conf for Cache Assets
2024-11-26 07:35:57 +10:00
b20dc5eade Merge pull request #4167 from NginxProxyManager/dependabot/npm_and_yarn/test/eslint/plugin-kit-0.2.3
Bump @eslint/plugin-kit from 0.2.0 to 0.2.3 in /test
2024-11-26 07:35:10 +10:00
586afc0c91 Merge pull request #4187 from kerstenremco/avatar
Fix entries of a deleted user break the UI
2024-11-26 07:31:03 +10:00
93ea17a9bb Fix entries of a deleted user break the UI 2024-11-25 20:37:49 +01:00
151160a834 Update index.md: add link to Proxmox VE Helper-Scripts
Update index.md: add link to Proxmox VE Helper-Scripts
2024-11-24 20:10:17 +01:00
2075f98cad Bump cross-spawn from 7.0.3 to 7.0.6 in /backend
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-24 03:36:44 +00:00
07a4e5791f Merge pull request #4179 from tametsi/develop
All checks were successful
Close stale issues and PRs / stale (push) Successful in 4s
Return generic auth error to prevent user enumeration attacks
2024-11-23 22:39:37 +10:00
640a1eeb68 Return generic auth error to prevent user enumeration attacks
On invalid user/password error the error message "Invalid email or password" is returned.
Thereby, no information about the existence of the user is given.
2024-11-22 10:37:09 +01:00
126d3d44ca Bump certbot-dns-porkbun 2024-11-17 10:44:29 +00:00
20646e7bb5 Bump @eslint/plugin-kit from 0.2.0 to 0.2.3 in /test
Bumps [@eslint/plugin-kit](https://github.com/eslint/rewrite) from 0.2.0 to 0.2.3.
- [Release notes](https://github.com/eslint/rewrite/releases)
- [Changelog](https://github.com/eslint/rewrite/blob/main/release-please-config.json)
- [Commits](https://github.com/eslint/rewrite/compare/core-v0.2.0...plugin-kit-v0.2.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-15 21:19:05 +00:00
87998a03ce Fix bootloop if stream is used for http/https port 2024-11-14 11:39:48 -08:00
a56342c76a Fix credentials 2024-11-10 19:23:28 +04:00
4c89379671 Update version 'certbot-beget-plugin' 2024-11-10 18:31:07 +04:00
10b9a49274 Update version 'certbot-beget-plugin' 2024-11-10 16:16:45 +04:00
595a742c40 Change beget plugin 2024-11-10 15:09:41 +04:00
c171752137 Added certbot plugin for Beget DNS service 2024-11-08 02:29:38 +04:00
a0b26b9e98 Add woff2 format to assets.conf for Cache Assets 2024-11-04 20:01:39 +08:00
d6791f4e38 docs(setup): Remove deprecated version from docker-compose.yml 2024-10-31 11:25:38 +01:00
62c94f3099 Bump elliptic from 6.5.7 to 6.6.0 in /frontend
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.7 to 6.6.0.
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.7...v6.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-31 02:19:58 +00:00
5084cb7296 Merge pull request #4077 from NginxProxyManager/develop
v2.12.1
2024-10-17 09:49:07 +10:00
e677bfa2e8 Merge pull request #4073 from NginxProxyManager/develop
v2.12.0
2024-10-16 15:41:55 +10:00
61 changed files with 2027 additions and 1248 deletions

View File

@ -1 +1 @@
2.12.1 2.12.2

38
Jenkinsfile vendored
View File

@ -167,6 +167,44 @@ pipeline {
} }
} }
} }
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') { stage('MultiArch Build') {
when { when {
not { not {

View File

@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<img src="https://nginxproxymanager.com/github.png"> <img src="https://nginxproxymanager.com/github.png">
<br><br> <br><br>
<img src="https://img.shields.io/badge/version-2.12.1-green.svg?style=for-the-badge"> <img src="https://img.shields.io/badge/version-2.12.2-green.svg?style=for-the-badge">
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager"> <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"> <img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge">
</a> </a>

View File

@ -81,7 +81,7 @@ const internalAccessList = {
return internalAccessList.build(row) return internalAccessList.build(row)
.then(() => { .then(() => {
if (row.proxy_host_count) { if (parseInt(row.proxy_host_count, 10)) {
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts); return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
} }
}) })
@ -223,7 +223,7 @@ const internalAccessList = {
.then((row) => { .then((row) => {
return internalAccessList.build(row) return internalAccessList.build(row)
.then(() => { .then(() => {
if (row.proxy_host_count) { if (parseInt(row.proxy_host_count, 10)) {
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts); return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
} }
}).then(internalNginx.reload) }).then(internalNginx.reload)
@ -252,7 +252,10 @@ const internalAccessList = {
let query = accessListModel let query = accessListModel
.query() .query()
.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count')) .select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0') .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) .where('access_list.is_deleted', 0)
.andWhere('access_list.id', data.id) .andWhere('access_list.id', data.id)
.allowGraph('[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]') .allowGraph('[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]')
@ -373,7 +376,10 @@ const internalAccessList = {
let query = accessListModel let query = accessListModel
.query() .query()
.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count')) .select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0') .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) .where('access_list.is_deleted', 0)
.groupBy('access_list.id') .groupBy('access_list.id')
.allowGraph('[owner,items,clients]') .allowGraph('[owner,items,clients]')

View File

@ -1,5 +1,6 @@
const error = require('../lib/error'); const error = require('../lib/error');
const auditLogModel = require('../models/audit-log'); const auditLogModel = require('../models/audit-log');
const {castJsonIfNeed} = require('../lib/helpers');
const internalAuditLog = { const internalAuditLog = {
@ -22,9 +23,9 @@ const internalAuditLog = {
.allowGraph('[user]'); .allowGraph('[user]');
// Query is used for searching // Query is used for searching
if (typeof search_query === 'string') { if (typeof search_query === 'string' && search_query.length > 0) {
query.where(function () { query.where(function () {
this.where('meta', 'like', '%' + search_query + '%'); this.where(castJsonIfNeed('meta'), 'like', '%' + search_query + '%');
}); });
} }

View File

@ -6,6 +6,7 @@ const internalHost = require('./host');
const internalNginx = require('./nginx'); const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log'); const internalAuditLog = require('./audit-log');
const internalCertificate = require('./certificate'); const internalCertificate = require('./certificate');
const {castJsonIfNeed} = require('../lib/helpers');
function omissions () { function omissions () {
return ['is_deleted']; return ['is_deleted'];
@ -409,16 +410,16 @@ const internalDeadHost = {
.where('is_deleted', 0) .where('is_deleted', 0)
.groupBy('id') .groupBy('id')
.allowGraph('[owner,certificate]') .allowGraph('[owner,certificate]')
.orderBy('domain_names', 'ASC'); .orderBy(castJsonIfNeed('domain_names'), 'ASC');
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1)); query.andWhere('owner_user_id', access.token.getUserId(1));
} }
// Query is used for searching // Query is used for searching
if (typeof search_query === 'string') { if (typeof search_query === 'string' && search_query.length > 0) {
query.where(function () { query.where(function () {
this.where('domain_names', 'like', '%' + search_query + '%'); this.where(castJsonIfNeed('domain_names'), 'like', '%' + search_query + '%');
}); });
} }

View File

@ -2,6 +2,7 @@ const _ = require('lodash');
const proxyHostModel = require('../models/proxy_host'); const proxyHostModel = require('../models/proxy_host');
const redirectionHostModel = require('../models/redirection_host'); const redirectionHostModel = require('../models/redirection_host');
const deadHostModel = require('../models/dead_host'); const deadHostModel = require('../models/dead_host');
const {castJsonIfNeed} = require('../lib/helpers');
const internalHost = { const internalHost = {
@ -17,7 +18,7 @@ const internalHost = {
cleanSslHstsData: function (data, existing_data) { cleanSslHstsData: function (data, existing_data) {
existing_data = existing_data === undefined ? {} : existing_data; existing_data = existing_data === undefined ? {} : existing_data;
let combined_data = _.assign({}, existing_data, data); const combined_data = _.assign({}, existing_data, data);
if (!combined_data.certificate_id) { if (!combined_data.certificate_id) {
combined_data.ssl_forced = false; combined_data.ssl_forced = false;
@ -73,7 +74,7 @@ const internalHost = {
* @returns {Promise} * @returns {Promise}
*/ */
getHostsWithDomains: function (domain_names) { getHostsWithDomains: function (domain_names) {
let promises = [ const promises = [
proxyHostModel proxyHostModel
.query() .query()
.where('is_deleted', 0), .where('is_deleted', 0),
@ -125,19 +126,19 @@ const internalHost = {
* @returns {Promise} * @returns {Promise}
*/ */
isHostnameTaken: function (hostname, ignore_type, ignore_id) { isHostnameTaken: function (hostname, ignore_type, ignore_id) {
let promises = [ const promises = [
proxyHostModel proxyHostModel
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.andWhere('domain_names', 'like', '%' + hostname + '%'), .andWhere(castJsonIfNeed('domain_names'), 'like', '%' + hostname + '%'),
redirectionHostModel redirectionHostModel
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.andWhere('domain_names', 'like', '%' + hostname + '%'), .andWhere(castJsonIfNeed('domain_names'), 'like', '%' + hostname + '%'),
deadHostModel deadHostModel
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.andWhere('domain_names', 'like', '%' + hostname + '%') .andWhere(castJsonIfNeed('domain_names'), 'like', '%' + hostname + '%')
]; ];
return Promise.all(promises) return Promise.all(promises)

View File

@ -6,6 +6,7 @@ const internalHost = require('./host');
const internalNginx = require('./nginx'); const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log'); const internalAuditLog = require('./audit-log');
const internalCertificate = require('./certificate'); const internalCertificate = require('./certificate');
const {castJsonIfNeed} = require('../lib/helpers');
function omissions () { function omissions () {
return ['is_deleted', 'owner.is_deleted']; return ['is_deleted', 'owner.is_deleted'];
@ -416,16 +417,16 @@ const internalProxyHost = {
.where('is_deleted', 0) .where('is_deleted', 0)
.groupBy('id') .groupBy('id')
.allowGraph('[owner,access_list,certificate]') .allowGraph('[owner,access_list,certificate]')
.orderBy('domain_names', 'ASC'); .orderBy(castJsonIfNeed('domain_names'), 'ASC');
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1)); query.andWhere('owner_user_id', access.token.getUserId(1));
} }
// Query is used for searching // Query is used for searching
if (typeof search_query === 'string') { if (typeof search_query === 'string' && search_query.length > 0) {
query.where(function () { query.where(function () {
this.where('domain_names', 'like', '%' + search_query + '%'); this.where(castJsonIfNeed('domain_names'), 'like', `%${search_query}%`);
}); });
} }

View File

@ -6,6 +6,7 @@ const internalHost = require('./host');
const internalNginx = require('./nginx'); const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log'); const internalAuditLog = require('./audit-log');
const internalCertificate = require('./certificate'); const internalCertificate = require('./certificate');
const {castJsonIfNeed} = require('../lib/helpers');
function omissions () { function omissions () {
return ['is_deleted']; return ['is_deleted'];
@ -409,16 +410,16 @@ const internalRedirectionHost = {
.where('is_deleted', 0) .where('is_deleted', 0)
.groupBy('id') .groupBy('id')
.allowGraph('[owner,certificate]') .allowGraph('[owner,certificate]')
.orderBy('domain_names', 'ASC'); .orderBy(castJsonIfNeed('domain_names'), 'ASC');
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1)); query.andWhere('owner_user_id', access.token.getUserId(1));
} }
// Query is used for searching // Query is used for searching
if (typeof search_query === 'string') { if (typeof search_query === 'string' && search_query.length > 0) {
query.where(function () { query.where(function () {
this.where('domain_names', 'like', '%' + search_query + '%'); this.where(castJsonIfNeed('domain_names'), 'like', `%${search_query}%`);
}); });
} }

View File

@ -4,6 +4,7 @@ const utils = require('../lib/utils');
const streamModel = require('../models/stream'); const streamModel = require('../models/stream');
const internalNginx = require('./nginx'); const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log'); const internalAuditLog = require('./audit-log');
const {castJsonIfNeed} = require('../lib/helpers');
function omissions () { function omissions () {
return ['is_deleted']; return ['is_deleted'];
@ -293,21 +294,21 @@ const internalStream = {
getAll: (access, expand, search_query) => { getAll: (access, expand, search_query) => {
return access.can('streams:list') return access.can('streams:list')
.then((access_data) => { .then((access_data) => {
let query = streamModel const query = streamModel
.query() .query()
.where('is_deleted', 0) .where('is_deleted', 0)
.groupBy('id') .groupBy('id')
.allowGraph('[owner]') .allowGraph('[owner]')
.orderBy('incoming_port', 'ASC'); .orderByRaw('CAST(incoming_port AS INTEGER) ASC');
if (access_data.permission_visibility !== 'all') { if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1)); query.andWhere('owner_user_id', access.token.getUserId(1));
} }
// Query is used for searching // Query is used for searching
if (typeof search_query === 'string') { if (typeof search_query === 'string' && search_query.length > 0) {
query.where(function () { query.where(function () {
this.where('incoming_port', 'like', '%' + search_query + '%'); this.where(castJsonIfNeed('incoming_port'), 'like', `%${search_query}%`);
}); });
} }
@ -327,9 +328,9 @@ const internalStream = {
* @returns {Promise} * @returns {Promise}
*/ */
getCount: (user_id, visibility) => { getCount: (user_id, visibility) => {
let query = streamModel const query = streamModel
.query() .query()
.count('id as count') .count('id AS count')
.where('is_deleted', 0); .where('is_deleted', 0);
if (visibility !== 'all') { if (visibility !== 'all') {

View File

@ -5,6 +5,8 @@ const authModel = require('../models/auth');
const helpers = require('../lib/helpers'); const helpers = require('../lib/helpers');
const TokenModel = require('../models/token'); const TokenModel = require('../models/token');
const ERROR_MESSAGE_INVALID_AUTH = 'Invalid email or password';
module.exports = { module.exports = {
/** /**
@ -69,60 +71,19 @@ module.exports = {
}; };
}); });
} else { } else {
throw new error.AuthError('Invalid password'); throw new error.AuthError(ERROR_MESSAGE_INVALID_AUTH);
} }
}); });
} else { } else {
throw new error.AuthError('No password auth for user'); throw new error.AuthError(ERROR_MESSAGE_INVALID_AUTH);
} }
}); });
} else { } else {
throw new error.AuthError('No relevant user found'); throw new error.AuthError(ERROR_MESSAGE_INVALID_AUTH);
} }
}); });
}, },
/**
* @param {Object} data
* @param {String} data.identity
* @param {String} [issuer]
* @returns {Promise}
*/
getTokenFromOAuthClaim: (data) => {
let Token = new TokenModel();
data.scope = 'user';
data.expiry = '1d';
return userModel
.query()
.where('email', data.identity)
.andWhere('is_deleted', 0)
.andWhere('is_disabled', 0)
.first()
.then((user) => {
if (!user) {
throw new error.AuthError('No relevant user found');
}
// 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);
}
let iss = 'api',
attrs = { id: user.id },
scope = [ data.scope ],
expiresIn = data.expiry;
return Token.create({ iss, attrs, scope, expiresIn })
.then((signed) => {
return { token: signed.token, expires: expiry.toISOString() };
});
});
},
/** /**
* @param {Access} access * @param {Access} access
* @param {Object} [data] * @param {Object} [data]

View File

@ -2,7 +2,10 @@ const fs = require('fs');
const NodeRSA = require('node-rsa'); const NodeRSA = require('node-rsa');
const logger = require('../logger').global; const logger = require('../logger').global;
const keysFile = '/data/keys.json'; const keysFile = '/data/keys.json';
const mysqlEngine = 'mysql2';
const postgresEngine = 'pg';
const sqliteClientName = 'sqlite3';
let instance = null; let instance = null;
@ -14,7 +17,7 @@ const configure = () => {
let configData; let configData;
try { try {
configData = require(filename); configData = require(filename);
} catch (err) { } catch (_) {
// do nothing // do nothing
} }
@ -34,7 +37,7 @@ const configure = () => {
logger.info('Using MySQL configuration'); logger.info('Using MySQL configuration');
instance = { instance = {
database: { database: {
engine: 'mysql2', engine: mysqlEngine,
host: envMysqlHost, host: envMysqlHost,
port: process.env.DB_MYSQL_PORT || 3306, port: process.env.DB_MYSQL_PORT || 3306,
user: envMysqlUser, user: envMysqlUser,
@ -46,13 +49,33 @@ const configure = () => {
return; return;
} }
const envPostgresHost = process.env.DB_POSTGRES_HOST || null;
const envPostgresUser = process.env.DB_POSTGRES_USER || null;
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');
instance = {
database: {
engine: postgresEngine,
host: envPostgresHost,
port: process.env.DB_POSTGRES_PORT || 5432,
user: envPostgresUser,
password: process.env.DB_POSTGRES_PASSWORD,
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}`); logger.info(`Using Sqlite: ${envSqliteFile}`);
instance = { instance = {
database: { database: {
engine: 'knex-native', engine: 'knex-native',
knex: { knex: {
client: 'sqlite3', client: sqliteClientName,
connection: { connection: {
filename: envSqliteFile filename: envSqliteFile
}, },
@ -143,7 +166,27 @@ module.exports = {
*/ */
isSqlite: function () { isSqlite: function () {
instance === null && configure(); instance === null && configure();
return instance.database.knex && instance.database.knex.client === 'sqlite3'; return instance.database.knex && instance.database.knex.client === sqliteClientName;
},
/**
* 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;
}, },
/** /**

View File

@ -4,14 +4,7 @@ module.exports = () => {
return function (req, res, next) { return function (req, res, next) {
res.locals.access = null; res.locals.access = null;
let access = new Access(res.locals.token || null); let access = new Access(res.locals.token || null);
access.load()
// Allow unauthenticated access to get the oidc configuration
let oidc_access =
req.url === '/oidc-config' &&
req.method === 'GET' &&
!access.token.getUserId();
access.load(oidc_access)
.then(() => { .then(() => {
res.locals.access = access; res.locals.access = access;
next(); next();

View File

@ -1,4 +1,6 @@
const moment = require('moment'); const moment = require('moment');
const {isPostgres} = require('./config');
const {ref} = require('objection');
module.exports = { module.exports = {
@ -45,6 +47,16 @@ module.exports = {
} }
}); });
return obj; 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;
} }
}; };

View File

@ -10,6 +10,5 @@ module.exports = {
certbot: new Signale({scope: 'Certbot '}), certbot: new Signale({scope: 'Certbot '}),
import: new Signale({scope: 'Importer '}), import: new Signale({scope: 'Importer '}),
setup: new Signale({scope: 'Setup '}), setup: new Signale({scope: 'Setup '}),
ip_ranges: new Signale({scope: 'IP Ranges'}), ip_ranges: new Signale({scope: 'IP Ranges'})
oidc: new Signale({scope: 'OIDC '})
}; };

View File

@ -17,6 +17,9 @@ const boolFields = [
'preserve_path', 'preserve_path',
'ssl_forced', 'ssl_forced',
'block_exploits', 'block_exploits',
'hsts_enabled',
'hsts_subdomains',
'http2_support',
]; ];
class RedirectionHost extends Model { class RedirectionHost extends Model {

View File

@ -21,10 +21,9 @@
"moment": "^2.29.4", "moment": "^2.29.4",
"mysql2": "^3.11.1", "mysql2": "^3.11.1",
"node-rsa": "^1.0.8", "node-rsa": "^1.0.8",
"nodemon": "^2.0.2",
"openid-client": "^5.4.0",
"objection": "3.0.1", "objection": "3.0.1",
"path": "^0.12.7", "path": "^0.12.7",
"pg": "^8.13.1",
"signale": "1.4.0", "signale": "1.4.0",
"sqlite3": "5.1.6", "sqlite3": "5.1.6",
"temp-write": "^4.0.0" "temp-write": "^4.0.0"
@ -46,4 +45,4 @@
"scripts": { "scripts": {
"validate-schema": "node validate-schema.js" "validate-schema": "node validate-schema.js"
} }
} }

View File

@ -27,7 +27,6 @@ router.get('/', (req, res/*, next*/) => {
router.use('/schema', require('./schema')); router.use('/schema', require('./schema'));
router.use('/tokens', require('./tokens')); router.use('/tokens', require('./tokens'));
router.use('/oidc', require('./oidc'));
router.use('/users', require('./users')); router.use('/users', require('./users'));
router.use('/audit-log', require('./audit-log')); router.use('/audit-log', require('./audit-log'));
router.use('/reports', require('./reports')); router.use('/reports', require('./reports'));

View File

@ -1,168 +0,0 @@
const crypto = require('crypto');
const error = require('../lib/error');
const express = require('express');
const jwtdecode = require('../lib/express/jwt-decode');
const logger = require('../logger').oidc;
const oidc = require('openid-client');
const settingModel = require('../models/setting');
const internalToken = require('../internal/token');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/oidc
*
* OAuth Authorization Code flow initialisation
*/
.get(jwtdecode(), async (req, res) => {
logger.info('Initializing OAuth flow');
settingModel
.query()
.where({id: 'oidc-config'})
.first()
.then((row) => getInitParams(req, row))
.then((params) => redirectToAuthorizationURL(res, params))
.catch((err) => redirectWithError(res, err));
});
router
.route('/callback')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/oidc/callback
*
* Oauth Authorization Code flow callback
*/
.get(jwtdecode(), async (req, res) => {
logger.info('Processing callback');
settingModel
.query()
.where({id: 'oidc-config'})
.first()
.then((settings) => validateCallback(req, settings))
.then((token) => redirectWithJwtToken(res, token))
.catch((err) => redirectWithError(res, err));
});
/**
* Executes discovery and returns the configured `openid-client` client
*
* @param {Setting} row
* */
let getClient = async (row) => {
let issuer;
try {
issuer = await oidc.Issuer.discover(row.meta.issuerURL);
} catch (err) {
throw new error.AuthError(`Discovery failed for the specified URL with message: ${err.message}`);
}
return new issuer.Client({
client_id: row.meta.clientID,
client_secret: row.meta.clientSecret,
redirect_uris: [row.meta.redirectURL],
response_types: ['code'],
});
};
/**
* Generates state, nonce and authorization url.
*
* @param {Request} req
* @param {Setting} row
* @return { {String}, {String}, {String} } state, nonce and url
* */
let getInitParams = async (req, row) => {
let client = await getClient(row),
state = crypto.randomUUID(),
nonce = crypto.randomUUID(),
url = client.authorizationUrl({
scope: 'openid email profile',
resource: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
state,
nonce,
});
return { state, nonce, url };
};
/**
* Parses state and nonce from cookie during the callback phase.
*
* @param {Request} req
* @return { {String}, {String} } state and nonce
* */
let parseStateFromCookie = (req) => {
let state, nonce;
let cookies = req.headers.cookie.split(';');
for (let cookie of cookies) {
if (cookie.split('=')[0].trim() === 'npm_oidc') {
let raw = cookie.split('=')[1],
val = raw.split('--');
state = val[0].trim();
nonce = val[1].trim();
break;
}
}
return { state, nonce };
};
/**
* Executes validation of callback parameters.
*
* @param {Request} req
* @param {Setting} settings
* @return {Promise} a promise resolving to a jwt token
* */
let validateCallback = async (req, settings) => {
let client = await getClient(settings);
let { state, nonce } = parseStateFromCookie(req);
const params = client.callbackParams(req);
const tokenSet = await client.callback(settings.meta.redirectURL, params, { state, nonce });
let claims = tokenSet.claims();
if (!claims.email) {
throw new error.AuthError('The Identity Provider didn\'t send the \'email\' claim');
} else {
logger.info('Successful authentication for email ' + claims.email);
}
return internalToken.getTokenFromOAuthClaim({ identity: claims.email });
};
let redirectToAuthorizationURL = (res, params) => {
logger.info('Authorization URL: ' + params.url);
res.cookie('npm_oidc', params.state + '--' + params.nonce);
res.redirect(params.url);
};
let redirectWithJwtToken = (res, token) => {
res.cookie('npm_oidc', token.token + '---' + token.expires);
res.redirect('/login');
};
let redirectWithError = (res, error) => {
logger.error('Callback error: ' + error.message);
res.cookie('npm_oidc_error', error.message);
res.redirect('/login');
};
module.exports = router;

View File

@ -71,18 +71,6 @@ router
}); });
}) })
.then((row) => { .then((row) => {
if (row.id === 'oidc-config') {
// Redact oidc configuration via api (unauthenticated get call)
let m = row.meta;
row.meta = {
name: m.name,
enabled: m.enabled === true && !!(m.clientID && m.clientSecret && m.issuerURL && m.redirectURL && m.name)
};
// Remove these temporary cookies used during oidc authentication
res.clearCookie('npm_oidc');
res.clearCookie('npm_oidc_error');
}
res.status(200) res.status(200)
.send(row); .send(row);
}) })

View File

@ -29,8 +29,6 @@ router
scope: (typeof req.query.scope !== 'undefined' ? req.query.scope : null) scope: (typeof req.query.scope !== 'undefined' ? req.query.scope : null)
}) })
.then((data) => { .then((data) => {
// clear this temporary cookie following a successful oidc authentication
res.clearCookie('npm_oidc');
res.status(200) res.status(200)
.send(data); .send(data);
}) })

View File

@ -19,7 +19,9 @@
"incoming_port": { "incoming_port": {
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535,
"if": {"properties": {"tcp_forwarding": {"const": true}}},
"then": {"not": {"oneOf": [{"const": 80}, {"const": 443}]}}
}, },
"forwarding_host": { "forwarding_host": {
"anyOf": [ "anyOf": [

View File

@ -14,7 +14,7 @@
"schema": { "schema": {
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
"enum": ["default-site", "oidc-config"] "enum": ["default-site"]
}, },
"required": true, "required": true,
"description": "Setting ID", "description": "Setting ID",
@ -27,69 +27,28 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"oneOf": [ "type": "object",
{ "additionalProperties": false,
"type": "object", "minProperties": 1,
"additionalProperties": false, "properties": {
"minProperties": 1, "value": {
"properties": { "type": "string",
"value": { "minLength": 1,
"type": "string", "enum": ["congratulations", "404", "444", "redirect", "html"]
"minLength": 1,
"enum": [
"congratulations",
"404",
"444",
"redirect",
"html"
]
},
"meta": {
"type": "object",
"additionalProperties": false,
"properties": {
"redirect": {
"type": "string"
},
"html": {
"type": "string"
}
}
}
}
}, },
{ "meta": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"minProperties": 1,
"properties": { "properties": {
"meta": { "redirect": {
"type": "object", "type": "string"
"additionalProperties": false, },
"properties": { "html": {
"clientID": { "type": "string"
"type": "string"
},
"clientSecret": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"issuerURL": {
"type": "string"
},
"name": {
"type": "string"
},
"redirectURL": {
"type": "string"
}
}
} }
} }
} }
] }
} }
} }
} }

View File

@ -15,18 +15,18 @@ const certbot = require('./lib/certbot');
const setupDefaultUser = () => { const setupDefaultUser = () => {
return userModel return userModel
.query() .query()
.select(userModel.raw('COUNT(`id`) as `count`')) .select('id', )
.where('is_deleted', 0) .where('is_deleted', 0)
.first() .first()
.then((row) => { .then((row) => {
if (!row.count) { if (!row || !row.id) {
// Create a new user and set password // Create a new user and set password
let email = process.env.INITIAL_ADMIN_EMAIL || 'admin@example.com'; const email = process.env.INITIAL_ADMIN_EMAIL || 'admin@example.com';
let password = process.env.INITIAL_ADMIN_PASSWORD || 'changeme'; const password = process.env.INITIAL_ADMIN_PASSWORD || 'changeme';
logger.info('Creating a new user: ' + email + ' with password: ' + password); logger.info('Creating a new user: ' + email + ' with password: ' + password);
let data = { const data = {
is_deleted: 0, is_deleted: 0,
email: email, email: email,
name: 'Administrator', name: 'Administrator',
@ -75,56 +75,30 @@ const setupDefaultUser = () => {
* @returns {Promise} * @returns {Promise}
*/ */
const setupDefaultSettings = () => { const setupDefaultSettings = () => {
return Promise.all([ return settingModel
settingModel .query()
.query() .select('id')
.select(settingModel.raw('COUNT(`id`) as `count`')) .where({id: 'default-site'})
.where({id: 'default-site'}) .first()
.first() .then((row) => {
.then((row) => { if (!row || !row.id) {
if (!row.count) { settingModel
settingModel .query()
.query() .insert({
.insert({ id: 'default-site',
id: 'default-site', name: 'Default Site',
name: 'Default Site', description: 'What to show when Nginx is hit with an unknown Host',
description: 'What to show when Nginx is hit with an unknown Host', value: 'congratulations',
value: 'congratulations', meta: {},
meta: {}, })
}) .then(() => {
.then(() => { logger.info('Default settings added');
logger.info('Added default-site setting'); });
}); }
} if (config.debug()) {
if (config.debug()) { logger.info('Default setting setup not required');
logger.info('Default setting setup not required'); }
} });
}),
settingModel
.query()
.select(settingModel.raw('COUNT(`id`) as `count`'))
.where({id: 'oidc-config'})
.first()
.then((row) => {
if (!row.count) {
settingModel
.query()
.insert({
id: 'oidc-config',
name: 'Open ID Connect',
description: 'Sign in to Nginx Proxy Manager with an external Identity Provider',
value: 'metadata',
meta: {},
})
.then(() => {
logger.info('Added oidc-config setting');
});
}
if (config.debug()) {
logger.info('Default setting setup not required');
}
})
]);
}; };
/** /**

View File

@ -22,5 +22,7 @@ server {
} }
{% endif %} {% endif %}
# Custom
include /data/nginx/custom/server_dead[.]conf;
} }
{% endif %} {% endif %}

File diff suppressed because it is too large Load Diff

8
docker/ci.env Normal file
View File

@ -0,0 +1,8 @@
AUTHENTIK_SECRET_KEY=gl8woZe8L6IIX8SC0c5Ocsj0xPkX5uJo5DVZCFl+L/QGbzuplfutYuua2ODNLEiDD3aFd9H2ylJmrke0
AUTHENTIK_REDIS__HOST=authentik-redis
AUTHENTIK_POSTGRESQL__HOST=db-postgres
AUTHENTIK_POSTGRESQL__USER=authentik
AUTHENTIK_POSTGRESQL__NAME=authentik
AUTHENTIK_POSTGRESQL__PASSWORD=07EKS5NLI6Tpv68tbdvrxfvj
AUTHENTIK_BOOTSTRAP_PASSWORD=admin
AUTHENTIK_BOOTSTRAP_EMAIL=admin@example.com

Binary file not shown.

View File

@ -29,7 +29,8 @@ COPY scripts/install-s6 /tmp/install-s6
RUN rm -f /etc/nginx/conf.d/production.conf \ RUN rm -f /etc/nginx/conf.d/production.conf \
&& chmod 644 /etc/logrotate.d/nginx-proxy-manager \ && chmod 644 /etc/logrotate.d/nginx-proxy-manager \
&& /tmp/install-s6 "${TARGETPLATFORM}" \ && /tmp/install-s6 "${TARGETPLATFORM}" \
&& rm -f /tmp/install-s6 && rm -f /tmp/install-s6 \
&& chmod 644 -R /root/.cache
# Certs for testing purposes # Certs for testing purposes
COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem

View File

@ -0,0 +1,78 @@
# WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production.
services:
cypress:
environment:
CYPRESS_stack: 'postgres'
fullstack:
environment:
DB_POSTGRES_HOST: 'db-postgres'
DB_POSTGRES_PORT: '5432'
DB_POSTGRES_USER: 'npm'
DB_POSTGRES_PASSWORD: 'npmpass'
DB_POSTGRES_NAME: 'npm'
depends_on:
- db-postgres
- authentik
- authentik-worker
- authentik-ldap
db-postgres:
image: postgres:latest
environment:
POSTGRES_USER: 'npm'
POSTGRES_PASSWORD: 'npmpass'
POSTGRES_DB: 'npm'
volumes:
- psql_vol:/var/lib/postgresql/data
- ./ci/postgres:/docker-entrypoint-initdb.d
networks:
- fulltest
authentik-redis:
image: 'redis:alpine'
command: --save 60 1 --loglevel warning
restart: unless-stopped
healthcheck:
test: ['CMD-SHELL', 'redis-cli ping | grep PONG']
start_period: 20s
interval: 30s
retries: 5
timeout: 3s
volumes:
- redis_vol:/data
authentik:
image: ghcr.io/goauthentik/server:2024.10.1
restart: unless-stopped
command: server
env_file:
- ci.env
depends_on:
- authentik-redis
- db-postgres
authentik-worker:
image: ghcr.io/goauthentik/server:2024.10.1
restart: unless-stopped
command: worker
env_file:
- ci.env
depends_on:
- authentik-redis
- db-postgres
authentik-ldap:
image: ghcr.io/goauthentik/ldap:2024.10.1
environment:
AUTHENTIK_HOST: 'http://authentik:9000'
AUTHENTIK_INSECURE: 'true'
AUTHENTIK_TOKEN: 'wKYZuRcI0ETtb8vWzMCr04oNbhrQUUICy89hSpDln1OEKLjiNEuQ51044Vkp'
restart: unless-stopped
depends_on:
- authentik
volumes:
psql_vol:
redis_vol:

View File

@ -2,8 +2,8 @@
services: services:
fullstack: fullstack:
image: nginxproxymanager:dev image: npm2dev:core
container_name: npm_core container_name: npm2dev.core
build: build:
context: ./ context: ./
dockerfile: ./dev/Dockerfile dockerfile: ./dev/Dockerfile
@ -26,11 +26,17 @@ services:
DEVELOPMENT: 'true' DEVELOPMENT: 'true'
LE_STAGING: 'true' LE_STAGING: 'true'
# db: # db:
DB_MYSQL_HOST: 'db' # DB_MYSQL_HOST: 'db'
DB_MYSQL_PORT: '3306' # DB_MYSQL_PORT: '3306'
DB_MYSQL_USER: 'npm' # DB_MYSQL_USER: 'npm'
DB_MYSQL_PASSWORD: 'npm' # DB_MYSQL_PASSWORD: 'npm'
DB_MYSQL_NAME: 'npm' # DB_MYSQL_NAME: 'npm'
# db-postgres:
DB_POSTGRES_HOST: 'db-postgres'
DB_POSTGRES_PORT: '5432'
DB_POSTGRES_USER: 'npm'
DB_POSTGRES_PASSWORD: 'npmpass'
DB_POSTGRES_NAME: 'npm'
# DB_SQLITE_FILE: "/data/database.sqlite" # DB_SQLITE_FILE: "/data/database.sqlite"
# DISABLE_IPV6: "true" # DISABLE_IPV6: "true"
# Required for DNS Certificate provisioning testing: # Required for DNS Certificate provisioning testing:
@ -49,11 +55,15 @@ services:
timeout: 3s timeout: 3s
depends_on: depends_on:
- db - db
- db-postgres
- authentik
- authentik-worker
- authentik-ldap
working_dir: /app working_dir: /app
db: db:
image: jc21/mariadb-aria image: jc21/mariadb-aria
container_name: npm_db container_name: npm2dev.db
ports: ports:
- 33306:3306 - 33306:3306
networks: networks:
@ -66,8 +76,22 @@ services:
volumes: volumes:
- db_data:/var/lib/mysql - db_data:/var/lib/mysql
db-postgres:
image: postgres:latest
container_name: npm2dev.db-postgres
networks:
- nginx_proxy_manager
environment:
POSTGRES_USER: 'npm'
POSTGRES_PASSWORD: 'npmpass'
POSTGRES_DB: 'npm'
volumes:
- psql_data:/var/lib/postgresql/data
- ./ci/postgres:/docker-entrypoint-initdb.d
stepca: stepca:
image: jc21/testca image: jc21/testca
container_name: npm2dev.stepca
volumes: volumes:
- './dev/resolv.conf:/etc/resolv.conf:ro' - './dev/resolv.conf:/etc/resolv.conf:ro'
- '/etc/localtime:/etc/localtime:ro' - '/etc/localtime:/etc/localtime:ro'
@ -78,6 +102,7 @@ services:
dnsrouter: dnsrouter:
image: jc21/dnsrouter image: jc21/dnsrouter
container_name: npm2dev.dnsrouter
volumes: volumes:
- ./dev/dnsrouter-config.json.tmp:/dnsrouter-config.json:ro - ./dev/dnsrouter-config.json.tmp:/dnsrouter-config.json:ro
networks: networks:
@ -85,7 +110,7 @@ services:
swagger: swagger:
image: swaggerapi/swagger-ui:latest image: swaggerapi/swagger-ui:latest
container_name: npm_swagger container_name: npm2dev.swagger
ports: ports:
- 3082:80 - 3082:80
environment: environment:
@ -96,7 +121,7 @@ services:
squid: squid:
image: ubuntu/squid image: ubuntu/squid
container_name: npm_squid container_name: npm2dev.squid
volumes: volumes:
- './dev/squid.conf:/etc/squid/squid.conf:ro' - './dev/squid.conf:/etc/squid/squid.conf:ro'
- './dev/resolv.conf:/etc/resolv.conf:ro' - './dev/resolv.conf:/etc/resolv.conf:ro'
@ -108,6 +133,7 @@ services:
pdns: pdns:
image: pschiffe/pdns-mysql image: pschiffe/pdns-mysql
container_name: npm2dev.pdns
volumes: volumes:
- '/etc/localtime:/etc/localtime:ro' - '/etc/localtime:/etc/localtime:ro'
environment: environment:
@ -136,6 +162,7 @@ services:
pdns-db: pdns-db:
image: mariadb image: mariadb
container_name: npm2dev.pdns-db
environment: environment:
MYSQL_ROOT_PASSWORD: 'pdns' MYSQL_ROOT_PASSWORD: 'pdns'
MYSQL_DATABASE: 'pdns' MYSQL_DATABASE: 'pdns'
@ -149,7 +176,8 @@ services:
- nginx_proxy_manager - nginx_proxy_manager
cypress: cypress:
image: "npm_dev_cypress" image: npm2dev:cypress
container_name: npm2dev.cypress
build: build:
context: ../ context: ../
dockerfile: test/cypress/Dockerfile dockerfile: test/cypress/Dockerfile
@ -164,16 +192,77 @@ services:
networks: networks:
- nginx_proxy_manager - nginx_proxy_manager
authentik-redis:
image: 'redis:alpine'
container_name: npm2dev.authentik-redis
command: --save 60 1 --loglevel warning
networks:
- nginx_proxy_manager
restart: unless-stopped
healthcheck:
test: ['CMD-SHELL', 'redis-cli ping | grep PONG']
start_period: 20s
interval: 30s
retries: 5
timeout: 3s
volumes:
- redis_data:/data
authentik:
image: ghcr.io/goauthentik/server:2024.10.1
container_name: npm2dev.authentik
restart: unless-stopped
command: server
networks:
- nginx_proxy_manager
env_file:
- ci.env
ports:
- 9000:9000
depends_on:
- authentik-redis
- db-postgres
authentik-worker:
image: ghcr.io/goauthentik/server:2024.10.1
container_name: npm2dev.authentik-worker
restart: unless-stopped
command: worker
networks:
- nginx_proxy_manager
env_file:
- ci.env
depends_on:
- authentik-redis
- db-postgres
authentik-ldap:
image: ghcr.io/goauthentik/ldap:2024.10.1
container_name: npm2dev.authentik-ldap
networks:
- nginx_proxy_manager
environment:
AUTHENTIK_HOST: 'http://authentik:9000'
AUTHENTIK_INSECURE: 'true'
AUTHENTIK_TOKEN: 'wKYZuRcI0ETtb8vWzMCr04oNbhrQUUICy89hSpDln1OEKLjiNEuQ51044Vkp'
restart: unless-stopped
depends_on:
- authentik
volumes: volumes:
npm_data: npm_data:
name: npm_core_data name: npm2dev_core_data
le_data: le_data:
name: npm_le_data name: npm2dev_le_data
db_data: db_data:
name: npm_db_data name: npm2dev_db_data
pdns_mysql: pdns_mysql:
name: npm_pdns_mysql name: npnpm2dev_pdns_mysql
psql_data:
name: npm2dev_psql_data
redis_data:
name: npm2dev_redis_data
networks: networks:
nginx_proxy_manager: nginx_proxy_manager:
name: npm_network name: npm2dev_network

View File

@ -1,4 +1,4 @@
location ~* ^.*\.(css|js|jpe?g|gif|png|webp|woff|eot|ttf|svg|ico|css\.map|js\.map)$ { location ~* ^.*\.(css|js|jpe?g|gif|png|webp|woff|woff2|eot|ttf|svg|ico|css\.map|js\.map)$ {
if_modified_since off; if_modified_since off;
# use the public cache # use the public cache

View File

@ -50,7 +50,6 @@ networks:
Let's look at a Portainer example: Let's look at a Portainer example:
```yml ```yml
version: '3.8'
services: services:
portainer: portainer:
@ -92,8 +91,6 @@ This image supports the use of Docker secrets to import from files and keep sens
You can set any environment variable from a file by appending `__FILE` (double-underscore FILE) to the environmental variable name. You can set any environment variable from a file by appending `__FILE` (double-underscore FILE) to the environmental variable name.
```yml ```yml
version: '3.8'
secrets: secrets:
# Secrets are single-line text files where the sole content is the secret # Secrets are single-line text files where the sole content is the secret
# Paths in this example assume that secrets are kept in local folder called ".secrets" # Paths in this example assume that secrets are kept in local folder called ".secrets"
@ -184,6 +181,7 @@ You can add your custom configuration snippet files at `/data/nginx/custom` as f
- `/data/nginx/custom/server_stream.conf`: Included at the end of every stream server block - `/data/nginx/custom/server_stream.conf`: Included at the end of every stream server block
- `/data/nginx/custom/server_stream_tcp.conf`: Included at the end of every TCP stream server block - `/data/nginx/custom/server_stream_tcp.conf`: Included at the end of every TCP stream server block
- `/data/nginx/custom/server_stream_udp.conf`: Included at the end of every UDP stream server block - `/data/nginx/custom/server_stream_udp.conf`: Included at the end of every UDP stream server block
- `/data/nginx/custom/server_dead.conf`: Included at the end of every 404 server block
Every file is optional. Every file is optional.

View File

@ -9,7 +9,6 @@ outline: deep
Create a `docker-compose.yml` file: Create a `docker-compose.yml` file:
```yml ```yml
version: '3.8'
services: services:
app: app:
image: 'jc21/nginx-proxy-manager:latest' image: 'jc21/nginx-proxy-manager:latest'
@ -22,8 +21,7 @@ services:
# Add any other Stream port you want to expose # Add any other Stream port you want to expose
# - '21:21' # FTP # - '21:21' # FTP
# Uncomment the next line if you uncomment anything in the section environment:
# environment:
# Uncomment this if you want to change the location of # Uncomment this if you want to change the location of
# the SQLite DB file within the container # the SQLite DB file within the container
# DB_SQLITE_FILE: "/data/database.sqlite" # DB_SQLITE_FILE: "/data/database.sqlite"
@ -55,7 +53,6 @@ are going to use.
Here is an example of what your `docker-compose.yml` will look like when using a MariaDB container: Here is an example of what your `docker-compose.yml` will look like when using a MariaDB container:
```yml ```yml
version: '3.8'
services: services:
app: app:
image: 'jc21/nginx-proxy-manager:latest' image: 'jc21/nginx-proxy-manager:latest'
@ -101,6 +98,53 @@ Please note, that `DB_MYSQL_*` environment variables will take precedent over `D
::: :::
## Using Postgres database
Similar to the MySQL server setup:
```yml
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
# These ports are in format <host-port>:<container-port>
- '80:80' # Public HTTP Port
- '443:443' # Public HTTPS Port
- '81:81' # Admin Web Port
# Add any other Stream port you want to expose
# - '21:21' # FTP
environment:
# Postgres parameters:
DB_POSTGRES_HOST: 'db'
DB_POSTGRES_PORT: '5432'
DB_POSTGRES_USER: 'npm'
DB_POSTGRES_PASSWORD: 'npmpass'
DB_POSTGRES_NAME: 'npm'
# Uncomment this if IPv6 is not enabled on your host
# DISABLE_IPV6: 'true'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
depends_on:
- db
db:
image: postgres:latest
environment:
POSTGRES_USER: 'npm'
POSTGRES_PASSWORD: 'npmpass'
POSTGRES_DB: 'npm'
volumes:
- ./postgres:/var/lib/postgresql/data
```
::: warning
Custom Postgres schema is not supported, as such `public` will be used.
:::
## Running on Raspberry PI / ARM devices ## Running on Raspberry PI / ARM devices
The docker images support the following architectures: The docker images support the following architectures:

View File

@ -12,6 +12,7 @@ Known integrations:
- [HomeAssistant Hass.io plugin](https://github.com/hassio-addons/addon-nginx-proxy-manager) - [HomeAssistant Hass.io plugin](https://github.com/hassio-addons/addon-nginx-proxy-manager)
- [UnRaid / Synology](https://github.com/jlesage/docker-nginx-proxy-manager) - [UnRaid / Synology](https://github.com/jlesage/docker-nginx-proxy-manager)
- [Proxmox Scripts](https://github.com/ej52/proxmox-scripts/tree/main/apps/nginx-proxy-manager) - [Proxmox Scripts](https://github.com/ej52/proxmox-scripts/tree/main/apps/nginx-proxy-manager)
- [Proxmox VE Helper-Scripts](https://community-scripts.github.io/ProxmoxVE/scripts?id=nginxproxymanager)
- [nginxproxymanagerGraf](https://github.com/ma-karai/nginxproxymanagerGraf) - [nginxproxymanagerGraf](https://github.com/ma-karai/nginxproxymanagerGraf)

View File

@ -873,9 +873,9 @@ mitt@^3.0.1:
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
nanoid@^3.3.7: nanoid@^3.3.7:
version "3.3.7" version "3.3.8"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
oniguruma-to-js@0.4.3: oniguruma-to-js@0.4.3:
version "0.4.3" version "0.4.3"

View File

@ -59,11 +59,6 @@ function fetch(verb, path, data, options) {
}, },
beforeSend: function (xhr) { beforeSend: function (xhr) {
// Allow unauthenticated access to get the oidc configuration
if (path === 'settings/oidc-config' && verb === "get") {
return;
}
xhr.setRequestHeader('Authorization', 'Bearer ' + (token ? token.t : null)); xhr.setRequestHeader('Authorization', 'Bearer ' + (token ? token.t : null));
}, },

View File

@ -434,11 +434,6 @@ module.exports = {
App.UI.showModalDialog(new View({model: model})); App.UI.showModalDialog(new View({model: model}));
}); });
} }
if (model.get('id') === 'oidc-config') {
require(['./main', './settings/oidc-config/main'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
} }
}, },

View File

@ -1,6 +1,6 @@
<td class="text-center"> <td class="text-center">
<div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>"> <div class="avatar d-block" style="background-image: url(<%- (owner && owner.avatar) || '/images/default-avatar.jpg' %>)" title="Owned by <%- (owner && owner.name) || 'a deleted user' %>">
<span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span> <span class="avatar-status <%- owner && !owner.is_disabled ? 'bg-green' : 'bg-red' %>"></span>
</div> </div>
</td> </td>
<td> <td>

View File

@ -1,6 +1,6 @@
<td class="text-center"> <td class="text-center">
<div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>"> <div class="avatar d-block" style="background-image: url(<%- (owner && owner.avatar) || '/images/default-avatar.jpg' %>)" title="Owned by <%- (owner && owner.name) || 'a deleted user' %>">
<span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span> <span class="avatar-status <%- owner && !owner.is_disabled ? 'bg-green' : 'bg-red' %>"></span>
</div> </div>
</td> </td>
<td> <td>

View File

@ -1,6 +1,6 @@
<td class="text-center"> <td class="text-center">
<div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>"> <div class="avatar d-block" style="background-image: url(<%- (owner && owner.avatar) || '/images/default-avatar.jpg' %>)" title="Owned by <%- (owner && owner.name) || 'a deleted user' %>">
<span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span> <span class="avatar-status <%- owner && !owner.is_disabled ? 'bg-green' : 'bg-red' %>"></span>
</div> </div>
</td> </td>
<td> <td>

View File

@ -1,6 +1,6 @@
<td class="text-center"> <td class="text-center">
<div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>"> <div class="avatar d-block" style="background-image: url(<%- (owner && owner.avatar) || '/images/default-avatar.jpg' %>)" title="Owned by <%- (owner && owner.name) || 'a deleted user' %>">
<span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span> <span class="avatar-status <%- owner && !owner.is_disabled ? 'bg-green' : 'bg-red' %>"></span>
</div> </div>
</td> </td>
<td> <td>

View File

@ -1,6 +1,6 @@
<td class="text-center"> <td class="text-center">
<div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>"> <div class="avatar d-block" style="background-image: url(<%- (owner && owner.avatar) || '/images/default-avatar.jpg' %>)" title="Owned by <%- (owner && owner.name) || 'a deleted user' %>">
<span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span> <span class="avatar-status <%- owner && !owner.is_disabled ? 'bg-green' : 'bg-red' %>"></span>
</div> </div>
</td> </td>
<td> <td>

View File

@ -1,6 +1,6 @@
<td class="text-center"> <td class="text-center">
<div class="avatar d-block" style="background-image: url(<%- owner.avatar || '/images/default-avatar.jpg' %>)" title="Owned by <%- owner.name %>"> <div class="avatar d-block" style="background-image: url(<%- (owner && owner.avatar) || '/images/default-avatar.jpg' %>)" title="Owned by <%- (owner && owner.name) || 'a deleted user' %>">
<span class="avatar-status <%- owner.is_disabled ? 'bg-red' : 'bg-green' %>"></span> <span class="avatar-status <%- owner && !owner.is_disabled ? 'bg-green' : 'bg-red' %>"></span>
</div> </div>
</td> </td>
<td> <td>

View File

@ -1,19 +1,7 @@
<td> <td>
<div> <div><%- i18n('settings', 'default-site') %></div>
<% if (id === 'default-site') { %>
<%- i18n('settings', 'default-site') %>
<% } %>
<% if (id === 'oidc-config') { %>
<%- i18n('settings', 'oidc-config') %>
<% } %>
</div>
<div class="small text-muted"> <div class="small text-muted">
<% if (id === 'default-site') { %> <%- i18n('settings', 'default-site-description') %>
<%- i18n('settings', 'default-site-description') %>
<% } %>
<% if (id === 'oidc-config') { %>
<%- i18n('settings', 'oidc-config-description') %>
<% } %>
</div> </div>
</td> </td>
<td> <td>
@ -21,14 +9,6 @@
<% if (id === 'default-site') { %> <% if (id === 'default-site') { %>
<%- i18n('settings', 'default-site-' + value) %> <%- i18n('settings', 'default-site-' + value) %>
<% } %> <% } %>
<% if (id === 'oidc-config' && meta && meta.name && meta.clientID && meta.clientSecret && meta.issuerURL && meta.redirectURL) { %>
<%- meta.name %>
<% if (!meta.enabled) { %>
(<%- i18n('str', 'disabled') %>)
<% } %>
<% } else if (id === 'oidc-config') { %>
<%- i18n('settings', 'oidc-not-configured') %>
<% } %>
</div> </div>
</td> </td>
<td class="text-right"> <td class="text-right">

View File

@ -1,56 +0,0 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%- i18n('settings', id) %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div>
<div class="modal-body">
<form>
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-group">
<div class="form-label"><%- description %></div>
<div>
<p><%- i18n('settings', 'oidc-config-hint-1') %></p>
<p><%- i18n('settings', 'oidc-config-hint-2') %></p>
</div>
<div class="custom-controls-stacked">
<div class="form-group">
<label class="form-label"><%- i18n('str', 'name') %> <span class="form-required">*</span>
<input class="form-control name-input" name="meta[name]" required type="text" value="<%- meta && typeof meta.name !== 'undefined' ? meta.name : '' %>">
</label>
</div>
<div class="form-group">
<label class="form-label">Client ID <span class="form-required">*</span>
<input class="form-control id-input" name="meta[clientID]" required type="text" value="<%- meta && typeof meta.clientID !== 'undefined' ? meta.clientID : '' %>">
</label>
</div>
<div class="form-group">
<label class="form-label">Client Secret <span class="form-required">*</span>
<input class="form-control secret-input" name="meta[clientSecret]" required type="text" value="<%- meta && typeof meta.clientSecret !== 'undefined' ? meta.clientSecret : '' %>">
</label>
</div>
<div class="form-group">
<label class="form-label">Issuer URL <span class="form-required">*</span>
<input class="form-control issuer-input" name="meta[issuerURL]" required placeholder="https://" type="url" value="<%- meta && typeof meta.issuerURL !== 'undefined' ? meta.issuerURL : '' %>">
</label>
</div>
<div class="form-group">
<label class="form-label">Redirect URL <span class="form-required">*</span>
<input class="form-control redirect-url-input" name="meta[redirectURL]" required placeholder="https://" type="url" value="<%- meta && typeof meta.redirectURL !== 'undefined' ? meta.redirectURL : document.location.origin + '/api/oidc/callback' %>">
</label>
</div>
<div class="form-group">
<div class="form-label"><%- i18n('str', 'enable') %></div>
<input class="form-check enabled-input" name="meta[enabled]" placeholder="" type="checkbox" <%- meta && (typeof meta.enabled !== 'undefined' && meta.enabled === true) || (JSON.stringify(meta) === '{}') ? 'checked="checked"' : '' %> >
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
<button type="button" class="btn btn-teal save"><%- i18n('str', 'save') %></button>
</div>
</div>

View File

@ -1,46 +0,0 @@
const Mn = require('backbone.marionette');
const App = require('../../main');
const template = require('./main.ejs');
require('jquery-serializejson');
module.exports = Mn.View.extend({
template: template,
className: 'modal-dialog wide',
ui: {
form: 'form',
buttons: '.modal-footer button',
cancel: 'button.cancel',
save: 'button.save',
},
events: {
'click @ui.save': function (e) {
e.preventDefault();
if (!this.ui.form[0].checkValidity()) {
$('<input type="submit">').hide().appendTo(this.ui.form).click().remove();
return;
}
let view = this;
let data = this.ui.form.serializeJSON();
data.id = this.model.get('id');
if (data.meta.enabled) {
data.meta.enabled = data.meta.enabled === 'on' || data.meta.enabled === 'true';
}
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
App.Api.Settings.update(data)
.then((result) => {
view.model.set(result);
App.UI.closeModal();
})
.catch((err) => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
}
});

View File

@ -1,10 +1,10 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <form>
<h5 class="modal-title"><%- i18n('users', 'form-title', {id: id}) %></h5> <div class="modal-header">
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button> <h5 class="modal-title"><%- i18n('users', 'form-title', {id: id}) %></h5>
</div> <button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
<div class="modal-body"> </div>
<form> <div class="modal-body">
<div class="row"> <div class="row">
<div class="col-sm-6 col-md-6"> <div class="col-sm-6 col-md-6">
<div class="form-group"> <div class="form-group">
@ -49,10 +49,10 @@
</div> </div>
<% } %> <% } %>
</div> </div>
</form> </div>
</div> <div class="modal-footer">
<div class="modal-footer"> <button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button> <button type="submit" class="btn btn-teal save"><%- i18n('str', 'save') %></button>
<button type="button" class="btn btn-teal save"><%- i18n('str', 'save') %></button> </div>
</div> </form>
</div> </div>

View File

@ -19,7 +19,7 @@ module.exports = Mn.View.extend({
events: { events: {
'click @ui.save': function (e) { 'submit @ui.form': function (e) {
e.preventDefault(); e.preventDefault();
this.ui.error.hide(); this.ui.error.hide();
let view = this; let view = this;

View File

@ -5,7 +5,6 @@
"username": "Username", "username": "Username",
"password": "Password", "password": "Password",
"sign-in": "Sign in", "sign-in": "Sign in",
"sign-in-with": "Sign in with",
"sign-out": "Sign out", "sign-out": "Sign out",
"try-again": "Try again", "try-again": "Try again",
"name": "Name", "name": "Name",
@ -291,12 +290,7 @@
"default-site-404": "404 Page", "default-site-404": "404 Page",
"default-site-444": "No Response (444)", "default-site-444": "No Response (444)",
"default-site-html": "Custom Page", "default-site-html": "Custom Page",
"default-site-redirect": "Redirect", "default-site-redirect": "Redirect"
"oidc-config": "Open ID Conncect Configuration",
"oidc-config-description": "Sign in to Nginx Proxy Manager with an external Identity Provider",
"oidc-not-configured": "Not configured",
"oidc-config-hint-1": "Provide configuration for an IdP that supports Open ID Connect Discovery.",
"oidc-config-hint-2": "The 'RedirectURL' must be set to '[base URL]/api/oidc/callback', the IdP must send the 'email' claim and a user with matching email address must exist in Nginx Proxy Manager."
} }
} }
} }

View File

@ -5,7 +5,7 @@
<div class="card-body p-6"> <div class="card-body p-6">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-6 margin-auto"> <div class="col-sm-12 col-md-6">
<div class="text-center p-6"> <div class="text-center p-6">
<img src="/images/logo-text-vertical-grey.png" alt="Logo" /> <img src="/images/logo-text-vertical-grey.png" alt="Logo" />
<div class="text-center text-muted mt-5"> <div class="text-center text-muted mt-5">
@ -27,13 +27,6 @@
<div class="form-footer"> <div class="form-footer">
<button type="submit" class="btn btn-teal btn-block"><%- i18n('str', 'sign-in') %></button> <button type="submit" class="btn btn-teal btn-block"><%- i18n('str', 'sign-in') %></button>
</div> </div>
<div class="form-footer login-oidc">
<div class="separator"><slot>OR</slot></div>
<button type="button" id="login-oidc" class="btn btn-teal btn-block">
<%- i18n('str', 'sign-in-with') %> <span class="oidc-provider"></span>
</button>
<div class="invalid-feedback oidc-error"></div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,22 +3,17 @@ const Mn = require('backbone.marionette');
const template = require('./login.ejs'); const template = require('./login.ejs');
const Api = require('../../app/api'); const Api = require('../../app/api');
const i18n = require('../../app/i18n'); const i18n = require('../../app/i18n');
const Tokens = require('../../app/tokens');
module.exports = Mn.View.extend({ module.exports = Mn.View.extend({
template: template, template: template,
className: 'page-single', className: 'page-single',
ui: { ui: {
form: 'form', form: 'form',
identity: 'input[name="identity"]', identity: 'input[name="identity"]',
secret: 'input[name="secret"]', secret: 'input[name="secret"]',
error: '.secret-error', error: '.secret-error',
button: 'button[type=submit]', button: 'button'
oidcLogin: 'div.login-oidc',
oidcButton: 'button#login-oidc',
oidcError: '.oidc-error',
oidcProvider: 'span.oidc-provider'
}, },
events: { events: {
@ -31,56 +26,10 @@ module.exports = Mn.View.extend({
.then(() => { .then(() => {
window.location = '/'; window.location = '/';
}) })
.catch((err) => { .catch(err => {
this.ui.error.text(err.message).show(); this.ui.error.text(err.message).show();
this.ui.button.removeClass('btn-loading').prop('disabled', false); this.ui.button.removeClass('btn-loading').prop('disabled', false);
}); });
},
'click @ui.oidcButton': function() {
this.ui.identity.prop('disabled', true);
this.ui.secret.prop('disabled', true);
this.ui.button.prop('disabled', true);
this.ui.oidcButton.addClass('btn-loading').prop('disabled', true);
// redirect to initiate oauth flow
document.location.replace('/api/oidc/');
},
},
async onRender() {
// read oauth callback state cookies
let cookies = document.cookie.split(';'),
token, expiry, error;
for (cookie of cookies) {
let raw = cookie.split('='),
name = raw[0].trim(),
value = raw[1];
if (name === 'npm_oidc') {
let v = value.split('---');
token = v[0];
expiry = v[1];
}
if (name === 'npm_oidc_error') {
error = decodeURIComponent(value);
}
}
// register a newly acquired jwt token following successful oidc authentication
if (token && expiry && (new Date(Date.parse(decodeURIComponent(expiry)))) > new Date() ) {
Tokens.addToken(token);
document.location.replace('/');
}
// show error message following a failed oidc authentication
if (error) {
this.ui.oidcError.html(error);
}
// fetch oidc configuration and show alternative action button if enabled
let response = await Api.Settings.getById('oidc-config');
if (response && response.meta && response.meta.enabled === true) {
this.ui.oidcProvider.html(response.meta.name);
this.ui.oidcLogin.show();
this.ui.oidcError.show();
} }
}, },

View File

@ -39,34 +39,4 @@ a:hover {
.col-login { .col-login {
max-width: 48rem; max-width: 48rem;
}
.margin-auto {
margin: auto;
}
.separator {
display: flex;
align-items: center;
text-align: center;
margin-bottom: 1em;
}
.separator::before, .separator::after {
content: "";
flex: 1 1 0%;
border-bottom: 1px solid #ccc;
}
.separator:not(:empty)::before {
margin-right: 0.5em;
}
.separator:not(:empty)::after {
margin-left: 0.5em;
}
.login-oidc {
display: none;
margin-top: 1em;
} }

View File

@ -2648,9 +2648,9 @@ electron-to-chromium@^1.3.47:
integrity sha512-67V62Z4CFOiAtox+o+tosGfVk0QX4DJgH609tjT8QymbJZVAI/jWnAthnr8c5hnRNziIRwkc9EMQYejiVz3/9Q== integrity sha512-67V62Z4CFOiAtox+o+tosGfVk0QX4DJgH609tjT8QymbJZVAI/jWnAthnr8c5hnRNziIRwkc9EMQYejiVz3/9Q==
elliptic@^6.5.3, elliptic@^6.5.4: elliptic@^6.5.3, elliptic@^6.5.4:
version "6.5.7" version "6.6.0"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.0.tgz#5919ec723286c1edf28685aa89261d4761afa210"
integrity sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q== integrity sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==
dependencies: dependencies:
bn.js "^4.11.9" bn.js "^4.11.9"
brorand "^1.1.0" brorand "^1.1.0"

View File

@ -7,7 +7,7 @@
"credentials": "dns_acmedns_api_url = http://acmedns-server/\ndns_acmedns_registration_file = /data/acme-registration.json", "credentials": "dns_acmedns_api_url = http://acmedns-server/\ndns_acmedns_registration_file = /data/acme-registration.json",
"full_plugin_name": "dns-acmedns" "full_plugin_name": "dns-acmedns"
}, },
"active24":{ "active24": {
"name": "Active24", "name": "Active24",
"package_name": "certbot-dns-active24", "package_name": "certbot-dns-active24",
"version": "~=1.5.1", "version": "~=1.5.1",
@ -18,7 +18,7 @@
"aliyun": { "aliyun": {
"name": "Aliyun", "name": "Aliyun",
"package_name": "certbot-dns-aliyun", "package_name": "certbot-dns-aliyun",
"version": "~=0.38.1", "version": "~=2.0.0",
"dependencies": "", "dependencies": "",
"credentials": "dns_aliyun_access_key = 12345678\ndns_aliyun_access_key_secret = 1234567890abcdef1234567890abcdef", "credentials": "dns_aliyun_access_key = 12345678\ndns_aliyun_access_key_secret = 1234567890abcdef1234567890abcdef",
"full_plugin_name": "dns-aliyun" "full_plugin_name": "dns-aliyun"
@ -31,6 +31,14 @@
"credentials": "# This plugin supported API authentication using either Service Principals or utilizing a Managed Identity assigned to the virtual machine.\n# Regardless which authentication method used, the identity will need the “DNS Zone Contributor” role assigned to it.\n# As multiple Azure DNS Zones in multiple resource groups can exist, the config file needs a mapping of zone to resource group ID. Multiple zones -> ID mappings can be listed by using the key dns_azure_zoneX where X is a unique number. At least 1 zone mapping is required.\n\n# Using a service principal (option 1)\ndns_azure_sp_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\ndns_azure_sp_client_secret = E-xqXU83Y-jzTI6xe9fs2YC~mck3ZzUih9\ndns_azure_tenant_id = ed1090f3-ab18-4b12-816c-599af8a88cf7\n\n# Using used assigned MSI (option 2)\n# dns_azure_msi_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\n\n# Using system assigned MSI (option 3)\n# dns_azure_msi_system_assigned = true\n\n# Zones (at least one always required)\ndns_azure_zone1 = example.com:/subscriptions/c135abce-d87d-48df-936c-15596c6968a5/resourceGroups/dns1\ndns_azure_zone2 = example.org:/subscriptions/99800903-fb14-4992-9aff-12eaf2744622/resourceGroups/dns2", "credentials": "# This plugin supported API authentication using either Service Principals or utilizing a Managed Identity assigned to the virtual machine.\n# Regardless which authentication method used, the identity will need the “DNS Zone Contributor” role assigned to it.\n# As multiple Azure DNS Zones in multiple resource groups can exist, the config file needs a mapping of zone to resource group ID. Multiple zones -> ID mappings can be listed by using the key dns_azure_zoneX where X is a unique number. At least 1 zone mapping is required.\n\n# Using a service principal (option 1)\ndns_azure_sp_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\ndns_azure_sp_client_secret = E-xqXU83Y-jzTI6xe9fs2YC~mck3ZzUih9\ndns_azure_tenant_id = ed1090f3-ab18-4b12-816c-599af8a88cf7\n\n# Using used assigned MSI (option 2)\n# dns_azure_msi_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\n\n# Using system assigned MSI (option 3)\n# dns_azure_msi_system_assigned = true\n\n# Zones (at least one always required)\ndns_azure_zone1 = example.com:/subscriptions/c135abce-d87d-48df-936c-15596c6968a5/resourceGroups/dns1\ndns_azure_zone2 = example.org:/subscriptions/99800903-fb14-4992-9aff-12eaf2744622/resourceGroups/dns2",
"full_plugin_name": "dns-azure" "full_plugin_name": "dns-azure"
}, },
"beget": {
"name":"Beget",
"package_name": "certbot-beget-plugin",
"version": "~=1.0.0.dev9",
"dependencies": "",
"credentials": "# Beget API credentials used by Certbot\nbeget_plugin_username = username\nbeget_plugin_password = password",
"full_plugin_name": "beget-plugin"
},
"bunny": { "bunny": {
"name": "bunny.net", "name": "bunny.net",
"package_name": "certbot-dns-bunny", "package_name": "certbot-dns-bunny",
@ -247,6 +255,14 @@
"credentials": "dns_hetzner_api_token = 0123456789abcdef0123456789abcdef", "credentials": "dns_hetzner_api_token = 0123456789abcdef0123456789abcdef",
"full_plugin_name": "dns-hetzner" "full_plugin_name": "dns-hetzner"
}, },
"hostingnl": {
"name": "Hosting.nl",
"package_name": "certbot-dns-hostingnl",
"version": "~=0.1.5",
"dependencies": "",
"credentials": "dns_hostingnl_api_key = 0123456789abcdef0123456789abcdef",
"full_plugin_name": "dns-hostingnl"
},
"hover": { "hover": {
"name": "Hover", "name": "Hover",
"package_name": "certbot-dns-hover", "package_name": "certbot-dns-hover",
@ -402,7 +418,7 @@
"porkbun": { "porkbun": {
"name": "Porkbun", "name": "Porkbun",
"package_name": "certbot-dns-porkbun", "package_name": "certbot-dns-porkbun",
"version": "~=0.2", "version": "~=0.9",
"dependencies": "", "dependencies": "",
"credentials": "dns_porkbun_key=your-porkbun-api-key\ndns_porkbun_secret=your-porkbun-api-secret", "credentials": "dns_porkbun_key=your-porkbun-api-key\ndns_porkbun_secret=your-porkbun-api-secret",
"full_plugin_name": "dns-porkbun" "full_plugin_name": "dns-porkbun"
@ -495,7 +511,7 @@
"credentials": "dns_websupport_identifier = <api_key>\ndns_websupport_secret_key = <secret>", "credentials": "dns_websupport_identifier = <api_key>\ndns_websupport_secret_key = <secret>",
"full_plugin_name": "dns-websupport" "full_plugin_name": "dns-websupport"
}, },
"wedos":{ "wedos": {
"name": "Wedos", "name": "Wedos",
"package_name": "certbot-dns-wedos", "package_name": "certbot-dns-wedos",
"version": "~=2.2", "version": "~=2.2",
@ -511,4 +527,4 @@
"credentials": "edgedns_client_secret = as3d1asd5d1a32sdfsdfs2d1asd5=\nedgedns_host = sdflskjdf-dfsdfsdf-sdfsdfsdf.luna.akamaiapis.net\nedgedns_access_token = kjdsi3-34rfsdfsdf-234234fsdfsdf\nedgedns_client_token = dkfjdf-342fsdfsd-23fsdfsdfsdf", "credentials": "edgedns_client_secret = as3d1asd5d1a32sdfsdfs2d1asd5=\nedgedns_host = sdflskjdf-dfsdfsdf-sdfsdfsdf.luna.akamaiapis.net\nedgedns_access_token = kjdsi3-34rfsdfsdf-234234fsdfsdf\nedgedns_client_token = dkfjdf-342fsdfsd-23fsdfsdfsdf",
"full_plugin_name": "edgedns" "full_plugin_name": "edgedns"
} }
} }

View File

@ -11,7 +11,7 @@ YELLOW='\E[1;33m'
export BLUE CYAN GREEN RED RESET YELLOW export BLUE CYAN GREEN RED RESET YELLOW
# Docker Compose # Docker Compose
COMPOSE_PROJECT_NAME="npmdev" COMPOSE_PROJECT_NAME="npm2dev"
COMPOSE_FILE="docker/docker-compose.dev.yml" COMPOSE_FILE="docker/docker-compose.dev.yml"
export COMPOSE_FILE COMPOSE_PROJECT_NAME export COMPOSE_FILE COMPOSE_PROJECT_NAME

View File

@ -67,6 +67,8 @@ printf "nameserver %s\noptions ndots:0" "${DNSROUTER_IP}" > "${LOCAL_RESOLVE}"
# bring up all remaining containers, except cypress! # bring up all remaining containers, except cypress!
docker-compose up -d --remove-orphans stepca squid docker-compose up -d --remove-orphans stepca squid
docker-compose pull db-mysql || true # ok to fail docker-compose pull db-mysql || true # ok to fail
docker-compose pull db-postgres || true # ok to fail
docker-compose pull authentik authentik-redis authentik-ldap || true # ok to fail
docker-compose up -d --remove-orphans --pull=never fullstack docker-compose up -d --remove-orphans --pull=never fullstack
# wait for main container to be healthy # wait for main container to be healthy

View File

@ -36,12 +36,11 @@ if hash docker-compose 2>/dev/null; then
# bring up all remaining containers, except cypress! # bring up all remaining containers, except cypress!
docker-compose up -d --remove-orphans stepca squid docker-compose up -d --remove-orphans stepca squid
docker-compose pull db docker-compose pull db db-postgres authentik-redis authentik authentik-worker authentik-ldap
docker-compose up -d --remove-orphans --pull=never fullstack docker-compose build --pull --parallel fullstack
docker-compose up -d --remove-orphans fullstack
docker-compose up -d --remove-orphans swagger docker-compose up -d --remove-orphans swagger
# docker-compose up -d --remove-orphans --force-recreate --build
# wait for main container to be healthy # wait for main container to be healthy
bash "$DIR/wait-healthy" "$(docker-compose ps --all -q fullstack)" 120 bash "$DIR/wait-healthy" "$(docker-compose ps --all -q fullstack)" 120
@ -53,10 +52,10 @@ if hash docker-compose 2>/dev/null; then
if [ "$1" == "-f" ]; then if [ "$1" == "-f" ]; then
echo -e "${BLUE} ${YELLOW}Following Backend Container:${RESET}" echo -e "${BLUE} ${YELLOW}Following Backend Container:${RESET}"
docker logs -f npm_core docker logs -f npm2dev.core
else else
echo -e "${YELLOW}Hint:${RESET} You can follow the output of some of the containers with:" echo -e "${YELLOW}Hint:${RESET} You can follow the output of some of the containers with:"
echo " docker logs -f npm_core" echo " docker logs -f npm2dev.core"
fi fi
else else
echo -e "${RED} docker-compose command is not available${RESET}" echo -e "${RED} docker-compose command is not available${RESET}"

View File

@ -0,0 +1,64 @@
/// <reference types="cypress" />
describe('LDAP with Authentik', () => {
let token;
if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') {
before(() => {
cy.getToken().then((tok) => {
token = tok;
// cy.task('backendApiPut', {
// token: token,
// path: '/api/settings/ldap-auth',
// data: {
// value: {
// host: 'authentik-ldap:3389',
// base_dn: 'ou=users,DC=ldap,DC=goauthentik,DC=io',
// user_dn: 'cn={{USERNAME}},ou=users,DC=ldap,DC=goauthentik,DC=io',
// email_property: 'mail',
// name_property: 'sn',
// self_filter: '(&(cn={{USERNAME}})(ak-active=TRUE))',
// auto_create_user: true
// }
// }
// }).then((data) => {
// cy.validateSwaggerSchema('put', 200, '/settings/{name}', data);
// expect(data.result).to.have.property('id');
// expect(data.result.id).to.be.greaterThan(0);
// });
// cy.task('backendApiPut', {
// token: token,
// path: '/api/settings/auth-methods',
// data: {
// value: [
// 'local',
// 'ldap'
// ]
// }
// }).then((data) => {
// cy.validateSwaggerSchema('put', 200, '/settings/{name}', data);
// expect(data.result).to.have.property('id');
// expect(data.result.id).to.be.greaterThan(0);
// });
});
});
it.skip('Should log in with LDAP', function() {
// cy.task('backendApiPost', {
// token: token,
// path: '/api/auth',
// data: {
// // Authentik LDAP creds:
// type: 'ldap',
// identity: 'cypress',
// secret: 'fqXBfUYqHvYqiwBHWW7f'
// }
// }).then((data) => {
// cy.validateSwaggerSchema('post', 200, '/auth', data);
// expect(data.result).to.have.property('token');
// });
});
}
});

View File

@ -0,0 +1,97 @@
/// <reference types="cypress" />
describe('OAuth with Authentik', () => {
let token;
if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') {
before(() => {
cy.getToken().then((tok) => {
token = tok;
// cy.task('backendApiPut', {
// token: token,
// path: '/api/settings/oauth-auth',
// data: {
// value: {
// client_id: '7iO2AvuUp9JxiSVkCcjiIbQn4mHmUMBj7yU8EjqU',
// client_secret: 'VUMZzaGTrmXJ8PLksyqzyZ6lrtz04VvejFhPMBP9hGZNCMrn2LLBanySs4ta7XGrDr05xexPyZT1XThaf4ubg00WqvHRVvlu4Naa1aMootNmSRx3VAk6RSslUJmGyHzq',
// authorization_url: 'http://authentik:9000/application/o/authorize/',
// resource_url: 'http://authentik:9000/application/o/userinfo/',
// token_url: 'http://authentik:9000/application/o/token/',
// logout_url: 'http://authentik:9000/application/o/npm/end-session/',
// identifier: 'preferred_username',
// scopes: [],
// auto_create_user: true
// }
// }
// }).then((data) => {
// cy.validateSwaggerSchema('put', 200, '/settings/{name}', data);
// expect(data.result).to.have.property('id');
// expect(data.result.id).to.be.greaterThan(0);
// });
// cy.task('backendApiPut', {
// token: token,
// path: '/api/settings/auth-methods',
// data: {
// value: [
// 'local',
// 'oauth'
// ]
// }
// }).then((data) => {
// cy.validateSwaggerSchema('put', 200, '/settings/{name}', data);
// expect(data.result).to.have.property('id');
// expect(data.result.id).to.be.greaterThan(0);
// });
});
});
it.skip('Should log in with OAuth', function() {
// cy.task('backendApiGet', {
// path: '/oauth/login?redirect_base=' + encodeURI(Cypress.config('baseUrl')),
// }).then((data) => {
// expect(data).to.have.property('result');
// cy.origin('http://authentik:9000', {args: data.result}, (url) => {
// cy.visit(url);
// cy.get('ak-flow-executor')
// .shadow()
// .find('ak-stage-identification')
// .shadow()
// .find('input[name="uidField"]', { visible: true })
// .type('cypress');
// cy.get('ak-flow-executor')
// .shadow()
// .find('ak-stage-identification')
// .shadow()
// .find('button[type="submit"]', { visible: true })
// .click();
// cy.get('ak-flow-executor')
// .shadow()
// .find('ak-stage-password')
// .shadow()
// .find('input[name="password"]', { visible: true })
// .type('fqXBfUYqHvYqiwBHWW7f');
// cy.get('ak-flow-executor')
// .shadow()
// .find('ak-stage-password')
// .shadow()
// .find('button[type="submit"]', { visible: true })
// .click();
// })
// // we should be logged in
// cy.get('#root p.chakra-text')
// .first()
// .should('have.text', 'Nginx Proxy Manager');
// // logout:
// cy.clearLocalStorage();
// });
});
}
});

View File

@ -132,9 +132,9 @@
integrity sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ== integrity sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==
"@eslint/plugin-kit@^0.2.0": "@eslint/plugin-kit@^0.2.0":
version "0.2.0" version "0.2.3"
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz#8712dccae365d24e9eeecb7b346f85e750ba343d" resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz#812980a6a41ecf3a8341719f92a6d1e784a2e0e8"
integrity sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig== integrity sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==
dependencies: dependencies:
levn "^0.4.1" levn "^0.4.1"
@ -628,9 +628,9 @@ core-util-is@1.0.2:
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
cross-spawn@^7.0.0, cross-spawn@^7.0.2: cross-spawn@^7.0.0, cross-spawn@^7.0.2:
version "7.0.3" version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies: dependencies:
path-key "^3.1.0" path-key "^3.1.0"
shebang-command "^2.0.0" shebang-command "^2.0.0"