Compare commits

...

111 Commits

Author SHA1 Message Date
dependabot[bot]
2650648d68 Bump js-yaml from 4.1.0 to 4.1.1 in /backend
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-15 10:40:46 +00:00
jc21
a4d54a0291 Merge pull request #4932 from kraineff/develop
All checks were successful
Close stale issues and PRs / stale (push) Successful in 20s
Update Russian locale
2025-11-14 16:58:05 +10:00
jc21
2c630bbdca Merge branch 'develop' into develop 2025-11-14 15:25:10 +10: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
Alexey Krainev
9b2d60e67b Update Russian locale 2025-11-13 16:58:04 +05:00
Jamie Curnow
79a9653b26 Remove the compiled lang files, compile on dev server and when building in ci
All checks were successful
Close stale issues and PRs / stale (push) Successful in 23s
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
jc21
8838dabe8a Merge pull request #4906 from sopex/develop
Available upgrade notification
2025-11-13 10:15:33 +10:00
Konstantinos Spartalis
b4fd242eb7 remove 1 2025-11-13 00:48:49 +02:00
7heMech
963125f963 Space scandal retified (hopefully) 2025-11-13 00:45:07 +02:00
jc21
6ce9567e48 Merge pull request #4816 from fhennig42/azure-dns
All checks were successful
Close stale issues and PRs / stale (push) Successful in 22s
Bump certbot-azure-dns version
2025-11-13 07:13:11 +10:00
Konstantinos Spartalis
15394c6532 trigger Jenkins that failed due to internet connection problems 2025-11-12 15:50:11 +02:00
Konstantinos Spartalis
2d6252d75d https.get 2025-11-12 15:45:59 +02:00
jc21
adee0e39de Merge branch 'master' into develop 2025-11-12 23:02:28 +10:00
Jamie Curnow
5dde98cf3e Updates to polish locale after running through automated scripts 2025-11-12 23:01:40 +10:00
jc21
c41451618e Merge pull request #4924 from zdzichu6969/develop
Add Polish locale
2025-11-12 22:59:23 +10:00
jc21
1a3d45f6bc Merge branch 'develop' into develop 2025-11-12 22:14:28 +10:00
jc21
2ea54975b6 Merge pull request #4922 from NginxProxyManager/dodog-slovak
Add Slovak language by @dodog in #4911
2025-11-12 22:13:05 +10:00
Mateusz Gruszczyński
0373017a9f Add Polish locale 2025-11-12 13:10:29 +01:00
Florian Hennig
b043e70fc0 add azure-mgmt-dns fix version as dependency 2025-11-12 13:00:34 +01:00
Jamie Curnow
2b5182d339 Add Slovak language by @dodog in #4911 2025-11-12 21:49:04 +10:00
jc21
3c5ff81a54 Merge pull request #4910 from 7heMech/develop
Add scheme back in destination
2025-11-12 20:48:56 +10:00
jc21
8aa46c1f40 Merge pull request #4921 from NginxProxyManager/Firfr-chinese
Add Chinese language 添加中文
2025-11-12 20:47:15 +10:00
Jamie Curnow
b26db50ae7 Adds cn to check locales script 2025-11-12 20:26:22 +10:00
firfe
d66bb2104a Add the new translation for "redirection-host.forward-http-code". 2025-11-12 20:23:36 +10:00
firfe
8e900dbc92 Add Chinese HelpDoc 2025-11-12 20:23:34 +10:00
firfe
66aac3eb3e Add Chinese 中文 2025-11-12 20:22:57 +10:00
jc21
221c3eddbc Merge pull request #4919 from lastsamurai26/develop
Fix: German grammatical change
2025-11-12 20:16:58 +10:00
Jamie Curnow
8460b28597 Bump version 2025-11-12 20:13:18 +10:00
Frank
0344bb3c19 fix: Grammatical change
fix: Grammatical change
2025-11-12 10:47:53 +01:00
Frank
1a36bdce76 fix: Grammatical change
fix: Grammatical change
2025-11-12 10:47:51 +01:00
Jamie Curnow
06d7db43f7 Fix Russion locale, compiled file was comitted without a source file 2025-11-12 18:59:37 +10:00
jc21
4557244744 Merge pull request #4870 from kraineff/develop
Add Russian Support
2025-11-12 18:51:43 +10:00
jc21
f649288098 Merge branch 'develop' into develop 2025-11-12 18:39:05 +10:00
jc21
28df6db52b Merge pull request #4848 from Oka-Tak/develop
Add Japanese language support and translations
2025-11-12 18:36:18 +10:00
jc21
eee749652c Merge pull request #4917 from lastsamurai26/develop
Fix: wrong translate and adding missing translations
2025-11-12 18:13:08 +10:00
jc21
f6aa25b9b3 Merge branch 'develop' into develop 2025-11-12 18:12:10 +10:00
Frank
40db26b686 Merge branch 'NginxProxyManager:develop' into develop 2025-11-12 08:06:36 +01:00
Frank
f36d4e6906 Fix: CustomCertificateModal Wrong displayname
Fix: https://github.com/NginxProxyManager/nginx-proxy-manager/issues/4912 Wrong Locale for Custom
2025-11-12 07:47:06 +01:00
Frank
86c7cbddab Add certificate renewal message in German locale
Fix: add missing translation for renew certificates
2025-11-12 07:34:44 +01:00
Frank
e52975bf6c Translate 'Renew Certificate' to German
Fix: add missing translation for renew certificates
2025-11-12 07:34:42 +01:00
Frank
ff792f76af Add translation for 'Renew Certificate' in de.json
Fix: Add missing translation für renew Certificate
2025-11-12 07:32:34 +01:00
Jamie Curnow
711f312b71 Fix up language inconsistenties 2025-11-12 16:30:22 +10:00
Jamie Curnow
9f0f89ff03 Fix wrong translation for EN 2025-11-12 15:13:14 +10:00
jc21
f3633cb696 Merge pull request #4850 from TeenBiscuits/lang-spanish
Add Spanish language support and translations
2025-11-12 15:12:28 +10:00
Pablo Portas López
8773ce25d7 Merge branch 'develop' into lang-spanish 2025-11-12 02:14:09 +01:00
jc21
c3954e9845 Merge pull request #4824 from lastsamurai26/develop
Add German Support
2025-11-12 08:52:07 +10:00
Konstantinos Spartalis
87eef10ff8 remove useCallback logic 2025-11-11 18:30:23 +02:00
Konstantinos Spartalis
dc03ad8239 minimal changes 2025-11-11 17:42:46 +02:00
7heMech
441a7262cd Add scheme back in destination 2025-11-11 12:54:01 +00:00
Pablo Portas López
1600599410 Fix column.http-code translation 2025-11-11 13:53:45 +01:00
Pablo Portas López
74d381e7fa Add missing spanish translation 2025-11-11 13:50:23 +01:00
Konstantinos Spartalis
ae5faa75fa backend test 2025-11-11 10:35:00 +02:00
Frank
ba79bbc750 Update German translation for HTTP code
fix: Updated column http code
2025-11-11 08:56:32 +01:00
Frank
a7231777aa FIX: Update HTTP code message in German locale
fix: Updated column http code
2025-11-11 08:56:20 +01:00
jc21
2578105f86 Merge pull request #4907 from NginxProxyManager/develop
v2.13.3
2025-11-11 16:54:38 +10:00
Frank
3a6b221b0c Add HTTP Code translation to German locale
new: redirection-host.forward-http-code added
2025-11-11 07:13:13 +01:00
Frank
12b000abb9 Add HTTP Code message to German locale
new: redirection-host.forward-http-code added
2025-11-11 07:12:57 +01:00
jc21
39c9bbb167 Merge branch 'master' into develop
All checks were successful
Close stale issues and PRs / stale (push) Successful in 18s
2025-11-11 16:06:05 +10:00
jc21
30c2781a02 Merge pull request #4765 from mamasch19/develop
add MC-HOST24 DNS plugin
2025-11-11 16:05:32 +10:00
Jamie Curnow
53e78dcc17 Bump version 2025-11-11 16:01:06 +10:00
jc21
62092b2ddc Merge pull request #4859 from 7heMech/develop
Fix hamburger menu on mobile
2025-11-11 15:37:12 +10:00
Jamie Curnow
2c26ed8b11 Revert "Fix #4831 mobile header menu not working"
This reverts commit 4bd545c88e.
2025-11-11 15:36:46 +10:00
jc21
e3f5cd9a58 Merge pull request #4871 from prospo/develop
chore: Bump certbot-dns-leaseweb to 1.0.3
2025-11-11 15:24:11 +10:00
jc21
fba14817e7 Merge pull request #4894 from eduardpaul/feat-fix-pass_auth-template
Update _access.conf to fix access_list.pass_auth logic
2025-11-11 15:23:22 +10:00
Jamie Curnow
6825a9773b Fix #4854 Added missing forward http code for redirections 2025-11-11 15:17:43 +10:00
Jamie Curnow
8bc3078d87 Fix initial setup user bug, taking the fix from #4836 2025-11-11 14:52:39 +10:00
Jamie Curnow
8aeb2fa661 Fix #4692, #4856 - stick with auto for scheme in db, change it to $scheme when rendering 2025-11-11 14:46:25 +10:00
Jamie Curnow
4bd545c88e Fix #4831 mobile header menu not working 2025-11-11 14:05:26 +10:00
Jamie Curnow
7f0cce944d Relax the email validation in frontend 2025-11-11 08:54:48 +10:00
Pablo Portas López
7cde6ee7ca Add Spanish Test 2025-11-10 21:58:23 +01:00
Pablo Portas López
df1b414c2e Delete Spanish Test 2025-11-10 21:58:01 +01:00
Konstantinos Spartalis
b6dbb68ef3 Update SiteFooter.tsx 2025-11-10 20:42:52 +02:00
Konstantinos Spartalis
b434bba12f remove hardcoded version number 2025-11-10 20:37:25 +02:00
Konstantinos Spartalis
f1d7203212 v2 2025-11-10 19:57:55 +02:00
Konstantinos Spartalis
990ba28831 Update SiteFooter.tsx 2025-11-10 19:43:38 +02:00
Jamie Curnow
311d6a1541 Tweaks to CI stack for postgres
All checks were successful
Close stale issues and PRs / stale (push) Successful in 20s
2025-11-10 10:30:16 +10:00
mamasch19
5e7276e65b Add MC-HOST24 DNS plugin configuration
added the MC-HOST24 configuration to the new plugin file
2025-11-09 22:31:48 +01:00
Eduard Paul
2bcb942f93 Update _access.conf to ensure Authorization header remove when pass_auth = false or 0
Fixing prev commit as it's negative logic.
2025-11-09 21:02:18 +01:00
Eduard Paul
b3dac3df08 Update _access.conf to fix access_list.pass_auth logic
Wrong logic to pass auth as header: when disabled (pass_auth=0) credentials are included in Authorization header. However as soon as you enable (pass_auth=1) they are not.
2025-11-09 20:11:33 +01:00
jc21
64c5a863f8 Merge pull request #4878 from NginxProxyManager/develop
v2.13.2
2025-11-09 21:16:26 +10:00
Jamie Curnow
cd94863850 Bump version
All checks were successful
Close stale issues and PRs / stale (push) Successful in 25s
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
All checks were successful
Close stale issues and PRs / stale (push) Successful in 23s
2025-11-07 21:37:22 +10:00
Jamie Curnow
8eba31913f Remove pebble certs, they removed the dockerhub image that had armv7 support.
The ghcr image doesn't have it, so it was causing builds to fail.
2025-11-07 11:18:53 +10:00
Jamie Curnow
e4e3415120 Safer handling of backend date formats
and add frontend testing
2025-11-07 11:15:15 +10:00
Jamie Curnow
a03bb7ebce Remove Jenkinsfile, managed in other repo now 2025-11-07 10:54:21 +10:00
Jamie Curnow
51e25d1a40 Attempt to fix race condition with database instantiation 2025-11-07 09:46:00 +10:00
Pablo Portas López
123f7d1999 Add Spanish language support and translations 2025-11-06 01:04:02 +01:00
Takahisa-Okawa
9de40f067b Add Japanese language support and translations
Co-authored-by: kz2870 <kz2870@users.noreply.github.com>
2025-11-05 22:25:15 +09:00
Frank
b21d6d9d78 Fix German translations
Fix: German translations
2025-11-05 08:09:10 +01:00
Frank
bf1ad15ed7 Update de.json
fix: typos
2025-11-05 08:08:50 +01:00
Frank
1209303a1d Update DeadHosts.md
fix: translation "Umgangssprachlich"
2025-11-05 08:00:15 +01:00
Frank
cd3a09ebf6 Update Certificates.md
fix: typo
2025-11-05 07:59:45 +01:00
Frank
d0e20d4f1b Update de.json
fix: typo dark and light mode
2025-11-05 07:57:11 +01:00
Frank
ceb098fcfe Fix typo in German locale for min character length
fix: typo mainimale should be minimale
2025-11-05 07:53:56 +01:00
Frank
639ba3a525 Update de.json
fix: typo 
fix: translate Location with Pfad
2025-11-05 07:52:28 +01:00
jc21
e88d55f1d2 Merge pull request #4839 from NginxProxyManager/develop
v2.13.1
2025-11-05 15:40:32 +10:00
Florian Hennig
a85b5f664f Bump version after rebase 2025-11-04 20:03:09 +01:00
Frank
7e28d8a5d6 Add files via upload
add german
2025-11-03 17:51:48 +01:00
Frank
8991e88ff3 Update de.json 2025-11-03 14:22:13 +01:00
Frank
e2a8ffa2d3 Add files via upload
Add German
2025-11-03 14:18:08 +01:00
128 changed files with 5685 additions and 671 deletions

View File

@@ -1 +1 @@
2.13.1 2.13.4

285
Jenkinsfile vendored
View File

@@ -1,285 +0,0 @@
import groovy.transform.Field
@Field
def shOutput = ""
def buildxPushTags = ""
pipeline {
agent {
label 'docker-multiarch'
}
options {
buildDiscarder(logRotator(numToKeepStr: '5'))
disableConcurrentBuilds()
ansiColor('xterm')
}
environment {
IMAGE = 'nginx-proxy-manager'
BUILD_VERSION = getVersion()
MAJOR_VERSION = '2'
BRANCH_LOWER = "${BRANCH_NAME.toLowerCase().replaceAll('\\\\', '-').replaceAll('/', '-').replaceAll('\\.', '-')}"
BUILDX_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}"
COMPOSE_INTERACTIVE_NO_CLI = 1
}
stages {
stage('Environment') {
parallel {
stage('Master') {
when {
branch 'master'
}
steps {
script {
buildxPushTags = "-t docker.io/jc21/${IMAGE}:${BUILD_VERSION} -t docker.io/jc21/${IMAGE}:${MAJOR_VERSION} -t docker.io/jc21/${IMAGE}:latest"
}
}
}
stage('Other') {
when {
not {
branch 'master'
}
}
steps {
script {
// Defaults to the Branch name, which is applies to all branches AND pr's
buildxPushTags = "-t docker.io/nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}"
}
}
}
stage('Versions') {
steps {
sh 'cat frontend/package.json | jq --arg BUILD_VERSION "${BUILD_VERSION}" \'.version = $BUILD_VERSION\' | sponge frontend/package.json'
sh 'echo -e "\\E[1;36mFrontend Version is:\\E[1;33m $(cat frontend/package.json | jq -r .version)\\E[0m"'
sh 'cat backend/package.json | jq --arg BUILD_VERSION "${BUILD_VERSION}" \'.version = $BUILD_VERSION\' | sponge backend/package.json'
sh 'echo -e "\\E[1;36mBackend Version is:\\E[1;33m $(cat backend/package.json | jq -r .version)\\E[0m"'
sh 'sed -i -E "s/(version-)[0-9]+\\.[0-9]+\\.[0-9]+(-green)/\\1${BUILD_VERSION}\\2/" README.md'
}
}
stage('Docker Login') {
steps {
withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh 'docker login -u "${duser}" -p "${dpass}"'
}
}
}
}
}
stage('Builds') {
parallel {
stage('Project') {
steps {
script {
// Frontend and Backend
def shStatusCode = sh(label: 'Checking and Building', returnStatus: true, script: '''
set -e
./scripts/ci/frontend-build > ${WORKSPACE}/tmp-sh-build 2>&1
./scripts/ci/test-and-build > ${WORKSPACE}/tmp-sh-build 2>&1
''')
shOutput = readFile "${env.WORKSPACE}/tmp-sh-build"
if (shStatusCode != 0) {
error "${shOutput}"
}
}
}
post {
always {
sh 'rm -f ${WORKSPACE}/tmp-sh-build'
}
failure {
npmGithubPrComment("CI Error:\n\n```\n${shOutput}\n```", true)
}
}
}
stage('Docs') {
steps {
dir(path: 'docs') {
sh 'yarn install'
sh 'yarn build'
}
}
}
}
}
stage('Test Sqlite') {
environment {
COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_sqlite"
COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.sqlite.yml'
}
when {
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
steps {
sh 'rm -rf ./test/results/junit/*'
sh './scripts/ci/fulltest-cypress'
}
post {
always {
// Dumps to analyze later
sh 'mkdir -p debug/sqlite'
sh 'docker logs $(docker compose ps --all -q fullstack) > debug/sqlite/docker_fullstack.log 2>&1'
sh 'docker logs $(docker compose ps --all -q stepca) > debug/sqlite/docker_stepca.log 2>&1'
sh 'docker logs $(docker compose ps --all -q pdns) > debug/sqlite/docker_pdns.log 2>&1'
sh 'docker logs $(docker compose ps --all -q pdns-db) > debug/sqlite/docker_pdns-db.log 2>&1'
sh 'docker logs $(docker compose ps --all -q dnsrouter) > debug/sqlite/docker_dnsrouter.log 2>&1'
junit 'test/results/junit/*'
sh 'docker compose down --remove-orphans --volumes -t 30 || true'
}
unstable {
dir(path: 'test/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}
}
}
stage('Test Mysql') {
environment {
COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_mysql"
COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.mysql.yml'
}
when {
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
steps {
sh 'rm -rf ./test/results/junit/*'
sh './scripts/ci/fulltest-cypress'
}
post {
always {
// Dumps to analyze later
sh 'mkdir -p debug/mysql'
sh 'docker logs $(docker compose ps --all -q fullstack) > debug/mysql/docker_fullstack.log 2>&1'
sh 'docker logs $(docker compose ps --all -q stepca) > debug/mysql/docker_stepca.log 2>&1'
sh 'docker logs $(docker compose ps --all -q pdns) > debug/mysql/docker_pdns.log 2>&1'
sh 'docker logs $(docker compose ps --all -q pdns-db) > debug/mysql/docker_pdns-db.log 2>&1'
sh 'docker logs $(docker compose ps --all -q dnsrouter) > debug/mysql/docker_dnsrouter.log 2>&1'
junit 'test/results/junit/*'
sh 'docker compose down --remove-orphans --volumes -t 30 || true'
}
unstable {
dir(path: 'test/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}
}
}
stage('Test Postgres') {
environment {
COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_postgres"
COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.postgres.yml'
}
when {
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
steps {
sh 'rm -rf ./test/results/junit/*'
sh './scripts/ci/fulltest-cypress'
}
post {
always {
// Dumps to analyze later
sh 'mkdir -p debug/postgres'
sh 'docker logs $(docker compose ps --all -q fullstack) > debug/postgres/docker_fullstack.log 2>&1'
sh 'docker logs $(docker compose ps --all -q stepca) > debug/postgres/docker_stepca.log 2>&1'
sh 'docker logs $(docker compose ps --all -q pdns) > debug/postgres/docker_pdns.log 2>&1'
sh 'docker logs $(docker compose ps --all -q pdns-db) > debug/postgres/docker_pdns-db.log 2>&1'
sh 'docker logs $(docker compose ps --all -q dnsrouter) > debug/postgres/docker_dnsrouter.log 2>&1'
sh 'docker logs $(docker compose ps --all -q db-postgres) > debug/postgres/docker_db-postgres.log 2>&1'
sh 'docker logs $(docker compose ps --all -q authentik) > debug/postgres/docker_authentik.log 2>&1'
sh 'docker logs $(docker compose ps --all -q authentik-redis) > debug/postgres/docker_authentik-redis.log 2>&1'
sh 'docker logs $(docke rcompose ps --all -q authentik-ldap) > debug/postgres/docker_authentik-ldap.log 2>&1'
junit 'test/results/junit/*'
sh 'docker compose down --remove-orphans --volumes -t 30 || true'
}
unstable {
dir(path: 'test/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}
}
}
stage('MultiArch Build') {
when {
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
steps {
sh "./scripts/buildx --push ${buildxPushTags}"
}
}
stage('Docs / Comment') {
parallel {
stage('Docs Job') {
when {
allOf {
branch pattern: "^(develop|master)\$", comparator: "REGEXP"
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
}
steps {
build wait: false, job: 'nginx-proxy-manager-docs', parameters: [string(name: 'docs_branch', value: "$BRANCH_NAME")]
}
}
stage('PR Comment') {
when {
allOf {
changeRequest()
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
}
steps {
script {
npmGithubPrComment("""Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/nginxproxymanager/${IMAGE}-dev):
```
nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}
```
> [!NOTE]
> Ensure you backup your NPM instance before testing this image! Especially if there are database changes.
> This is a different docker image namespace than the official image.
> [!WARNING]
> Changes and additions to DNS Providers require verification by at least 2 members of the community!
""", true)
}
}
}
}
}
}
post {
always {
sh 'echo Reverting ownership'
sh 'docker run --rm -v "$(pwd):/data" jc21/ci-tools chown -R "$(id -u):$(id -g)" /data'
printResult(true)
}
failure {
archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true)
}
unstable {
archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true)
}
}
}
def getVersion() {
ver = sh(script: 'cat .version', returnStdout: true)
return ver.trim()
}
def getCommit() {
ver = sh(script: 'git log -n 1 --format=%h', returnStdout: true)
return ver.trim()
}

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.13.1-green.svg?style=for-the-badge"> <img src="https://img.shields.io/badge/version-2.13.4-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

@@ -26,8 +26,8 @@
"azure": { "azure": {
"name": "Azure", "name": "Azure",
"package_name": "certbot-dns-azure", "package_name": "certbot-dns-azure",
"version": "~=1.2.0", "version": "~=2.6.1",
"dependencies": "", "dependencies": "azure-mgmt-dns==8.2.0",
"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"
}, },
@@ -370,7 +370,7 @@
"leaseweb": { "leaseweb": {
"name": "LeaseWeb", "name": "LeaseWeb",
"package_name": "certbot-dns-leaseweb", "package_name": "certbot-dns-leaseweb",
"version": "~=1.0.1", "version": "~=1.0.3",
"dependencies": "", "dependencies": "",
"credentials": "dns_leaseweb_api_token = 01234556789", "credentials": "dns_leaseweb_api_token = 01234556789",
"full_plugin_name": "dns-leaseweb" "full_plugin_name": "dns-leaseweb"
@@ -399,6 +399,14 @@
"credentials": "dns_luadns_email = user@example.com\ndns_luadns_token = 0123456789abcdef0123456789abcdef", "credentials": "dns_luadns_email = user@example.com\ndns_luadns_token = 0123456789abcdef0123456789abcdef",
"full_plugin_name": "dns-luadns" "full_plugin_name": "dns-luadns"
}, },
"mchost24": {
"name": "MC-HOST24",
"package_name": "certbot-dns-mchost24",
"version": "",
"dependencies": "",
"credentials": "# Obtain API token using https://github.com/JoeJoeTV/mchost24-api-python\ndns_mchost24_api_token=<insert obtained API token here>",
"full_plugin_name": "dns-mchost24"
},
"mijnhost": { "mijnhost": {
"name": "mijn.host", "name": "mijn.host",
"package_name": "certbot-dns-mijn-host", "package_name": "certbot-dns-mijn-host",

View File

@@ -1,6 +1,8 @@
import knex from "knex"; import knex from "knex";
import {configGet, configHas} from "./lib/config.js"; import {configGet, configHas} from "./lib/config.js";
let instance = null;
const generateDbConfig = () => { const generateDbConfig = () => {
if (!configHas("database")) { if (!configHas("database")) {
throw new Error( throw new Error(
@@ -30,4 +32,11 @@ const generateDbConfig = () => {
}; };
}; };
export default knex(generateDbConfig()); const getInstance = () => {
if (!instance) {
instance = knex(generateDbConfig());
}
return instance;
}
export default getInstance;

View File

@@ -216,6 +216,11 @@ const internalNginx = {
} }
} }
// For redirection hosts, if the scheme is not http or https, set it to $scheme
if (nice_host_type === "redirection_host" && ['http', 'https'].indexOf(host.forward_scheme.toLowerCase()) === -1) {
host.forward_scheme = "$scheme";
}
if (host.locations) { if (host.locations) {
//logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2)); //logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2));
origLocations = [].concat(host.locations); origLocations = [].concat(host.locations);

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;

View File

@@ -15,6 +15,7 @@ const certbot = new signale.Signale({ scope: "Certbot ", ...opts });
const importer = new signale.Signale({ scope: "Importer ", ...opts }); const importer = new signale.Signale({ scope: "Importer ", ...opts });
const setup = new signale.Signale({ scope: "Setup ", ...opts }); const setup = new signale.Signale({ scope: "Setup ", ...opts });
const ipRanges = new signale.Signale({ scope: "IP Ranges", ...opts }); const ipRanges = new signale.Signale({ scope: "IP Ranges", ...opts });
const remoteVersion = new signale.Signale({ scope: "Remote Version", ...opts });
const debug = (logger, ...args) => { const debug = (logger, ...args) => {
if (isDebugMode()) { if (isDebugMode()) {
@@ -22,4 +23,4 @@ const debug = (logger, ...args) => {
} }
}; };
export { debug, global, migrate, express, access, nginx, ssl, certbot, importer, setup, ipRanges }; export { debug, global, migrate, express, access, nginx, ssl, certbot, importer, setup, ipRanges, remoteVersion };

View File

@@ -2,9 +2,9 @@ import db from "./db.js";
import { migrate as logger } from "./logger.js"; import { migrate as logger } from "./logger.js";
const migrateUp = async () => { const migrateUp = async () => {
const version = await db.migrate.currentVersion(); const version = await db().migrate.currentVersion();
logger.info("Current database version:", version); logger.info("Current database version:", version);
return await db.migrate.latest({ return await db().migrate.latest({
tableName: "migrations", tableName: "migrations",
directory: "migrations", directory: "migrations",
}); });

View File

@@ -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 };

View File

@@ -10,7 +10,7 @@ import now from "./now_helper.js";
import ProxyHostModel from "./proxy_host.js"; import ProxyHostModel from "./proxy_host.js";
import User from "./user.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"];

View File

@@ -6,7 +6,7 @@ import db from "../db.js";
import accessListModel from "./access_list.js"; import accessListModel from "./access_list.js";
import now from "./now_helper.js"; import now from "./now_helper.js";
Model.knex(db); Model.knex(db());
class AccessListAuth extends Model { class AccessListAuth extends Model {
$beforeInsert() { $beforeInsert() {

View File

@@ -6,7 +6,7 @@ import db from "../db.js";
import accessListModel from "./access_list.js"; import accessListModel from "./access_list.js";
import now from "./now_helper.js"; import now from "./now_helper.js";
Model.knex(db); Model.knex(db());
class AccessListClient extends Model { class AccessListClient extends Model {
$beforeInsert() { $beforeInsert() {

View File

@@ -6,7 +6,7 @@ import db from "../db.js";
import now from "./now_helper.js"; import now from "./now_helper.js";
import User from "./user.js"; import User from "./user.js";
Model.knex(db); Model.knex(db());
class AuditLog extends Model { class AuditLog extends Model {
$beforeInsert() { $beforeInsert() {

View File

@@ -8,7 +8,7 @@ import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.j
import now from "./now_helper.js"; import now from "./now_helper.js";
import User from "./user.js"; import User from "./user.js";
Model.knex(db); Model.knex(db());
const boolFields = ["is_deleted"]; const boolFields = ["is_deleted"];

View File

@@ -11,7 +11,7 @@ import redirectionHostModel from "./redirection_host.js";
import streamModel from "./stream.js"; import streamModel from "./stream.js";
import userModel from "./user.js"; import userModel from "./user.js";
Model.knex(db); Model.knex(db());
const boolFields = ["is_deleted"]; const boolFields = ["is_deleted"];

View File

@@ -8,7 +8,7 @@ import Certificate from "./certificate.js";
import now from "./now_helper.js"; import now from "./now_helper.js";
import User from "./user.js"; import User from "./user.js";
Model.knex(db); Model.knex(db());
const boolFields = ["is_deleted", "ssl_forced", "http2_support", "enabled", "hsts_enabled", "hsts_subdomains"]; const boolFields = ["is_deleted", "ssl_forced", "http2_support", "enabled", "hsts_enabled", "hsts_subdomains"];

View File

@@ -2,7 +2,7 @@ import { Model } from "objection";
import db from "../db.js"; import db from "../db.js";
import { isSqlite } from "../lib/config.js"; import { isSqlite } from "../lib/config.js";
Model.knex(db); Model.knex(db());
export default () => { export default () => {
if (isSqlite()) { if (isSqlite()) {

View File

@@ -9,7 +9,7 @@ import Certificate from "./certificate.js";
import now from "./now_helper.js"; import now from "./now_helper.js";
import User from "./user.js"; import User from "./user.js";
Model.knex(db); Model.knex(db());
const boolFields = [ const boolFields = [
"is_deleted", "is_deleted",

View File

@@ -8,7 +8,7 @@ import Certificate from "./certificate.js";
import now from "./now_helper.js"; import now from "./now_helper.js";
import User from "./user.js"; import User from "./user.js";
Model.knex(db); Model.knex(db());
const boolFields = [ const boolFields = [
"is_deleted", "is_deleted",

View File

@@ -4,7 +4,7 @@
import { Model } from "objection"; import { Model } from "objection";
import db from "../db.js"; import db from "../db.js";
Model.knex(db); Model.knex(db());
class Setting extends Model { class Setting extends Model {
$beforeInsert () { $beforeInsert () {

View File

@@ -5,7 +5,7 @@ import Certificate from "./certificate.js";
import now from "./now_helper.js"; import now from "./now_helper.js";
import User from "./user.js"; import User from "./user.js";
Model.knex(db); Model.knex(db());
const boolFields = ["is_deleted", "enabled", "tcp_forwarding", "udp_forwarding"]; const boolFields = ["is_deleted", "enabled", "tcp_forwarding", "udp_forwarding"];

View File

@@ -7,7 +7,7 @@ import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.j
import now from "./now_helper.js"; import now from "./now_helper.js";
import UserPermission from "./user_permission.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"];

View File

@@ -5,7 +5,7 @@ import { Model } from "objection";
import db from "../db.js"; import db from "../db.js";
import now from "./now_helper.js"; import now from "./now_helper.js";
Model.knex(db); Model.knex(db());
class UserPermission extends Model { class UserPermission extends Model {
$beforeInsert () { $beforeInsert () {

View File

@@ -14,6 +14,7 @@ import schemaRoutes from "./schema.js";
import settingsRoutes from "./settings.js"; import settingsRoutes from "./settings.js";
import tokensRoutes from "./tokens.js"; import tokensRoutes from "./tokens.js";
import usersRoutes from "./users.js"; import usersRoutes from "./users.js";
import versionRoutes from "./version.js";
const router = express.Router({ const router = express.Router({
caseSensitive: true, caseSensitive: true,
@@ -46,6 +47,7 @@ router.use("/users", usersRoutes);
router.use("/audit-log", auditLogRoutes); router.use("/audit-log", auditLogRoutes);
router.use("/reports", reportsRoutes); router.use("/reports", reportsRoutes);
router.use("/settings", settingsRoutes); router.use("/settings", settingsRoutes);
router.use("/version", versionRoutes);
router.use("/nginx/proxy-hosts", proxyHostsRoutes); router.use("/nginx/proxy-hosts", proxyHostsRoutes);
router.use("/nginx/redirection-hosts", redirectionHostsRoutes); router.use("/nginx/redirection-hosts", redirectionHostsRoutes);
router.use("/nginx/dead-hosts", deadHostsRoutes); router.use("/nginx/dead-hosts", deadHostsRoutes);

40
backend/routes/version.js Normal file
View File

@@ -0,0 +1,40 @@
import express from "express";
import internalRemoteVersion from "../internal/remote-version.js";
import { debug, express as logger } from "../logger.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/version/check
*/
router
.route("/check")
.options((_, res) => {
res.sendStatus(204);
})
/**
* GET /api/version/check
*
* Check for available updates
*/
.get(async (req, res, _next) => {
try {
const data = await internalRemoteVersion.get();
res.status(200).send(data);
} catch (error) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${error}`);
// Send 200 even though there's an error to avoid triggering update checks repeatedly
res.status(200).send({
current: null,
latest: null,
update_available: false,
});
}
});
export default router;

View File

@@ -0,0 +1,23 @@
{
"type": "object",
"description": "Check Version object",
"additionalProperties": false,
"required": ["current", "latest", "update_available"],
"properties": {
"current": {
"type": ["string", "null"],
"description": "Current version string",
"example": "v2.10.1"
},
"latest": {
"type": ["string", "null"],
"description": "Latest version string",
"example": "v2.13.4"
},
"update_available": {
"type": "boolean",
"description": "Whether there's an update available",
"example": true
}
}
}

View File

@@ -0,0 +1,26 @@
{
"operationId": "checkVersion",
"summary": "Returns any new version data from github",
"tags": ["public"],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"current": "v2.12.0",
"latest": "v2.13.4",
"update_available": true
}
}
},
"schema": {
"$ref": "../../../components/check-version-object.json"
}
}
}
}
}
}

View File

@@ -293,6 +293,11 @@
"$ref": "./paths/tokens/post.json" "$ref": "./paths/tokens/post.json"
} }
}, },
"/version/check": {
"get": {
"$ref": "./paths/version/check/get.json"
}
},
"/users": { "/users": {
"get": { "get": {
"$ref": "./paths/users/get.json" "$ref": "./paths/users/get.json"

View File

@@ -37,7 +37,7 @@ const setupDefaultUser = async () => {
const data = { const data = {
is_deleted: 0, is_deleted: 0,
email: email, email: initialAdminEmail,
name: "Administrator", name: "Administrator",
nickname: "Admin", nickname: "Admin",
avatar: "", avatar: "",
@@ -53,7 +53,7 @@ const setupDefaultUser = async () => {
.insert({ .insert({
user_id: user.id, user_id: user.id,
type: "password", type: "password",
secret: password, secret: initialAdminPassword,
meta: {}, meta: {},
}); });

View File

@@ -4,7 +4,7 @@
auth_basic "Authorization required"; auth_basic "Authorization required";
auth_basic_user_file /data/access/{{ access_list_id }}; auth_basic_user_file /data/access/{{ access_list_id }};
{% if access_list.pass_auth == 0 or access_list.pass_auth == true %} {% if access_list.pass_auth == 0 or access_list.pass_auth == false %}
proxy_set_header Authorization ""; proxy_set_header Authorization "";
{% endif %} {% endif %}

View File

@@ -1430,9 +1430,9 @@ isexe@^2.0.0:
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
js-yaml@^4.1.0: js-yaml@^4.1.0:
version "4.1.0" version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
dependencies: dependencies:
argparse "^2.0.1" argparse "^2.0.1"

View File

@@ -4,7 +4,6 @@
# This file assumes that the frontend has been built using ./scripts/frontend-build # This file assumes that the frontend has been built using ./scripts/frontend-build
FROM nginxproxymanager/testca AS testca FROM nginxproxymanager/testca AS testca
FROM letsencrypt/pebble AS pebbleca
FROM nginxproxymanager/nginx-full:certbot-node FROM nginxproxymanager/nginx-full:certbot-node
ARG TARGETPLATFORM ARG TARGETPLATFORM
@@ -46,7 +45,6 @@ RUN yarn install \
# add late to limit cache-busting by modifications # add late to limit cache-busting by modifications
COPY docker/rootfs / COPY docker/rootfs /
COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem
COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
# Remove frontend service not required for prod, dev nginx config as well # Remove frontend service not required for prod, dev nginx config as well

View File

@@ -1,6 +1,6 @@
AUTHENTIK_SECRET_KEY=gl8woZe8L6IIX8SC0c5Ocsj0xPkX5uJo5DVZCFl+L/QGbzuplfutYuua2ODNLEiDD3aFd9H2ylJmrke0 AUTHENTIK_SECRET_KEY=gl8woZe8L6IIX8SC0c5Ocsj0xPkX5uJo5DVZCFl+L/QGbzuplfutYuua2ODNLEiDD3aFd9H2ylJmrke0
AUTHENTIK_REDIS__HOST=authentik-redis AUTHENTIK_REDIS__HOST=authentik-redis
AUTHENTIK_POSTGRESQL__HOST=db-postgres AUTHENTIK_POSTGRESQL__HOST=pgdb.internal
AUTHENTIK_POSTGRESQL__USER=authentik AUTHENTIK_POSTGRESQL__USER=authentik
AUTHENTIK_POSTGRESQL__NAME=authentik AUTHENTIK_POSTGRESQL__NAME=authentik
AUTHENTIK_POSTGRESQL__PASSWORD=07EKS5NLI6Tpv68tbdvrxfvj AUTHENTIK_POSTGRESQL__PASSWORD=07EKS5NLI6Tpv68tbdvrxfvj

View File

@@ -1,5 +1,4 @@
FROM nginxproxymanager/testca AS testca FROM nginxproxymanager/testca AS testca
FROM letsencrypt/pebble AS pebbleca
FROM nginxproxymanager/nginx-full:certbot-node FROM nginxproxymanager/nginx-full:certbot-node
LABEL maintainer="Jamie Curnow <jc@jc21.com>" LABEL maintainer="Jamie Curnow <jc@jc21.com>"
@@ -33,7 +32,6 @@ RUN rm -f /etc/nginx/conf.d/production.conf \
&& chmod 644 -R /root/.cache && 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=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
EXPOSE 80 81 443 EXPOSE 80 81 443

View File

@@ -1,12 +0,0 @@
{
"pebble": {
"listenAddress": "0.0.0.0:443",
"managementListenAddress": "0.0.0.0:15000",
"certificate": "test/certs/localhost/cert.pem",
"privateKey": "test/certs/localhost/key.pem",
"httpPort": 80,
"tlsPort": 443,
"ocspResponderURL": "",
"externalAccountBindingRequired": false
}
}

View File

@@ -6,7 +6,7 @@ services:
fullstack: fullstack:
environment: environment:
DB_POSTGRES_HOST: "db-postgres" DB_POSTGRES_HOST: "pgdb.internal"
DB_POSTGRES_PORT: "5432" DB_POSTGRES_PORT: "5432"
DB_POSTGRES_USER: "npm" DB_POSTGRES_USER: "npm"
DB_POSTGRES_PASSWORD: "npmpass" DB_POSTGRES_PASSWORD: "npmpass"
@@ -27,7 +27,9 @@ services:
- psql_vol:/var/lib/postgresql/data - psql_vol:/var/lib/postgresql/data
- ./ci/postgres:/docker-entrypoint-initdb.d - ./ci/postgres:/docker-entrypoint-initdb.d
networks: networks:
- fulltest fulltest:
aliases:
- pgdb.internal
authentik-redis: authentik-redis:
image: "redis:alpine" image: "redis:alpine"
@@ -41,6 +43,8 @@ services:
timeout: 3s timeout: 3s
volumes: volumes:
- redis_vol:/data - redis_vol:/data
networks:
- fulltest
authentik: authentik:
image: ghcr.io/goauthentik/server:2024.10.1 image: ghcr.io/goauthentik/server:2024.10.1
@@ -51,6 +55,8 @@ services:
depends_on: depends_on:
- authentik-redis - authentik-redis
- db-postgres - db-postgres
networks:
- fulltest
authentik-worker: authentik-worker:
image: ghcr.io/goauthentik/server:2024.10.1 image: ghcr.io/goauthentik/server:2024.10.1
@@ -61,6 +67,8 @@ services:
depends_on: depends_on:
- authentik-redis - authentik-redis
- db-postgres - db-postgres
networks:
- fulltest
authentik-ldap: authentik-ldap:
image: ghcr.io/goauthentik/ldap:2024.10.1 image: ghcr.io/goauthentik/ldap:2024.10.1
@@ -71,6 +79,8 @@ services:
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- authentik - authentik
networks:
- fulltest
volumes: volumes:
psql_vol: psql_vol:

View File

@@ -3,31 +3,34 @@
# This is a base compose file, it should be extended with a # This is a base compose file, it should be extended with a
# docker-compose.ci.*.yml file # docker-compose.ci.*.yml file
services: services:
fullstack: fullstack:
image: "${IMAGE}:${BRANCH_LOWER}-ci-${BUILD_NUMBER}" image: "${IMAGE}:${BRANCH_LOWER}-ci-${BUILD_NUMBER}"
environment: environment:
TZ: "${TZ:-Australia/Brisbane}" TZ: "${TZ:-Australia/Brisbane}"
DEBUG: 'true' DEBUG: "true"
CI: 'true' CI: "true"
FORCE_COLOR: 1 FORCE_COLOR: 1
# Required for DNS Certificate provisioning in CI # Required for DNS Certificate provisioning in CI
LE_SERVER: 'https://ca.internal/acme/acme/directory' LE_SERVER: "https://ca.internal/acme/acme/directory"
REQUESTS_CA_BUNDLE: '/etc/ssl/certs/NginxProxyManager.crt' REQUESTS_CA_BUNDLE: "/etc/ssl/certs/NginxProxyManager.crt"
volumes: volumes:
- 'npm_data_ci:/data' - "npm_data_ci:/data"
- 'npm_le_ci:/etc/letsencrypt' - "npm_le_ci:/etc/letsencrypt"
- './dev/letsencrypt.ini:/etc/letsencrypt.ini:ro' - "./dev/letsencrypt.ini:/etc/letsencrypt.ini:ro"
- './dev/resolv.conf:/etc/resolv.conf:ro' - "./dev/resolv.conf:/etc/resolv.conf:ro"
- '/etc/localtime:/etc/localtime:ro' - "/etc/localtime:/etc/localtime:ro"
healthcheck: healthcheck:
test: ["CMD", "/usr/bin/check-health"] test: ["CMD", "/usr/bin/check-health"]
interval: 10s interval: 10s
timeout: 3s timeout: 3s
expose: expose:
- '80-81/tcp' - "80/tcp"
- '443/tcp' - "81/tcp"
- '1500-1503/tcp' - "443/tcp"
- "1500/tcp"
- "1501/tcp"
- "1502/tcp"
- "1503/tcp"
networks: networks:
fulltest: fulltest:
aliases: aliases:
@@ -38,8 +41,8 @@ services:
stepca: stepca:
image: jc21/testca image: jc21/testca
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"
networks: networks:
fulltest: fulltest:
aliases: aliases:
@@ -48,18 +51,18 @@ services:
pdns: pdns:
image: pschiffe/pdns-mysql:4.8 image: pschiffe/pdns-mysql:4.8
volumes: volumes:
- '/etc/localtime:/etc/localtime:ro' - "/etc/localtime:/etc/localtime:ro"
environment: environment:
PDNS_master: 'yes' PDNS_master: "yes"
PDNS_api: 'yes' PDNS_api: "yes"
PDNS_api_key: 'npm' PDNS_api_key: "npm"
PDNS_webserver: 'yes' PDNS_webserver: "yes"
PDNS_webserver_address: '0.0.0.0' PDNS_webserver_address: "0.0.0.0"
PDNS_webserver_password: 'npm' PDNS_webserver_password: "npm"
PDNS_webserver-allow-from: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8' PDNS_webserver-allow-from: "127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8"
PDNS_version_string: 'anonymous' PDNS_version_string: "anonymous"
PDNS_default_ttl: 1500 PDNS_default_ttl: 1500
PDNS_allow_axfr_ips: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8' PDNS_allow_axfr_ips: "127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8"
PDNS_gmysql_host: pdns-db PDNS_gmysql_host: pdns-db
PDNS_gmysql_port: 3306 PDNS_gmysql_port: 3306
PDNS_gmysql_user: pdns PDNS_gmysql_user: pdns
@@ -76,14 +79,14 @@ services:
pdns-db: pdns-db:
image: mariadb image: mariadb
environment: environment:
MYSQL_ROOT_PASSWORD: 'pdns' MYSQL_ROOT_PASSWORD: "pdns"
MYSQL_DATABASE: 'pdns' MYSQL_DATABASE: "pdns"
MYSQL_USER: 'pdns' MYSQL_USER: "pdns"
MYSQL_PASSWORD: 'pdns' MYSQL_PASSWORD: "pdns"
volumes: volumes:
- 'pdns_mysql_vol:/var/lib/mysql' - "pdns_mysql_vol:/var/lib/mysql"
- '/etc/localtime:/etc/localtime:ro' - "/etc/localtime:/etc/localtime:ro"
- './dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro' - "./dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro"
networks: networks:
- fulltest - fulltest
@@ -100,12 +103,12 @@ services:
context: ../ context: ../
dockerfile: test/cypress/Dockerfile dockerfile: test/cypress/Dockerfile
environment: environment:
HTTP_PROXY: 'squid:3128' HTTP_PROXY: "squid:3128"
HTTPS_PROXY: 'squid:3128' HTTPS_PROXY: "squid:3128"
volumes: volumes:
- 'cypress_logs:/test/results' - "cypress_logs:/test/results"
- './dev/resolv.conf:/etc/resolv.conf:ro' - "./dev/resolv.conf:/etc/resolv.conf:ro"
- '/etc/localtime:/etc/localtime:ro' - "/etc/localtime:/etc/localtime:ro"
command: cypress run --browser chrome --config-file=cypress/config/ci.js command: cypress run --browser chrome --config-file=cypress/config/ci.js
networks: networks:
- fulltest - fulltest
@@ -113,9 +116,9 @@ services:
squid: squid:
image: ubuntu/squid image: ubuntu/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"
- '/etc/localtime:/etc/localtime:ro' - "/etc/localtime:/etc/localtime:ro"
networks: networks:
- fulltest - fulltest

View File

@@ -32,7 +32,7 @@ services:
# DB_MYSQL_PASSWORD: 'npm' # DB_MYSQL_PASSWORD: 'npm'
# DB_MYSQL_NAME: 'npm' # DB_MYSQL_NAME: 'npm'
# db-postgres: # db-postgres:
DB_POSTGRES_HOST: "db-postgres" DB_POSTGRES_HOST: "pgdb.internal"
DB_POSTGRES_PORT: "5432" DB_POSTGRES_PORT: "5432"
DB_POSTGRES_USER: "npm" DB_POSTGRES_USER: "npm"
DB_POSTGRES_PASSWORD: "npmpass" DB_POSTGRES_PASSWORD: "npmpass"
@@ -81,8 +81,6 @@ services:
db-postgres: db-postgres:
image: postgres:17 image: postgres:17
container_name: npm2dev.db-postgres container_name: npm2dev.db-postgres
networks:
- nginx_proxy_manager
environment: environment:
POSTGRES_USER: "npm" POSTGRES_USER: "npm"
POSTGRES_PASSWORD: "npmpass" POSTGRES_PASSWORD: "npmpass"
@@ -90,6 +88,10 @@ services:
volumes: volumes:
- psql_data:/var/lib/postgresql/data - psql_data:/var/lib/postgresql/data
- ./ci/postgres:/docker-entrypoint-initdb.d - ./ci/postgres:/docker-entrypoint-initdb.d
networks:
nginx_proxy_manager:
aliases:
- pgdb.internal
stepca: stepca:
image: jc21/testca image: jc21/testca

2
frontend/.gitignore vendored
View File

@@ -1,3 +1,5 @@
src/locale/lang
# Logs # Logs
logs logs
*.log *.log

View File

@@ -8,7 +8,12 @@
const allLocales = [ const allLocales = [
["en", "en-US"], ["en", "en-US"],
["fa", "fa-IR"], ["es", "es-ES"],
["de", "de-DE"],
["ru", "ru-RU"],
["sk", "sk-SK"],
["zh", "zh-CN"],
["pl", "pl-PL"],
]; ];
const ignoreUnused = [ const ignoreUnused = [

View File

@@ -0,0 +1,8 @@
import * as api from "./base";
import type { VersionCheckResponse } from "./responseTypes";
export async function checkVersion(): Promise<VersionCheckResponse> {
return await api.get({
url: "/version/check",
});
}

View File

@@ -1,3 +1,4 @@
export * from "./checkVersion";
export * from "./createAccessList"; export * from "./createAccessList";
export * from "./createCertificate"; export * from "./createCertificate";
export * from "./createDeadHost"; export * from "./createDeadHost";

View File

@@ -19,3 +19,9 @@ export interface ValidatedCertificateResponse {
export interface LoginAsTokenResponse extends TokenResponse { export interface LoginAsTokenResponse extends TokenResponse {
user: User; user: User;
} }
export interface VersionCheckResponse {
current: string | null;
latest: string | null;
updateAvailable: boolean;
}

View File

@@ -4,7 +4,7 @@ import type { ReactNode } from "react";
import Select, { type ActionMeta, components, type OptionProps } from "react-select"; import Select, { type ActionMeta, components, type OptionProps } from "react-select";
import type { AccessList } from "src/api/backend"; import type { AccessList } from "src/api/backend";
import { useAccessLists } from "src/hooks"; import { useAccessLists } from "src/hooks";
import { DateTimeFormat, intl, T } from "src/locale"; import { formatDateTime, intl, T } from "src/locale";
interface AccessOption { interface AccessOption {
readonly value: number; readonly value: number;
@@ -48,7 +48,7 @@ export function AccessField({ name = "accessListId", label = "access-list", id =
{ {
users: item?.items?.length, users: item?.items?.length,
rules: item?.clients?.length, rules: item?.clients?.length,
date: item?.createdOn ? DateTimeFormat(item?.createdOn) : "N/A", date: item?.createdOn ? formatDateTime(item?.createdOn) : "N/A",
}, },
), ),
icon: <IconLock size={14} className="text-lime" />, icon: <IconLock size={14} className="text-lime" />,

View File

@@ -3,7 +3,7 @@ import { Field, useFormikContext } from "formik";
import Select, { type ActionMeta, components, type OptionProps } from "react-select"; import Select, { type ActionMeta, components, type OptionProps } from "react-select";
import type { Certificate } from "src/api/backend"; import type { Certificate } from "src/api/backend";
import { useCertificates } from "src/hooks"; import { useCertificates } from "src/hooks";
import { DateTimeFormat, intl, T } from "src/locale"; import { formatDateTime, intl, T } from "src/locale";
interface CertOption { interface CertOption {
readonly value: number | "new"; readonly value: number | "new";
@@ -75,7 +75,7 @@ export function SSLCertificateField({
data?.map((cert: Certificate) => ({ data?.map((cert: Certificate) => ({
value: cert.id, value: cert.id,
label: cert.niceName, label: cert.niceName,
subLabel: `${cert.provider === "letsencrypt" ? intl.formatMessage({ id: "lets-encrypt" }) : cert.provider} &mdash; ${intl.formatMessage({ id: "expires.on" }, { date: cert.expiresOn ? DateTimeFormat(cert.expiresOn) : "N/A" })}`, subLabel: `${cert.provider === "letsencrypt" ? intl.formatMessage({ id: "lets-encrypt" }) : cert.provider} ${intl.formatMessage({ id: "expires.on" }, { date: cert.expiresOn ? formatDateTime(cert.expiresOn) : "N/A" })}`,
icon: <IconShield size={14} className="text-pink" />, icon: <IconShield size={14} className="text-pink" />,
})) || []; })) || [];

View File

@@ -1,8 +1,9 @@
import { useHealth } from "src/hooks"; import { useCheckVersion, useHealth } from "src/hooks";
import { T } from "src/locale"; import { T } from "src/locale";
export function SiteFooter() { export function SiteFooter() {
const health = useHealth(); const health = useHealth();
const { data: versionData } = useCheckVersion();
const getVersion = () => { const getVersion = () => {
if (!health.data) { if (!health.data) {
@@ -55,6 +56,19 @@ export function SiteFooter() {
{getVersion()}{" "} {getVersion()}{" "}
</a> </a>
</li> </li>
{versionData?.updateAvailable && versionData?.latest && (
<li className="list-inline-item">
<a
href={`https://github.com/NginxProxyManager/nginx-proxy-manager/releases/tag/${versionData.latest}`}
className="link-warning fw-bold"
target="_blank"
rel="noopener"
title={`New version ${versionData.latest} is available`}
>
<T id="update-available" data={{ latestVersion: versionData.latest }} />
</a>
</li>
)}
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -190,7 +190,7 @@ export function SiteMenu() {
return ( return (
<header className="navbar-expand-md"> <header className="navbar-expand-md">
<div className="collapse navbar-collapse"> <div className="collapse navbar-collapse" id="navbar-menu">
<div className="navbar"> <div className="navbar">
<div className="container-xl"> <div className="container-xl">
<div className="row flex-column flex-md-row flex-fill align-items-center"> <div className="row flex-column flex-md-row flex-fill align-items-center">

View File

@@ -1,6 +1,6 @@
import cn from "classnames"; import cn from "classnames";
import { differenceInDays, isPast, parseISO } from "date-fns"; import { differenceInDays, isPast } from "date-fns";
import { DateTimeFormat } from "src/locale"; import { formatDateTime, parseDate } from "src/locale";
interface Props { interface Props {
value: string; value: string;
@@ -8,11 +8,12 @@ interface Props {
highlistNearlyExpired?: boolean; highlistNearlyExpired?: boolean;
} }
export function DateFormatter({ value, highlightPast, highlistNearlyExpired }: Props) { export function DateFormatter({ value, highlightPast, highlistNearlyExpired }: Props) {
const dateIsPast = isPast(parseISO(value)); const d = parseDate(value);
const days = differenceInDays(parseISO(value), new Date()); const dateIsPast = d ? isPast(d) : false;
const days = d ? differenceInDays(d, new Date()) : 0;
const cl = cn({ const cl = cn({
"text-danger": highlightPast && dateIsPast, "text-danger": highlightPast && dateIsPast,
"text-warning": highlistNearlyExpired && !dateIsPast && days <= 30 && days >= 0, "text-warning": highlistNearlyExpired && !dateIsPast && days <= 30 && days >= 0,
}); });
return <span className={cl}>{DateTimeFormat(value)}</span>; return <span className={cl}>{formatDateTime(value)}</span>;
} }

View File

@@ -1,6 +1,6 @@
import cn from "classnames"; import cn from "classnames";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { DateTimeFormat, T } from "src/locale"; import { formatDateTime, T } from "src/locale";
interface Props { interface Props {
domains: string[]; domains: string[];
@@ -10,35 +10,42 @@ interface Props {
color?: string; color?: string;
} }
const DomainLink = ({ domain, color }: { domain: string; color?: string }) => { const DomainLink = ({ domain, color }: { domain?: string; color?: string }) => {
// when domain contains a wildcard, make the link go nowhere. // when domain contains a wildcard, make the link go nowhere.
let onClick: ((e: React.MouseEvent) => void) | undefined; // Apparently the domain can be null or undefined sometimes.
if (domain.includes("*")) { // This try is just a safeguard to prevent the whole formatter from breaking.
onClick = (e: React.MouseEvent) => e.preventDefault(); if (!domain) return null;
try {
let onClick: ((e: React.MouseEvent) => void) | undefined;
if (domain.includes("*")) {
onClick = (e: React.MouseEvent) => e.preventDefault();
}
return (
<a
key={domain}
href={`http://${domain}`}
target="_blank"
onClick={onClick}
className={cn("badge", color ? `bg-${color}-lt` : null, "domain-name", "me-2")}
>
{domain}
</a>
);
} catch {
return null;
} }
return (
<a
key={domain}
href={`http://${domain}`}
target="_blank"
onClick={onClick}
className={cn("badge", color ? `bg-${color}-lt` : null, "domain-name", "me-2")}
>
{domain}
</a>
);
}; };
export function DomainsFormatter({ domains, createdOn, niceName, provider, color }: Props) { export function DomainsFormatter({ domains, createdOn, niceName, provider, color }: Props) {
const elms: ReactNode[] = []; const elms: ReactNode[] = [];
if (domains.length === 0 && !niceName) { if ((!domains || domains.length === 0) && !niceName) {
elms.push( elms.push(
<span key="nice-name" className="badge bg-danger-lt me-2"> <span key="nice-name" className="badge bg-danger-lt me-2">
Unknown Unknown
</span>, </span>,
); );
} }
if (niceName && provider !== "letsencrypt") { if (!domains || (niceName && provider !== "letsencrypt")) {
elms.push( elms.push(
<span key="nice-name" className="badge bg-info-lt me-2"> <span key="nice-name" className="badge bg-info-lt me-2">
{niceName} {niceName}
@@ -46,14 +53,16 @@ export function DomainsFormatter({ domains, createdOn, niceName, provider, color
); );
} }
domains.map((domain: string) => elms.push(<DomainLink key={domain} domain={domain} color={color} />)); if (domains) {
domains.map((domain: string) => elms.push(<DomainLink key={domain} domain={domain} color={color} />));
}
return ( return (
<div className="flex-fill"> <div className="flex-fill">
<div className="font-weight-medium">{...elms}</div> <div className="font-weight-medium">{...elms}</div>
{createdOn ? ( {createdOn ? (
<div className="text-secondary mt-1"> <div className="text-secondary mt-1">
<T id="created-on" data={{ date: DateTimeFormat(createdOn) }} /> <T id="created-on" data={{ date: formatDateTime(createdOn) }} />
</div> </div>
) : null} ) : null}
</div> </div>

View File

@@ -1,7 +1,7 @@
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconShield, IconUser } from "@tabler/icons-react"; import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconShield, IconUser } from "@tabler/icons-react";
import cn from "classnames"; import cn from "classnames";
import type { AuditLog } from "src/api/backend"; import type { AuditLog } from "src/api/backend";
import { DateTimeFormat, T } from "src/locale"; import { formatDateTime, T } from "src/locale";
const getEventValue = (event: AuditLog) => { const getEventValue = (event: AuditLog) => {
switch (event.objectType) { switch (event.objectType) {
@@ -73,7 +73,7 @@ export function EventFormatter({ row }: Props) {
<T id={`object.event.${row.action}`} tData={{ object: row.objectType }} /> <T id={`object.event.${row.action}`} tData={{ object: row.objectType }} />
&nbsp; &mdash; <span className="badge">{getEventValue(row)}</span> &nbsp; &mdash; <span className="badge">{getEventValue(row)}</span>
</div> </div>
<div className="text-secondary mt-1">{DateTimeFormat(row.createdOn)}</div> <div className="text-secondary mt-1">{formatDateTime(row.createdOn)}</div>
</div> </div>
); );
} }

View File

@@ -1,4 +1,4 @@
import { DateTimeFormat, T } from "src/locale"; import { formatDateTime, T } from "src/locale";
interface Props { interface Props {
value: string; value: string;
@@ -13,7 +13,7 @@ export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) {
</div> </div>
{createdOn ? ( {createdOn ? (
<div className={`text-secondary mt-1 ${disabled ? "text-red" : ""}`}> <div className={`text-secondary mt-1 ${disabled ? "text-red" : ""}`}>
<T id={disabled ? "disabled" : "created-on"} data={{ date: DateTimeFormat(createdOn) }} /> <T id={disabled ? "disabled" : "created-on"} data={{ date: formatDateTime(createdOn) }} />
</div> </div>
) : null} ) : null}
</div> </div>

View File

@@ -4,6 +4,7 @@ export * from "./useAuditLog";
export * from "./useAuditLogs"; export * from "./useAuditLogs";
export * from "./useCertificate"; export * from "./useCertificate";
export * from "./useCertificates"; export * from "./useCertificates";
export * from "./useCheckVersion";
export * from "./useDeadHost"; export * from "./useDeadHost";
export * from "./useDeadHosts"; export * from "./useDeadHosts";
export * from "./useDnsProviders"; export * from "./useDnsProviders";

View File

@@ -0,0 +1,18 @@
import { useQuery } from "@tanstack/react-query";
import { checkVersion, type VersionCheckResponse } from "src/api/backend";
const fetchVersion = () => checkVersion();
const useCheckVersion = (options = {}) => {
return useQuery<VersionCheckResponse, Error>({
queryKey: ["version-check"],
queryFn: fetchVersion,
refetchOnWindowFocus: false,
retry: 5,
refetchInterval: 30 * 1000, // 30 seconds
staleTime: 5 * 60 * 1000, // 5 mins
...options,
});
};
export { fetchVersion, useCheckVersion };

View File

@@ -1,15 +0,0 @@
import { intlFormat, parseISO } from "date-fns";
const DateTimeFormat = (isoDate: string) =>
intlFormat(parseISO(isoDate), {
weekday: "long",
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: true,
});
export { DateTimeFormat };

57
frontend/src/locale/IntlProvider.tsx Normal file → Executable file
View File

@@ -1,15 +1,45 @@
import { createIntl, createIntlCache } from "react-intl"; import { createIntl, createIntlCache } from "react-intl";
import langDe from "./lang/de.json";
import langEn from "./lang/en.json"; import langEn from "./lang/en.json";
import langEs from "./lang/es.json";
import langJa from "./lang/ja.json";
import langList from "./lang/lang-list.json"; import langList from "./lang/lang-list.json";
import langRu from "./lang/ru.json";
import langSk from "./lang/sk.json";
import langZh from "./lang/zh.json";
import langPl from "./lang/pl.json";
// first item of each array should be the language code, // first item of each array should be the language code,
// not the country code // not the country code
// Remember when adding to this list, also update check-locales.js script // Remember when adding to this list, also update check-locales.js script
const localeOptions = [["en", "en-US"]]; const localeOptions = [
["en", "en-US"],
["de", "de-DE"],
["es", "es-ES"],
["ja", "ja-JP"],
["ru", "ru-RU"],
["sk", "sk-SK"],
["zh", "zh-CN"],
["pl", "pl-PL"],
];
const loadMessages = (locale?: string): typeof langList & typeof langEn => { const loadMessages = (locale?: string): typeof langList & typeof langEn => {
const thisLocale = locale || "en"; const thisLocale = locale || "en";
switch (thisLocale.slice(0, 2)) { switch (thisLocale.slice(0, 2)) {
case "de":
return Object.assign({}, langList, langEn, langDe);
case "es":
return Object.assign({}, langList, langEn, langEs);
case "ja":
return Object.assign({}, langList, langEn, langJa);
case "ru":
return Object.assign({}, langList, langEn, langRu);
case "sk":
return Object.assign({}, langList, langEn, langSk);
case "zh":
return Object.assign({}, langList, langEn, langZh);
case "pl":
return Object.assign({}, langList, langEn, langPl);
default: default:
return Object.assign({}, langList, langEn); return Object.assign({}, langList, langEn);
} }
@@ -17,6 +47,27 @@ const loadMessages = (locale?: string): typeof langList & typeof langEn => {
const getFlagCodeForLocale = (locale?: string) => { const getFlagCodeForLocale = (locale?: string) => {
switch (locale) { switch (locale) {
case "es-ES":
case "es":
return "ES";
case "de-DE":
case "de":
return "DE";
case "ja-JP":
case "ja":
return "JP";
case "ru-RU":
case "ru":
return "RU";
case "sk-SK":
case "sk":
return "SK";
case "zh":
case "zh-CN":
return "CN";
case "pl":
case "pl-PL":
return "PL";
default: default:
return "EN"; return "EN";
} }
@@ -30,6 +81,10 @@ const getLocale = (short = false) => {
if (short) { if (short) {
return loc.slice(0, 2); return loc.slice(0, 2);
} }
// finally, fallback
if (!loc) {
loc = "en";
}
return loc; return loc;
}; };

View File

@@ -39,8 +39,9 @@ not be complete by the time you're reading this:
- frontend/src/locale/src/[yourlang].json - frontend/src/locale/src/[yourlang].json
- frontend/src/locale/src/lang-list.json - frontend/src/locale/src/lang-list.json
- frontend/src/locale/src/HelpDoc/* - frontend/src/locale/src/HelpDoc/[yourlang]/*
- frontend/src/locale/IntlProvider.tsx - frontend/src/locale/IntlProvider.tsx
- frontend/check-locales.cjs
## Checking for missing translations in languages ## Checking for missing translations in languages

View File

@@ -0,0 +1,74 @@
import { formatDateTime } from "src/locale";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
describe("DateFormatter", () => {
// Keep a reference to the real Intl to restore later
const RealIntl = global.Intl;
const desiredTimeZone = "Europe/London";
const desiredLocale = "en-GB";
beforeAll(() => {
// Ensure Node-based libs using TZ behave deterministically
try {
process.env.TZ = desiredTimeZone;
} catch {
// ignore if not available
}
// Mock Intl.DateTimeFormat so formatting is stable regardless of host
const MockedDateTimeFormat = class extends RealIntl.DateTimeFormat {
constructor(_locales?: string | string[], options?: Intl.DateTimeFormatOptions) {
super(desiredLocale, {
...options,
timeZone: desiredTimeZone,
});
}
} as unknown as typeof Intl.DateTimeFormat;
global.Intl = {
...RealIntl,
DateTimeFormat: MockedDateTimeFormat,
};
});
afterAll(() => {
// Restore original Intl after tests
global.Intl = RealIntl;
});
it("format date from iso date", () => {
const value = "2024-01-01T00:00:00.000Z";
const text = formatDateTime(value);
expect(text).toBe("Monday, 01/01/2024, 12:00:00 am");
});
it("format date from unix timestamp number", () => {
const value = 1762476112;
const text = formatDateTime(value);
expect(text).toBe("Friday, 07/11/2025, 12:41:52 am");
});
it("format date from unix timestamp string", () => {
const value = "1762476112";
const text = formatDateTime(value);
expect(text).toBe("Friday, 07/11/2025, 12:41:52 am");
});
it("catch bad format from string", () => {
const value = "this is not a good date";
const text = formatDateTime(value);
expect(text).toBe("this is not a good date");
});
it("catch bad format from number", () => {
const value = -100;
const text = formatDateTime(value);
expect(text).toBe("-100");
});
it("catch bad format from number as string", () => {
const value = "-100";
const text = formatDateTime(value);
expect(text).toBe("-100");
});
});

View File

@@ -0,0 +1,42 @@
import { fromUnixTime, intlFormat, parseISO } from "date-fns";
const isUnixTimestamp = (value: unknown): boolean => {
if (typeof value !== "number" && typeof value !== "string") return false;
const num = Number(value);
if (!Number.isFinite(num)) return false;
// Check plausible Unix timestamp range: from 1970 to ~year 3000
// Support both seconds and milliseconds
if (num > 0 && num < 10000000000) return true; // seconds (<= 10 digits)
if (num >= 10000000000 && num < 32503680000000) return true; // milliseconds (<= 13 digits)
return false;
};
const parseDate = (value: string | number): Date | null => {
if (typeof value !== "number" && typeof value !== "string") return null;
try {
return isUnixTimestamp(value) ? fromUnixTime(+value) : parseISO(`${value}`);
} catch {
return null;
}
};
const formatDateTime = (value: string | number): string => {
const d = parseDate(value);
if (!d) return `${value}`;
try {
return intlFormat(d, {
weekday: "long",
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: true,
});
} catch {
return `${value}`;
}
};
export { formatDateTime, parseDate, isUnixTimestamp };

View File

@@ -1,2 +1,2 @@
export * from "./DateTimeFormat";
export * from "./IntlProvider"; export * from "./IntlProvider";
export * from "./Utils";

View File

@@ -1,215 +0,0 @@
{
"access-list": "Access List",
"access-list.access-count": "{count} {count, plural, one {Rule} other {Rules}}",
"access-list.auth-count": "{count} {count, plural, one {User} other {Users}}",
"access-list.help-rules-last": "When at least 1 rule exists, this deny all rule will be added last",
"access-list.help.rules-order": "Note that the allow and deny directives will be applied in the order they are defined.",
"access-list.pass-auth": "Pass Auth to Upstream",
"access-list.public": "Publicly Accessible",
"access-list.public.subtitle": "No basic auth required",
"access-list.satisfy-any": "Satisfy Any",
"access-list.subtitle": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Rule} other {Rules}} - Created: {date}",
"access-lists": "Access Lists",
"action.add": "Add",
"action.add-location": "Add Location",
"action.close": "Close",
"action.delete": "Delete",
"action.disable": "Disable",
"action.download": "Download",
"action.edit": "Edit",
"action.enable": "Enable",
"action.permissions": "Permissions",
"action.renew": "Renew",
"action.view-details": "View Details",
"auditlogs": "Audit Logs",
"cancel": "Cancel",
"certificate": "Certificate",
"certificate.custom-certificate": "Certificate",
"certificate.custom-certificate-key": "Certificate Key",
"certificate.custom-intermediate": "Intermediate Certificate",
"certificate.in-use": "In Use",
"certificate.none.subtitle": "No certificate assigned",
"certificate.none.subtitle.for-http": "This host will not use HTTPS",
"certificate.none.title": "None",
"certificate.not-in-use": "Not Used",
"certificate.renew": "Renew Certificate",
"certificates": "Certificates",
"certificates.custom": "Custom Certificate",
"certificates.custom.warning": "Key files protected with a passphrase are not supported.",
"certificates.dns.credentials": "Credentials File Content",
"certificates.dns.credentials-note": "This plugin requires a configuration file containing an API token or other credentials for your provider",
"certificates.dns.credentials-warning": "This data will be stored as plaintext in the database and in a file!",
"certificates.dns.propagation-seconds": "Propagation Seconds",
"certificates.dns.propagation-seconds-note": "Leave empty to use the plugins default value. Number of seconds to wait for DNS propagation.",
"certificates.dns.provider": "DNS Provider",
"certificates.dns.warning": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation.",
"certificates.http.reachability-404": "There is a server found at this domain but it does not seem to be Nginx Proxy Manager. Please make sure your domain points to the IP where your NPM instance is running.",
"certificates.http.reachability-failed-to-check": "Failed to check the reachability due to a communication error with site24x7.com.",
"certificates.http.reachability-not-resolved": "There is no server available at this domain. Please make sure your domain exists and points to the IP where your NPM instance is running and if necessary port 80 is forwarded in your router.",
"certificates.http.reachability-ok": "Your server is reachable and creating certificates should be possible.",
"certificates.http.reachability-other": "There is a server found at this domain but it returned an unexpected status code {code}. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.",
"certificates.http.reachability-wrong-data": "There is a server found at this domain but it returned an unexpected data. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.",
"certificates.http.test-results": "Test Results",
"certificates.http.warning": "These domains must be already configured to point to this installation.",
"certificates.request.subtitle": "with Let's Encrypt",
"certificates.request.title": "Request a new Certificate",
"column.access": "Access",
"column.authorization": "Authorization",
"column.authorizations": "Authorizations",
"column.custom-locations": "Custom Locations",
"column.destination": "Destination",
"column.details": "Details",
"column.email": "Email",
"column.event": "Event",
"column.expires": "Expires",
"column.http-code": "Access",
"column.incoming-port": "Incoming Port",
"column.name": "Name",
"column.protocol": "Protocol",
"column.provider": "Provider",
"column.roles": "Roles",
"column.rules": "Rules",
"column.satisfy": "Satisfy",
"column.satisfy-all": "All",
"column.satisfy-any": "Any",
"column.scheme": "Scheme",
"column.source": "Source",
"column.ssl": "SSL",
"column.status": "Status",
"created-on": "Created: {date}",
"dashboard": "Dashboard",
"dead-host": "404 Host",
"dead-hosts": "404 Hosts",
"dead-hosts.count": "{count} {count, plural, one {404 Host} other {404 Hosts}}",
"disabled": "Disabled",
"domain-names": "Domain Names",
"domain-names.max": "{count} domain names maximum",
"domain-names.placeholder": "Start typing to add domain...",
"domain-names.wildcards-not-permitted": "Wildcards not permitted for this type",
"domain-names.wildcards-not-supported": "Wildcards not supported for this CA",
"domains.force-ssl": "Force SSL",
"domains.hsts-enabled": "HSTS Enabled",
"domains.hsts-subdomains": "HSTS Sub-domains",
"domains.http2-support": "HTTP/2 Support",
"domains.use-dns": "Use DNS Challenge",
"email-address": "Email address",
"empty-search": "No results found",
"empty-subtitle": "Why don't you create one?",
"enabled": "Enabled",
"error.access.at-least-one": "Either one Authorization or one Access Rule is required",
"error.access.duplicate-usernames": "Authorization Usernames must be unique",
"error.invalid-auth": "Invalid email or password",
"error.invalid-domain": "Invalid domain: {domain}",
"error.invalid-email": "Invalid email address",
"error.max-character-length": "Maximum length is {max} character{max, plural, one {} other {s}}",
"error.max-domains": "Too many domains, max is {max}",
"error.maximum": "Maximum is {max}",
"error.min-character-length": "Minimum length is {min} character{min, plural, one {} other {s}}",
"error.minimum": "Minimum is {min}",
"error.passwords-must-match": "Passwords must match",
"error.required": "This is required",
"expires.on": "Expires: {date}",
"footer.github-fork": "Fork me on Github",
"host.flags.block-exploits": "Block Common Exploits",
"host.flags.cache-assets": "Cache Assets",
"host.flags.preserve-path": "Preserve Path",
"host.flags.protocols": "Protocols",
"host.flags.websockets-upgrade": "Websockets Support",
"host.forward-port": "Forward Port",
"host.forward-scheme": "Scheme",
"hosts": "Hosts",
"http-only": "HTTP Only",
"lets-encrypt": "Let's Encrypt",
"lets-encrypt-via-dns": "Let's Encrypt via DNS",
"lets-encrypt-via-http": "Let's Encrypt via HTTP",
"loading": "Loading…",
"login.title": "Login to your account",
"nginx-config.label": "Custom Nginx Configuration",
"nginx-config.placeholder": "# Enter your custom Nginx configuration here at your own risk!",
"no-permission-error": "You do not have access to view this.",
"notfound.action": "Take me home",
"notfound.content": "We are sorry but the page you are looking for was not found",
"notfound.title": "Oops… You just found an error page",
"notification.error": "Error",
"notification.object-deleted": "{object} has been deleted",
"notification.object-disabled": "{object} has been disabled",
"notification.object-enabled": "{object} has been enabled",
"notification.object-renewed": "{object} has been renewed",
"notification.object-saved": "{object} has been saved",
"notification.success": "Success",
"object.actions-title": "{object} #{id}",
"object.add": "Add {object}",
"object.delete": "Delete {object}",
"object.delete.content": "Are you sure you want to delete this {object}?",
"object.edit": "Edit {object}",
"object.empty": "There are no {objects}",
"object.event.created": "Created {object}",
"object.event.deleted": "Deleted {object}",
"object.event.disabled": "Disabled {object}",
"object.event.enabled": "Enabled {object}",
"object.event.renewed": "Renewed {object}",
"object.event.updated": "Updated {object}",
"offline": "Offline",
"online": "Online",
"options": "Options",
"password": "Password",
"password.generate": "Generate random password",
"password.hide": "Hide Password",
"password.show": "Show Password",
"permissions.hidden": "Hidden",
"permissions.manage": "Manage",
"permissions.view": "View Only",
"permissions.visibility.all": "All Items",
"permissions.visibility.title": "Item Visibility",
"permissions.visibility.user": "Created Items Only",
"proxy-host": "Proxy Host",
"proxy-host.forward-host": "Forward Hostname / IP",
"proxy-hosts": "Proxy Hosts",
"proxy-hosts.count": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}",
"public": "Public",
"redirection-host": "Redirection Host",
"redirection-host.forward-domain": "Forward Domain",
"redirection-hosts": "Redirection Hosts",
"redirection-hosts.count": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}",
"role.admin": "Administrator",
"role.standard-user": "Standard User",
"save": "Save",
"setting": "Setting",
"settings": "Settings",
"settings.default-site": "Default Site",
"settings.default-site.404": "404 Page",
"settings.default-site.444": "No Response (444)",
"settings.default-site.congratulations": "Congratulations Page",
"settings.default-site.description": "What to show when Nginx is hit with an unknown Host",
"settings.default-site.html": "Custom HTML",
"settings.default-site.html.placeholder": "<!-- Enter your custom HTML content here -->",
"settings.default-site.redirect": "Redirect",
"setup.preamble": "Get started by creating your admin account.",
"setup.title": "Welcome!",
"sign-in": "Sign in",
"ssl-certificate": "SSL Certificate",
"stream": "Stream",
"stream.forward-host": "Forward Host",
"stream.incoming-port": "Incoming Port",
"streams": "Streams",
"streams.count": "{count} {count, plural, one {Stream} other {Streams}}",
"streams.tcp": "TCP",
"streams.udp": "UDP",
"test": "Test",
"user": "User",
"user.change-password": "Change Password",
"user.confirm-password": "Confirm Password",
"user.current-password": "Current Password",
"user.edit-profile": "Edit Profile",
"user.full-name": "Full Name",
"user.login-as": "Sign in as {name}",
"user.logout": "Logout",
"user.new-password": "New Password",
"user.nickname": "Nickname",
"user.set-password": "Set Password",
"user.set-permissions": "Set Permissions for {name}",
"user.switch-dark": "Switch to Dark mode",
"user.switch-light": "Switch to Light mode",
"username": "Username",
"users": "Users"
}

View File

@@ -1,3 +0,0 @@
{
"locale-en-US": "English"
}

View File

@@ -0,0 +1,7 @@
## Was ist eine Zugriffsliste?
Zugriffslisten bieten eine Blacklist oder Whitelist mit bestimmten Client-IP-Adressen sowie eine Authentifizierung für die Proxy-Hosts über die grundlegende HTTP-Authentifizierung.
Sie können mehrere Client-Regeln, Benutzernamen und Passwörter für eine einzelne Zugriffsliste konfigurieren und diese dann auf einen oder mehrere Proxy-Hosts anwenden.
Dies ist besonders nützlich für weitergeleitete Webdienste, die keine integrierten Authentifizierungsmechanismen haben, oder wenn Sie sich vor unbekannten Clients schützen möchten.

View File

@@ -0,0 +1,32 @@
## Hilfe zu Zertifikaten
### HTTP-Zertifikat
Ein HTTP-validiertes Zertifikat bedeutet, dass Let's Encrypt-Server
versuchen, Ihre Domains über HTTP (nicht HTTPS!) zu erreichen, und wenn dies erfolgreich ist,
stellen sie Ihr Zertifikat aus.
Für diese Methode müssen Sie einen _Proxy-Host_ für Ihre Domain(s) erstellen, der
über HTTP zugänglich ist und auf diese Nginx-Installation verweist. Nachdem ein Zertifikat
ausgestellt wurde, können Sie den _Proxy-Host_ so ändern, dass dieses Zertifikat auch für HTTPS-Verbindungen
verwendet wird. Der _Proxy-Host_ muss jedoch weiterhin für den HTTP-Zugriff konfiguriert sein,
damit das Zertifikat erneuert werden kann.
Dieser Prozess unterstützt keine Wildcard-Domains.
### DNS-Zertifikat
Für ein DNS-validiertes Zertifikat müssen Sie ein DNS-Provider-Plugin verwenden. Dieser DNS-
Provider wird verwendet, um temporäre Einträge auf Ihrer Domain zu erstellen. Anschließend fragt Let's
Encrypt diese Einträge ab, um sicherzustellen, dass Sie der Eigentümer sind. Bei Erfolg wird
Ihr Zertifikat ausgestellt.
Sie müssen vor der Beantragung dieser Art von Zertifikat keinen _Proxy-Host_ erstellen.
Sie müssen Ihren _Proxy-Host_ auch nicht für den HTTP-Zugriff konfigurieren.
Dieser Prozess unterstützt Wildcard-Domains.
### Benutzerdefiniertes Zertifikat
Verwenden Sie diese Option, um Ihr eigenes SSL-Zertifikat hochzuladen, das Ihnen von Ihrer eigenen
Zertifizierungsstelle bereitgestellt wurde.

View File

@@ -0,0 +1,10 @@
## Was ist ein 404-Host?
Ein 404-Host ist ein Host-Setup, das eine 404-Seite anzeigt.
Dies kann nützlich sein, wenn Ihre Domain in Suchmaschinen gelistet ist und Sie
eine ansprechendere Fehlerseite bereitstellen oder den Suchindexern ausdrücklich mitteilen möchten, dass
die Domain-Seiten nicht mehr existieren.
Ein weiterer Vorteil dieses Hosts besteht darin, dass Sie die Protokolle für Zugriffe darauf verfolgen und
die Verweise anzeigen können.

View File

@@ -0,0 +1,7 @@
## Was ist ein Proxy-Host?
Ein Proxy-Host ist der eingehende Endpunkt für einen Webdienst, den Sie weiterleiten möchten.
Er bietet optionale SSL-Terminierung für Ihren Dienst, der möglicherweise keine integrierte SSL-Unterstützung hat.
Proxy-Hosts sind die häufigste Verwendung für den Nginx Proxy Manager.

View File

@@ -0,0 +1,7 @@
## Was ist ein Redirection Host?
Ein Redirection Host leitet Anfragen von der eingehenden Domain weiter und leitet den
Besucher zu einer anderen Domain weiter.
Der häufigste Grund für die Verwendung dieses Host-Typs ist, wenn Ihre Website die
Domain wechselt, aber Sie noch Suchmaschinen- oder Referrer-Links haben, die auf die alte Domain verweisen.

View File

@@ -0,0 +1,6 @@
## Was ist ein Stream?
Ein Stream ist eine relativ neue Funktion von Nginx, die dazu dient, TCP/UDP-Datenverkehr
direkt an einen anderen Computer im Netzwerk weiterzuleiten.
Wenn Sie Spielserver, FTP- oder SSH-Server betreiben, kann dies sehr nützlich sein.

View File

@@ -0,0 +1,6 @@
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";

View File

@@ -0,0 +1,7 @@
## ¿Qué es una Lista de Acceso?
Las Listas de Acceso proporcionan una lista negra o blanca de direcciones IP de cliente específicas junto con autenticación para los Hosts Proxy a través de Autenticación HTTP Básica.
Puede configurar múltiples reglas de cliente, nombres de usuario y contraseñas para una única Lista de Acceso y luego aplicarla a uno o más _Hosts Proxy_.
Esto es más útil para servicios web reenviados que no tienen mecanismos de autenticación integrados o cuando desea protegerse de clientes desconocidos.

View File

@@ -0,0 +1,32 @@
## Ayuda de Certificados
### Certificado HTTP
Un certificado validado por HTTP significa que los servidores de Let's Encrypt
intentarán acceder a tus dominios a través de HTTP (¡no HTTPS!) y, si tienen éxito,
emitirán tu certificado.
Para este método, deberás tener un _Host Proxy_ creado para tu(s) dominio(s) que
sea accesible por HTTP y que apunte a esta instalación de Nginx. Después de que se
haya emitido un certificado, puedes modificar el _Host Proxy_ para que también use
este certificado para conexiones HTTPS. Sin embargo, el _Host Proxy_ seguirá
necesitando estar configurado para acceso HTTP para que el certificado se renueve.
Este proceso _no_ admite dominios comodín.
### Certificado DNS
Un certificado validado por DNS requiere que uses un complemento de Proveedor de DNS.
Este Proveedor de DNS se usará para crear registros temporales en tu dominio y luego
Let's Encrypt consultará esos registros para asegurarse de que eres el propietario y,
si tiene éxito, emitirá tu certificado.
No necesitas tener un _Host Proxy_ creado antes de solicitar este tipo de certificado.
Tampoco necesitas tener tu _Host Proxy_ configurado para acceso HTTP.
Este proceso __ admite dominios comodín.
### Certificado Personalizado
Usa esta opción para cargar tu propio Certificado SSL, proporcionado por tu propia
Autoridad de Certificación.

View File

@@ -0,0 +1,10 @@
## ¿Qué es un Host 404?
Un Host 404 es simplemente una configuración de host que muestra una página 404.
Esto puede ser útil cuando tu dominio está listado en los motores de búsqueda y deseas
proporcionar una página de error más agradable o específicamente para indicar a los indexadores de búsqueda que
las páginas del dominio ya no existen.
Otro beneficio de tener este host es rastrear los registros de visitas a él y
ver los referentes.

View File

@@ -0,0 +1,7 @@
## ¿Qué es un Host Proxy?
Un Host Proxy es el punto de entrada para un servicio web que deseas reenviar.
Proporciona terminación SSL opcional para tu servicio que podría no tener soporte SSL integrado.
Los Hosts Proxy son el uso más común del Nginx Proxy Manager.

View File

@@ -0,0 +1,7 @@
## ¿Qué es un Host de Redirección?
Un Host de Redirección redirigirá las solicitudes del dominio entrante e impulsará al
visitante a otro dominio.
La razón más común para usar este tipo de host es cuando tu sitio web cambia de
dominios pero aún tienes enlaces de motores de búsqueda o referencias apuntando al dominio anterior.

View File

@@ -0,0 +1,6 @@
## ¿Qué es un Stream?
Una característica relativamente nueva para Nginx, un Stream servirá para reenviar tráfico TCP/UDP
directamente a otra computadora en la red.
Si estás ejecutando servidores de juegos, FTP o servidores SSH esto puede ser muy útil.

View File

@@ -0,0 +1,6 @@
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";

View File

@@ -1,8 +1,12 @@
// import * as de from "./de/index"; import * as de from "./de/index";
// import * as fa from "./fa/index";
import * as en from "./en/index"; import * as en from "./en/index";
import * as ja from "./ja/index";
import * as pl from "./pl/index";
import * as ru from "./ru/index";
import * as sk from "./sk/index";
import * as zh from "./zh/index";
const items: any = { en }; const items: any = { en, de, ja, sk, zh, pl, ru };
const fallbackLang = "en"; const fallbackLang = "en";

View File

@@ -0,0 +1,8 @@
## アクセスリストとは
アクセスリストは特定のクライアントIPへのブラックリストとホワイトリストを提供し、ベーシック認証によるプロキシホストへの認証を可能にします。
複数のクライアントルールやユーザー名とパスワードを一つのアクセスリストに設定し、一つ以上の _プロキシホスト_ に適応することができます。
これは認証システムを持たないサービスや不明なクライアントからの保護が必要な場合に有効です。

View File

@@ -0,0 +1,21 @@
## 証明書
### HTTP 証明書
HTTPによって検証された証明書はLet's EncryptサーバーがHTTPでドメインにアクセスを試みサーバーを管理していることを確認できた場合に発行される証明書です。
この方法では、HTTPアクセス可能でこのNginxを指しているドメインに対して _プロキシホスト_ を作成する必要があります。証明書が発行された後は、 _プロキシホスト_ を編集してこの証明書をHTTPS接続に使用するように設定できます。ただし、証明書の更新には、_プロキシホスト_ がHTTP接続用に設定された状態を維持する必要があります。
この方法はワイルドカードのドメインをサポート _していません_
### DNS 証明書
DNSによって検証された証明書にはDNSプロバイダープラグインが必要です。このプロバイダーはドメイン上に一時レコードを作成するために使用されます。その後Let's Encryptサーバーがそのレコードを参照し、あなたが所有していることを確認できると証明書が発行されます。
このタイプの証明書を作成する際に、 _プロキシホスト_ を作成する必要はありません。また、_プロキシホスト_ をHTTPアクセス用に設定する必要もありません。
この方法はワイルドカードのドメインをサポート _します_
### カスタム証明書
このオプションでは、あなたの証明書認証局によって提供された自身の証明書をアップロードして使用できます。

View File

@@ -0,0 +1,7 @@
## 404ホストとはなんですか?
404ホストとは、単に404ページを表示するよう設定されたホストです。
これは、検索エンジンに登録されたドメインに分かりやすいエラーページを提供したい場合や、検索エンジンのインデクサーにドメインページがもう存在しないことを伝えたい場合に便利です。
このホストを持つもう一つの利点は、アクセスログを追跡し、参照元を確認できることです。

View File

@@ -0,0 +1,7 @@
## プロキシホストとは何ですか?
プロキシホストは転送したいwebサービスの受信エンドポイントです。
サービスにSSLサーバーが組み込まれていない場合でも、オプションでSSL終端機能を提供します。
プロキシホストはNginx Proxy Managerのもっとも一般的な使用方法です。

View File

@@ -0,0 +1,5 @@
## リダイレクトホストとは何ですか?
リダイレクトホストは受信したリクエストを別のドメインにリダイレクトして訪問者に表示します。
このタイプのもっとも一般的な使用理由は、webサイトのドメインが変更されたが検索エンジンやリンクが古いドメインを指し続けている場合です。

View File

@@ -0,0 +1,5 @@
## ストリームとは何ですか?
Nginxの比較的新しい機能であるストリームは、TCP/UDPトラフィックをネットワーク上の別のコンピュータに直接転送します。
ゲームサーバー、FTPサーバー、SSHサーバーを運用している場合に便利です。

View File

@@ -0,0 +1,6 @@
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";

View File

@@ -0,0 +1,7 @@
## Czym jest lista dostępu?
Listy dostępu zapewniają czarną lub białą listę określonych adresów IP klientów wraz z uwierzytelnianiem dla hostów proxy za pomocą podstawowego uwierzytelniania HTTP.
Możesz skonfigurować wiele reguł klienta, nazw użytkowników i haseł dla pojedynczej listy dostępu, a następnie zastosować ją do jednego lub więcej hostów proxy.
Jest to najbardziej przydatne w przypadku przekierowywanych usług internetowych, które nie mają wbudowanych mechanizmów uwierzytelniania lub gdy chcesz zabezpieczyć się przed nieznanymi klientami.

View File

@@ -0,0 +1,22 @@
## Pomoc dotycząca certyfikatów
### Certyfikat HTTP
Certyfikat weryfikowany przez HTTP oznacza, że serwery Let's Encrypt będą próbowały połączyć się z twoimi domenami przez HTTP (nie HTTPS!) i jeśli się to powiedzie, wydadzą twój certyfikat.
W przypadku tej metody musisz mieć utworzony Host proxy dla swoich domen, który jest dostępny przez HTTP i wskazuje na tę instalację Nginx.
Po otrzymaniu certyfikatu możesz zmodyfikować Host proxy, aby używał również tego certyfikatu do połączeń HTTPS. Jednak Host proxy nadal będzie musiał być skonfigurowany do dostępu przez HTTP, aby certyfikat mógł być odnawiany.
Ten proces nie obsługuje domen wieloznacznych (wildcard).
### Certyfikat DNS
Certyfikat weryfikowany przez DNS wymaga użycia wtyczki dostawcy DNS. Ten dostawca DNS zostanie użyty do utworzenia tymczasowych rekordów w twojej domenie, a następnie Let's Encrypt sprawdzi te rekordy, aby upewnić się, że jesteś właścicielem i jeśli się powiedzie, wydadzą twój certyfikat.
Nie musisz mieć utworzonego Hosta proxy przed wystąpieniem o ten typ certyfikatu. Nie musisz również mieć skonfigurowanego Hosta proxy do dostępu przez HTTP.
Ten proces obsługuje domeny wieloznaczne (wildcard).
### Własny certyfikat
Użyj tej opcji, aby przesłać własny certyfikat SSL, dostarczony przez twój własny urząd certyfikacji.

View File

@@ -0,0 +1,7 @@
## Czym jest host 404?
Host 404 to po prostu konfiguracja hosta, która wyświetla stronę 404.
Może to być przydatne, gdy twoja domena jest indeksowana w wyszukiwarkach i chcesz zapewnić ładniejszą stronę błędu lub konkretnie poinformować roboty indeksujące, że strony domeny już nie istnieją.
Kolejną zaletą posiadania tego hosta jest możliwość śledzenia logów dla odwiedzin oraz przeglądania źródeł ruchu (referrerów).

View File

@@ -0,0 +1,7 @@
## Czym jest host proxy?
Host proxy to punkt wejściowy dla usługi internetowej, którą chcesz przekierować.
Zapewnia opcjonalne zakończenie SSL dla twojej usługi, która może nie mieć wbudowanej obsługi SSL.
Hosty proxy są najpopularniejszym zastosowaniem Nginx Proxy Manager

View File

@@ -0,0 +1,5 @@
## Czym jest host przekierowania?
Host przekierowania przekierowuje żądania z domeny przychodzącej i przenosi odwiedzającego na inną domenę.
Najczęstszym powodem używania tego typu hosta jest sytuacja, gdy twoja strona internetowa zmienia domeny, ale nadal masz linki z wyszukiwarek lub odnośniki wskazujące na starą domenę.

View File

@@ -0,0 +1,5 @@
## Czym jest strumień?
Stosunkowo nowa funkcja dla Nginx, strumień służy do przekazywania ruchu TCP/UDP bezpośrednio na inny komputer/serwer w sieci.
Jeśli prowadzisz serwery gier, FTP lub SSH, może się to okazać przydatne

View File

@@ -0,0 +1,6 @@
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";

View File

@@ -0,0 +1,7 @@
## Что такое список доступа?
Списки доступа позволяют задавать белый/чёрный список IPадресов клиентов и настраивать аутентификацию для прокси‑хостов через базовую HTTPаутентификацию.
Для одного списка доступа можно настроить несколько правил клиентов, логины и пароли, а затем применить его к одному или нескольким _прокси‑хостам_.
Это особенно полезно для проксируемых веб‑сервисов без встроенной аутентификации или когда нужно защититься от неизвестных клиентов.

View File

@@ -0,0 +1,21 @@
## Справка по сертификатам
### HTTP-сертификат
Сертификат, подтверждённый по HTTP, означает, что серверы Let's Encrypt попытаются обратиться к вашим доменам по HTTP (не HTTPS!) и при успехе выпустят сертификат.
Для этого метода должен существовать _прокси‑хост_ для ваших доменов, доступный по HTTP и указывающий на эту установку Nginx. После выдачи сертификата вы можете настроить _прокси‑хост_ на использование этого сертификата для HTTPSподключений. Однако доступ по HTTP должен сохраняться, чтобы сертификат мог обновляться.
Этот способ _не_ поддерживает wildcardдомены.
### DNS-сертификат
Сертификат, подтверждённый по DNS, требует использования плагина DNSпровайдера. Такой провайдер создаст временные записи в вашем домене, затем Let's Encrypt проверит эти записи, чтобы убедиться, что вы владелец домена, и при успехе выпустит сертификат.
Для запроса такого сертификата предварительно создавать _прокси‑хост_ не требуется. Также не нужен доступ по HTTP для вашего _прокси‑хоста_.
Этот способ _поддерживает_ wildcardдомены.
### Свой сертификат
Используйте этот вариант, чтобы загрузить собственный SSLсертификат, выданный вашим удостоверяющим центром (CA).

View File

@@ -0,0 +1,7 @@
## Что такое 404хост?
404хост — это конфигурация, которая показывает страницу 404.
Это полезно, когда ваш домен присутствует в поисковых системах и вы хотите показать более дружелюбную страницу ошибки или явно сообщить индексаторам, что страницы домена больше не существуют.
Ещё одно преимущество — можно отдельно отслеживать обращения в журналах и смотреть источники переходов.

View File

@@ -0,0 +1,7 @@
## Что такое прокси‑хост?
Прокси‑хост — это входная точка веб‑сервиса, который вы проксируете.
Он может выполнять терминaцию SSL для сервиса, у которого нет собственной поддержки SSL.
Прокси‑хосты — самый распространённый сценарий использования Nginx Proxy Manager.

View File

@@ -0,0 +1,5 @@
## Что такое редирект‑хост?
Редирект‑хост перенаправляет запросы, поступающие на входящий домен, на другой домен.
Чаще всего это используют, когда сайт сменил домен, а в поиске или на сторонних ресурсах всё ещё остаются ссылки на старый домен.

View File

@@ -0,0 +1,5 @@
## Что такое поток?
Относительно новая возможность Nginx: поток позволяет напрямую проксировать TCP/UDPтрафик на другой компьютер в сети.
Полезно для игровых серверов, FTP или SSHсерверов.

View File

@@ -0,0 +1,6 @@
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";

View File

@@ -0,0 +1,7 @@
## Čo je zoznam prístupov?
Zoznamy prístupov poskytujú čiernu alebo bielu listinu konkrétnych IP adries klientov spolu s overovaním pre proxy hostiteľov prostredníctvom základného overovania HTTP.
Môžete nakonfigurovať viacero pravidiel pre klientov, používateľských mien a hesiel pre jeden zoznam prístupov a potom ho použiť na jeden alebo viacero proxy hostiteľov.
Toto je najužitočnejšie pre presmerované webové služby, ktoré nemajú zabudované overovacie mechanizmy, alebo ak sa chcete chrániť pred neznámymi klientmi.

View File

@@ -0,0 +1,32 @@
## Pomoc s certifikátmi
### Certifikát HTTP
Certifikát overený protokolom HTTP znamená, že servery Let's Encrypt sa
pokúsia pripojiť k vašim doménam cez protokol HTTP (nie HTTPS!) a v prípade úspechu
vydajú váš certifikát.
Pre túto metódu budete musieť mať pre svoje domény vytvorený _Proxy Host_, ktorý
je prístupný cez HTTP a smeruje na túto inštaláciu Nginx. Po vydaní certifikátu
môžete zmeniť _Proxy Host_ tak, aby tento certifikát používal aj pre HTTPS
pripojenia. _Proxy Host_ však bude stále potrebné nakonfigurovať pre prístup cez HTTP,
aby sa certifikát mohol obnoviť.
Tento proces _nepodporuje_ domény s divokými kartami.
### Certifikát DNS
Certifikát overený DNS vyžaduje použitie pluginu DNS Provider. Tento DNS
Provider sa použije na vytvorenie dočasných záznamov vo vašej doméne a potom Let's
Encrypt overí tieto záznamy, aby sa uistil, že ste vlastníkom, a ak bude úspešný,
vydá váš certifikát.
Pred požiadaním o tento typ certifikátu nie je potrebné vytvoriť _Proxy Host_.
Tiež nie je potrebné mať _Proxy Host_ nakonfigurovaný pre prístup HTTP.
Tento proces _podporuje_ domény s divokými kartami.
### Vlastný certifikát
Túto možnosť použite na nahratie vlastného SSL certifikátu, ktorý vám poskytla vaša
certifikačná autorita.

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