Compare commits

...

188 Commits

Author SHA1 Message Date
jc21
f9edcb10e6 Merge pull request #4987 from Bare7a/patch-1
Update Locale README.md to include HelpDoc/index.tsx
2025-11-26 08:35:54 +10:00
jc21
896951f6cd Merge pull request #4985 from Bare7a/bg-locale
Add Bulgarian Language Support
2025-11-26 08:33:55 +10:00
jc21
865b566ea6 Merge pull request #4989 from alatalo/develop
Add Glesys certbot plugin
2025-11-26 08:32:03 +10:00
Ville Alatalo
45bc44c6fa Add Glesys certbot plugin 2025-11-25 07:49:24 +02:00
Bare7a
4ff402fff4 Update Locale README.md to include HelpDoc/index.tsx 2025-11-24 18:28:49 +02:00
Bare7a
1c6f54fa3c Changed the port translation 2025-11-24 18:23:40 +02:00
Bare7a
e8ca72fb6a Adds bg inside HelpDoc index.ts file 2025-11-24 18:14:16 +02:00
Bare7a
4712633568 After Translate 2025-11-24 18:07:46 +02:00
Bare7a
a1fb54c394 Before Translating 2025-11-24 18:04:50 +02:00
jc21
0528d65317 Merge pull request #4964 from xluyenx/develop
All checks were successful
Close stale issues and PRs / stale (push) Successful in 21s
Correct Vietnam flag
2025-11-20 11:54:55 +10:00
jc21
f9991084fc Merge pull request #4966 from 7heMech/7heMech-patch-1
Increase max propagation seconds to 7200
2025-11-20 11:54:15 +10:00
7heMech
20e2d5ffb3 Increase max propagation seconds to 7200 2025-11-19 13:00:06 +02:00
Louis Tran's
e3cdc8bb30 Update IntlProvider.tsx 2025-11-19 11:37:20 +07:00
Louis Tran's
ba79eefe5e Merge pull request #1 from xluyenx/xluyenx-patch-1
Update IntlProvider.tsx
2025-11-19 11:30:49 +07:00
Louis Tran's
bb94ce75c1 Update IntlProvider.tsx
Correct Vietnam flag
2025-11-19 11:27:42 +07:00
jc21
89b8b747e1 Merge branch 'master' into develop
All checks were successful
Close stale issues and PRs / stale (push) Successful in 27s
2025-11-18 19:46:03 +10:00
Jamie Curnow
3231023513 Bump version 2025-11-18 19:42:54 +10:00
Jamie Curnow
dc89635971 Fix up locales, optimised some functions 2025-11-18 19:38:21 +10:00
jc21
cfa98361d1 Merge pull request #4955 from NginxProxyManager/lang-nl
Add Dutch language - resolves #4935
2025-11-18 19:03:48 +10:00
Jelcoo
c2177abe39 Add language to frontend settings & correct some translations 2025-11-18 19:00:00 +10:00
Jelcoo
2c6d614597 Add HelpDoc translations 2025-11-18 18:58:26 +10:00
Jelcoo
484ce8db3c Add Dutch language 2025-11-18 18:57:40 +10:00
jc21
2c11c0c7e2 Merge pull request #4937 from archettitechnology/develop
Add Italian Language Support
2025-11-18 18:50:52 +10:00
jc21
f1039ce2ef Merge pull request #4928 from 7heMech/develop
UI/UX improvements
2025-11-18 18:37:22 +10:00
jc21
d49ff6e7c2 Merge pull request #4934 from zdzichu6969/develop
fix(i18n): replace "Dodaj" with "Nowy" for better Polish grammar and typo Role
2025-11-18 18:30:24 +10:00
jc21
a87f24c9dc Merge pull request #4940 from vsc55/issues_4939
Fix issues #4939, #4938
2025-11-18 18:29:04 +10:00
jc21
decdfec447 Merge branch 'develop' into develop 2025-11-18 18:27:00 +10:00
jc21
32ab3faf57 Merge pull request #4943 from NginxProxyManager/dependabot/npm_and_yarn/backend/js-yaml-4.1.1
Bump js-yaml from 4.1.0 to 4.1.1 in /backend
2025-11-18 18:24:31 +10:00
jc21
c7f999fa7a Merge pull request #4944 from gjssss/patch-1
Fix message for GitHub fork reference in zh.json
2025-11-18 18:24:14 +10:00
jc21
de7d3b0d19 Merge pull request #4950 from dominhhieu1405/develop
Add Vietnamese Support
2025-11-18 18:22:43 +10:00
jc21
2d4b7399c0 Merge pull request #4953 from dodog/develop
Update Slovak language label
2025-11-18 18:20:03 +10:00
Jamie Curnow
316b758455 Tweaks to cypress suite
All checks were successful
Close stale issues and PRs / stale (push) Successful in 20s
2025-11-18 07:21:06 +10:00
Jozef Gaal
890d06c863 Update Slovak language label 2025-11-17 21:07:56 +01:00
dominhhieu1405
81f2aa17d4 Add vietnamese 2025-11-17 22:28:08 +07:00
Jamie Curnow
9b4c34915c Update porkbun certbot plugin
All checks were successful
Close stale issues and PRs / stale (push) Successful in 21s
2025-11-17 08:46:31 +10:00
Javier Pastor
fce569ca21 Modify host.forward-port to avoid line breaks 2025-11-16 01:53:48 +01:00
Json Gao
87ec9c4bdf Fix message for GitHub fork reference in zh.json 2025-11-15 20:09:19 +08:00
dependabot[bot]
2650648d68 Bump js-yaml from 4.1.0 to 4.1.1 in /backend
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-15 10:40:46 +00:00
7heMech
fdc0c29f28 Improve modals in dark mode via a dark backdrop and shadow. 2025-11-14 15:51:54 +02:00
angioletto
6cae088432 Rename ProxyHost.md to ProxyHosts.md
i think have problem with letter s ahaha
2025-11-14 08:16:41 +01:00
angioletto
9d8c4cc30b Rename DeadHost.md to DeadHosts.md 2025-11-14 08:14:26 +01:00
angioletto
66ebecdb43 Merge branch 'develop' into develop 2025-11-14 08:01:32 +01:00
angioletto
60f3ee03c0 Fix typo in file name from 'indes.ts' to 'index.ts'
typing error
2025-11-14 08:00:30 +01:00
jc21
a4d54a0291 Merge pull request #4932 from kraineff/develop
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
angioletto
7536b1b1c9 Merge branch 'develop' into develop 2025-11-14 07:19:32 +01:00
angioletto
5288fbd7af Update index.ts 2025-11-14 07:18:14 +01:00
jc21
2c630bbdca Merge branch 'develop' into develop 2025-11-14 15:25:10 +10:00
Javier Pastor
0ec1a09c30 fix issues #4939
add other translations
2025-11-14 00:06:18 +01:00
Jamie Curnow
118c4793e3 Amend locale readme 2025-11-14 08:34:16 +10:00
Jamie Curnow
d7384c568f Fix #4933 when cert may not have domain names 2025-11-14 08:33:42 +10:00
angioletto
0bcfe0bba6 Add Italian language support to lang-list.json 2025-11-13 21:12:52 +01:00
angioletto
74cbfb2c58 Create indes.ts to export HelpDoc modules 2025-11-13 21:12:15 +01:00
angioletto
8ef65caa5a Add Italian documentation for Streams feature 2025-11-13 21:11:19 +01:00
angioletto
bc341c1dff Add RedirectionHosts.md with explanation in Italian 2025-11-13 21:10:36 +01:00
angioletto
5fc9febf1f Update title of ProxyHost.md in Italian 2025-11-13 21:09:40 +01:00
angioletto
b23ceebfd8 Add Italian documentation for ProxyHost 2025-11-13 21:09:23 +01:00
angioletto
c281fc54a1 Add Italian HelpDoc for 404 Host explanation 2025-11-13 21:08:50 +01:00
angioletto
d0f7dc5b48 Add Italian HelpDoc for certificate options 2025-11-13 21:07:26 +01:00
angioletto
fb53df862e Add Italian documentation for Access Lists 2025-11-13 21:03:33 +01:00
angioletto
8d8463ae41 Add Italian language support to HelpDoc 2025-11-13 20:57:52 +01:00
angioletto
8774cfe5f9 Add Italian locale to check-locales 2025-11-13 20:56:42 +01:00
angioletto
4ca5cadd19 Add Italian language support to IntlProvider 2025-11-13 20:55:35 +01:00
angioletto
45a8d50e03 Add IT Translation 2025-11-13 20:52:42 +01:00
7heMech
960d4bfe6f Revert change which should have no effect on theory 2025-11-13 14:51:00 +02:00
7heMech
8c3c964c52 Fix page offset 2025-11-13 14:27:55 +02:00
7heMech
afd6134a3e Get rid of logo flicker and improve LCP 2025-11-13 14:04:37 +02:00
Alexey Krainev
9b2d60e67b Update Russian locale 2025-11-13 16:58:04 +05:00
7heMech
9807e25d45 Remove unused import 2025-11-13 12:49:48 +02:00
7heMech
824c895f52 Remove cn where not needed 2025-11-13 12:47:01 +02:00
7heMech
7f9b9dfea4 Fix for dropdown menus being clipped by table-responsive containers. 2025-11-13 12:06:36 +02:00
Mateusz Gruszczyński
d848ba9f65 Fixed typo: corrected 'role' to proper Polish declension 'rola' and 'nowy' 2025-11-13 09:05:07 +01:00
Mateusz Gruszczyński
47db5c9aa6 Fixed typo: corrected 'role' to proper Polish declension 'rola' 2025-11-13 08:57:30 +01:00
Jamie Curnow
79a9653b26 Remove the compiled lang files, compile on dev server and when building in ci
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
7heMech
1c442dcce6 True mobile layout with responsive table rows (sticky header) 2025-11-13 02:44:24 +02:00
7heMech
dadd10f89b Fixed my troubles with text wrap 2025-11-13 02:21:58 +02:00
jc21
8838dabe8a Merge pull request #4906 from sopex/develop
Available upgrade notification
2025-11-13 10:15:33 +10:00
7heMech
75c012b558 Fix linter error 2025-11-13 01:58:48 +02:00
7heMech
9be1381ffe Uhhh, I didn't like the Standard User lol 2025-11-13 01:46:39 +02:00
7heMech
f40fe56572 Add new section with theme and locale pickers. 2025-11-13 01:40:34 +02:00
Konstantinos Spartalis
b4fd242eb7 remove 1 2025-11-13 00:48:49 +02:00
7heMech
911476f82f Delay before close for smooth feel. 2025-11-13 00:46:36 +02:00
7heMech
963125f963 Space scandal retified (hopefully) 2025-11-13 00:45:07 +02:00
7heMech
e86a34f2f3 Close menu after navigation. 2025-11-13 00:30:45 +02:00
jc21
6ce9567e48 Merge pull request #4816 from fhennig42/azure-dns
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
jc21
f02145c5ef Merge pull request #4925 from NginxProxyManager/develop
v2.13.4
2025-11-13 06:57:28 +10:00
7heMech
66fa08fd8e Add profile back to main app on mobile 2025-11-12 18:12:58 +02:00
7heMech
d783cc3b90 Remove unused styles 2025-11-12 17:58:54 +02:00
7heMech
17cc75fe7d Fix language and theme selectors on mobile and desktop 2025-11-12 17:43:46 +02:00
Konstantinos Spartalis
15394c6532 trigger Jenkins that failed due to internet connection problems 2025-11-12 15:50:11 +02:00
Konstantinos Spartalis
2d6252d75d https.get 2025-11-12 15:45:59 +02:00
jc21
adee0e39de Merge branch 'master' into develop 2025-11-12 23:02:28 +10:00
Jamie Curnow
5dde98cf3e Updates to polish locale after running through automated scripts 2025-11-12 23:01:40 +10:00
jc21
c41451618e Merge pull request #4924 from zdzichu6969/develop
Add Polish locale
2025-11-12 22:59:23 +10:00
jc21
1a3d45f6bc Merge branch 'develop' into develop 2025-11-12 22:14:28 +10:00
jc21
2ea54975b6 Merge pull request #4922 from NginxProxyManager/dodog-slovak
Add Slovak language by @dodog in #4911
2025-11-12 22:13:05 +10:00
Mateusz Gruszczyński
0373017a9f Add Polish locale 2025-11-12 13:10:29 +01:00
Florian Hennig
b043e70fc0 add azure-mgmt-dns fix version as dependency 2025-11-12 13:00:34 +01:00
Jamie Curnow
2b5182d339 Add Slovak language by @dodog in #4911 2025-11-12 21:49:04 +10:00
jc21
3c5ff81a54 Merge pull request #4910 from 7heMech/develop
Add scheme back in destination
2025-11-12 20:48:56 +10:00
jc21
8aa46c1f40 Merge pull request #4921 from NginxProxyManager/Firfr-chinese
Add Chinese language 添加中文
2025-11-12 20:47:15 +10:00
Jamie Curnow
b26db50ae7 Adds cn to check locales script 2025-11-12 20:26:22 +10:00
firfe
d66bb2104a Add the new translation for "redirection-host.forward-http-code". 2025-11-12 20:23:36 +10:00
firfe
8e900dbc92 Add Chinese HelpDoc 2025-11-12 20:23:34 +10:00
firfe
66aac3eb3e Add Chinese 中文 2025-11-12 20:22:57 +10:00
jc21
221c3eddbc Merge pull request #4919 from lastsamurai26/develop
Fix: German grammatical change
2025-11-12 20:16:58 +10:00
Jamie Curnow
8460b28597 Bump version 2025-11-12 20:13:18 +10:00
Frank
0344bb3c19 fix: Grammatical change
fix: Grammatical change
2025-11-12 10:47:53 +01:00
Frank
1a36bdce76 fix: Grammatical change
fix: Grammatical change
2025-11-12 10:47:51 +01:00
Jamie Curnow
06d7db43f7 Fix Russion locale, compiled file was comitted without a source file 2025-11-12 18:59:37 +10:00
jc21
4557244744 Merge pull request #4870 from kraineff/develop
Add Russian Support
2025-11-12 18:51:43 +10:00
jc21
f649288098 Merge branch 'develop' into develop 2025-11-12 18:39:05 +10:00
jc21
28df6db52b Merge pull request #4848 from Oka-Tak/develop
Add Japanese language support and translations
2025-11-12 18:36:18 +10:00
jc21
eee749652c Merge pull request #4917 from lastsamurai26/develop
Fix: wrong translate and adding missing translations
2025-11-12 18:13:08 +10:00
jc21
f6aa25b9b3 Merge branch 'develop' into develop 2025-11-12 18:12:10 +10:00
Frank
40db26b686 Merge branch 'NginxProxyManager:develop' into develop 2025-11-12 08:06:36 +01:00
Frank
f36d4e6906 Fix: CustomCertificateModal Wrong displayname
Fix: https://github.com/NginxProxyManager/nginx-proxy-manager/issues/4912 Wrong Locale for Custom
2025-11-12 07:47:06 +01:00
Frank
86c7cbddab Add certificate renewal message in German locale
Fix: add missing translation for renew certificates
2025-11-12 07:34:44 +01:00
Frank
e52975bf6c Translate 'Renew Certificate' to German
Fix: add missing translation for renew certificates
2025-11-12 07:34:42 +01:00
Frank
ff792f76af Add translation for 'Renew Certificate' in de.json
Fix: Add missing translation für renew Certificate
2025-11-12 07:32:34 +01:00
Jamie Curnow
711f312b71 Fix up language inconsistenties 2025-11-12 16:30:22 +10:00
Jamie Curnow
9f0f89ff03 Fix wrong translation for EN 2025-11-12 15:13:14 +10:00
jc21
f3633cb696 Merge pull request #4850 from TeenBiscuits/lang-spanish
Add Spanish language support and translations
2025-11-12 15:12:28 +10:00
Pablo Portas López
8773ce25d7 Merge branch 'develop' into lang-spanish 2025-11-12 02:14:09 +01:00
jc21
c3954e9845 Merge pull request #4824 from lastsamurai26/develop
Add German Support
2025-11-12 08:52:07 +10:00
Konstantinos Spartalis
87eef10ff8 remove useCallback logic 2025-11-11 18:30:23 +02:00
Konstantinos Spartalis
dc03ad8239 minimal changes 2025-11-11 17:42:46 +02:00
7heMech
441a7262cd Add scheme back in destination 2025-11-11 12:54:01 +00:00
Pablo Portas López
1600599410 Fix column.http-code translation 2025-11-11 13:53:45 +01:00
Pablo Portas López
74d381e7fa Add missing spanish translation 2025-11-11 13:50:23 +01:00
Konstantinos Spartalis
ae5faa75fa backend test 2025-11-11 10:35:00 +02:00
Frank
ba79bbc750 Update German translation for HTTP code
fix: Updated column http code
2025-11-11 08:56:32 +01:00
Frank
a7231777aa FIX: Update HTTP code message in German locale
fix: Updated column http code
2025-11-11 08:56:20 +01:00
jc21
2578105f86 Merge pull request #4907 from NginxProxyManager/develop
v2.13.3
2025-11-11 16:54:38 +10:00
Frank
3a6b221b0c Add HTTP Code translation to German locale
new: redirection-host.forward-http-code added
2025-11-11 07:13:13 +01:00
Frank
12b000abb9 Add HTTP Code message to German locale
new: redirection-host.forward-http-code added
2025-11-11 07:12:57 +01:00
jc21
39c9bbb167 Merge branch 'master' into develop
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
176 changed files with 8834 additions and 776 deletions

View File

@@ -1 +1 @@
2.13.1
2.13.5

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">
<img src="https://nginxproxymanager.com/github.png">
<br><br>
<img src="https://img.shields.io/badge/version-2.13.1-green.svg?style=for-the-badge">
<img src="https://img.shields.io/badge/version-2.13.5-green.svg?style=for-the-badge">
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager">
<img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge">
</a>

View File

@@ -26,8 +26,8 @@
"azure": {
"name": "Azure",
"package_name": "certbot-dns-azure",
"version": "~=1.2.0",
"dependencies": "",
"version": "~=2.6.1",
"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",
"full_plugin_name": "dns-azure"
},
@@ -255,6 +255,14 @@
"credentials": "dns_gcore_apitoken = 0123456789abcdef0123456789abcdef01234567",
"full_plugin_name": "dns-gcore"
},
"glesys": {
"name": "Glesys",
"package_name": "certbot-dns-glesys",
"version": "~=2.1.0",
"dependencies": "",
"credentials": "dns_glesys_user = CL00000\ndns_glesys_password = apikeyvalue",
"full_plugin_name": "dns-glesys"
},
"godaddy": {
"name": "GoDaddy",
"package_name": "certbot-dns-godaddy",
@@ -370,7 +378,7 @@
"leaseweb": {
"name": "LeaseWeb",
"package_name": "certbot-dns-leaseweb",
"version": "~=1.0.1",
"version": "~=1.0.3",
"dependencies": "",
"credentials": "dns_leaseweb_api_token = 01234556789",
"full_plugin_name": "dns-leaseweb"
@@ -399,6 +407,14 @@
"credentials": "dns_luadns_email = user@example.com\ndns_luadns_token = 0123456789abcdef0123456789abcdef",
"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": {
"name": "mijn.host",
"package_name": "certbot-dns-mijn-host",
@@ -474,7 +490,7 @@
"porkbun": {
"name": "Porkbun",
"package_name": "certbot-dns-porkbun",
"version": "~=0.9",
"version": "~=0.11.0",
"dependencies": "",
"credentials": "dns_porkbun_key=your-porkbun-api-key\ndns_porkbun_secret=your-porkbun-api-secret",
"full_plugin_name": "dns-porkbun"

View File

@@ -1,6 +1,8 @@
import knex from "knex";
import {configGet, configHas} from "./lib/config.js";
let instance = null;
const generateDbConfig = () => {
if (!configHas("database")) {
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) {
//logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2));
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 setup = new signale.Signale({ scope: "Setup ", ...opts });
const ipRanges = new signale.Signale({ scope: "IP Ranges", ...opts });
const remoteVersion = new signale.Signale({ scope: "Remote Version", ...opts });
const debug = (logger, ...args) => {
if (isDebugMode()) {
@@ -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";
const migrateUp = async () => {
const version = await db.migrate.currentVersion();
const version = await db().migrate.currentVersion();
logger.info("Current database version:", version);
return await db.migrate.latest({
return await db().migrate.latest({
tableName: "migrations",
directory: "migrations",
});

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 User from "./user.js";
Model.knex(db);
Model.knex(db());
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 now from "./now_helper.js";
Model.knex(db);
Model.knex(db());
class AccessListAuth extends Model {
$beforeInsert() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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"
}
},
"/version/check": {
"get": {
"$ref": "./paths/version/check/get.json"
}
},
"/users": {
"get": {
"$ref": "./paths/users/get.json"

View File

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

View File

@@ -4,7 +4,7 @@
auth_basic "Authorization required";
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 "";
{% endif %}

View File

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

View File

@@ -4,7 +4,6 @@
# This file assumes that the frontend has been built using ./scripts/frontend-build
FROM nginxproxymanager/testca AS testca
FROM letsencrypt/pebble AS pebbleca
FROM nginxproxymanager/nginx-full:certbot-node
ARG TARGETPLATFORM
@@ -46,7 +45,6 @@ RUN yarn install \
# add late to limit cache-busting by modifications
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
# 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_REDIS__HOST=authentik-redis
AUTHENTIK_POSTGRESQL__HOST=db-postgres
AUTHENTIK_POSTGRESQL__HOST=pgdb.internal
AUTHENTIK_POSTGRESQL__USER=authentik
AUTHENTIK_POSTGRESQL__NAME=authentik
AUTHENTIK_POSTGRESQL__PASSWORD=07EKS5NLI6Tpv68tbdvrxfvj

View File

@@ -1,5 +1,4 @@
FROM nginxproxymanager/testca AS testca
FROM letsencrypt/pebble AS pebbleca
FROM nginxproxymanager/nginx-full:certbot-node
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
# 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
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:
environment:
DB_POSTGRES_HOST: "db-postgres"
DB_POSTGRES_HOST: "pgdb.internal"
DB_POSTGRES_PORT: "5432"
DB_POSTGRES_USER: "npm"
DB_POSTGRES_PASSWORD: "npmpass"
@@ -27,7 +27,9 @@ services:
- psql_vol:/var/lib/postgresql/data
- ./ci/postgres:/docker-entrypoint-initdb.d
networks:
- fulltest
fulltest:
aliases:
- pgdb.internal
authentik-redis:
image: "redis:alpine"
@@ -41,6 +43,8 @@ services:
timeout: 3s
volumes:
- redis_vol:/data
networks:
- fulltest
authentik:
image: ghcr.io/goauthentik/server:2024.10.1
@@ -51,6 +55,8 @@ services:
depends_on:
- authentik-redis
- db-postgres
networks:
- fulltest
authentik-worker:
image: ghcr.io/goauthentik/server:2024.10.1
@@ -61,6 +67,8 @@ services:
depends_on:
- authentik-redis
- db-postgres
networks:
- fulltest
authentik-ldap:
image: ghcr.io/goauthentik/ldap:2024.10.1
@@ -71,6 +79,8 @@ services:
restart: unless-stopped
depends_on:
- authentik
networks:
- fulltest
volumes:
psql_vol:

View File

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

View File

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

2
frontend/.gitignore vendored
View File

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

View File

@@ -8,7 +8,17 @@
const allLocales = [
["en", "en-US"],
["fa", "fa-IR"],
["de", "de-DE"],
["es", "es-ES"],
["it", "it-IT"],
["ja", "ja-JP"],
["nl", "nl-NL"],
["pl", "pl-PL"],
["ru", "ru-RU"],
["sk", "sk-SK"],
["vi", "vi-VN"],
["zh", "zh-CN"],
["bg", "bg-BG"],
];
const ignoreUnused = [

View File

@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Nginx Proxy Manager</title>
<meta name="description" content="In The Office Planner" />
<link rel="preload" href="/images/logo-no-text.svg" as="image" type="image/svg+xml" fetchPriority="high">
<link
rel="apple-touch-icon"
sizes="180x180"

View File

@@ -13,6 +13,15 @@
--tblr-backdrop-opacity: 0.8 !important;
}
[data-bs-theme="dark"] .modal-content {
--tblr-modal-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
[data-bs-theme="dark"] .modal-backdrop {
--tblr-backdrop-bg: #000 !important;
--tblr-backdrop-opacity: 0.65 !important;
}
.domain-name {
font-family: monospace;
}
@@ -95,3 +104,15 @@ label.row {
border-radius: var(--tblr-border-radius) 0 0 var(--tblr-border-radius);
}
}
/* Fix for dropdown menus being clipped by table-responsive containers. */
.table-responsive .dropdown {
position: static;
}
/* Fix for Tabler scrollbar compensation */
@media (min-width: 992px) {
:host, :root {
margin-left: 0;
}
}

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 "./createCertificate";
export * from "./createDeadHost";

View File

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

View File

@@ -3,7 +3,7 @@ import cn from "classnames";
import { useFormikContext } from "formik";
import { useState } from "react";
import type { AccessListClient } from "src/api/backend";
import { T } from "src/locale";
import { intl, T } from "src/locale";
interface Props {
initialValues: AccessListClient[];
@@ -65,8 +65,8 @@ export function AccessClientFields({ initialValues, name = "clients" }: Props) {
value={client.directive}
onChange={(e) => handleChange(idx, "directive", e.target.value)}
>
<option value="allow">Allow</option>
<option value="deny">Deny</option>
<option value="allow"><T id="action.allow" /></option>
<option value="deny"><T id="action.deny" /></option>
</select>
</span>
<input
@@ -76,7 +76,7 @@ export function AccessClientFields({ initialValues, name = "clients" }: Props) {
autoComplete="off"
value={client.address}
onChange={(e) => handleChange(idx, "address", e.target.value)}
placeholder="192.168.1.100 or 192.168.1.0/24 or 2001:0db8::/32"
placeholder={intl.formatMessage({ id: "access-list.rule-source.placeholder" })}
/>
</div>
</div>
@@ -112,7 +112,7 @@ export function AccessClientFields({ initialValues, name = "clients" }: Props) {
value="deny"
disabled
>
<option value="deny">Deny</option>
<option value="deny"><T id="action.deny" /></option>
</select>
</span>
<input

View File

@@ -4,7 +4,7 @@ import type { ReactNode } from "react";
import Select, { type ActionMeta, components, type OptionProps } from "react-select";
import type { AccessList } from "src/api/backend";
import { useAccessLists } from "src/hooks";
import { DateTimeFormat, intl, T } from "src/locale";
import { formatDateTime, intl, T } from "src/locale";
interface AccessOption {
readonly value: number;
@@ -48,7 +48,7 @@ export function AccessField({ name = "accessListId", label = "access-list", id =
{
users: item?.items?.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" />,

View File

@@ -5,7 +5,7 @@ import { useState } from "react";
import Select, { type ActionMeta } from "react-select";
import type { DNSProvider } from "src/api/backend";
import { useDnsProviders } from "src/hooks";
import { T } from "src/locale";
import { intl, T } from "src/locale";
import styles from "./DNSProviderFields.module.css";
interface DNSProviderOption {
@@ -57,7 +57,7 @@ export function DNSProviderFields({ showBoundaryBox = false }: Props) {
id="dnsProvider"
closeMenuOnSelect={true}
isClearable={false}
placeholder="Select a Provider..."
placeholder={intl.formatMessage({ id: "certificates.dns.provider.placeholder" })}
isLoading={isLoading}
isSearchable
onChange={handleChange}
@@ -116,7 +116,7 @@ export function DNSProviderFields({ showBoundaryBox = false }: Props) {
type="number"
className="form-control"
min={0}
max={600}
max={7200}
{...field}
/>
<small className="text-muted">

View File

@@ -3,7 +3,7 @@ import { Field, useFormikContext } from "formik";
import Select, { type ActionMeta, components, type OptionProps } from "react-select";
import type { Certificate } from "src/api/backend";
import { useCertificates } from "src/hooks";
import { DateTimeFormat, intl, T } from "src/locale";
import { formatDateTime, intl, T } from "src/locale";
interface CertOption {
readonly value: number | "new";
@@ -75,7 +75,7 @@ export function SSLCertificateField({
data?.map((cert: Certificate) => ({
value: cert.id,
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" />,
})) || [];

View File

@@ -5,7 +5,11 @@ import { useTheme } from "src/hooks";
import { changeLocale, getFlagCodeForLocale, localeOptions, T } from "src/locale";
import styles from "./LocalePicker.module.css";
function LocalePicker() {
interface Props {
menuAlign?: "start" | "end";
}
function LocalePicker({ menuAlign = "start" }: Props) {
const { locale, setLocale } = useLocaleState();
const { getTheme } = useTheme();
@@ -23,22 +27,24 @@ function LocalePicker() {
<button type="button" className={cns} data-bs-toggle="dropdown">
<Flag countryCode={getFlagCodeForLocale(locale)} />
</button>
<div className="dropdown-menu">
{localeOptions.map((item) => {
return (
<a
className="dropdown-item"
href={`/locale/${item[0]}`}
key={`locale-${item[0]}`}
onClick={(e) => {
e.preventDefault();
changeTo(item[0]);
}}
>
<Flag countryCode={getFlagCodeForLocale(item[0])} /> <T id={`locale-${item[1]}`} />
</a>
);
<div
className={cn("dropdown-menu", {
"dropdown-menu-end": menuAlign === "end",
})}
>
{localeOptions.map((item: any) => (
<a
className="dropdown-item"
href={`/locale/${item[0]}`}
key={`locale-${item[0]}`}
onClick={(e) => {
e.preventDefault();
changeTo(item[0]);
}}
>
<Flag countryCode={getFlagCodeForLocale(item[0])} /> <T id={`locale-${item[1]}`} />
</a>
))}
</div>
</div>
);

View File

@@ -2,5 +2,5 @@ interface Props {
children: React.ReactNode;
}
export function SiteContainer({ children }: Props) {
return <div className="container-xl py-3">{children}</div>;
return <div className="container-xl py-3 min-w-0 overflow-x-auto">{children}</div>;
}

View File

@@ -1,8 +1,9 @@
import { useHealth } from "src/hooks";
import { useCheckVersion, useHealth } from "src/hooks";
import { T } from "src/locale";
export function SiteFooter() {
const health = useHealth();
const { data: versionData } = useCheckVersion();
const getVersion = () => {
if (!health.data) {
@@ -55,6 +56,19 @@ export function SiteFooter() {
{getVersion()}{" "}
</a>
</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>
</div>
</div>

View File

@@ -25,7 +25,7 @@ export function SiteHeader() {
>
<span className="navbar-toggler-icon" />
</button>
<div className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
<div className="navbar-brand navbar-brand-autodark pe-0 pe-md-3">
<NavLink to="/">
<div className={styles.logo}>
<img
@@ -48,11 +48,11 @@ export function SiteHeader() {
<ThemeSwitcher />
</div>
</div>
<div className="nav-item d-none d-md-flex me-3">
<div className="nav-item d-md-flex">
<div className="nav-item dropdown">
<a
href="/"
className="nav-link d-flex lh-1 p-0 px-2"
className="nav-link d-flex lh-1"
data-bs-toggle="dropdown"
aria-label="Open user menu"
>
@@ -70,6 +70,22 @@ export function SiteHeader() {
</div>
</a>
<div className="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
<div className="d-md-none">
{/* biome-ignore lint/a11y/noStaticElementInteractions lint/a11y/useKeyWithClickEvents: This div is not interactive. */}
<div className="p-2 pb-1 pe-1 d-flex align-items-center" onClick={e => e.stopPropagation()}>
<div className="ps-2 pe-1 me-auto">
<div>{currentUser?.nickname}</div>
<div className="mt-1 small text-secondary text-nowrap">
<T id={isAdmin ? "role.admin" : "role.standard-user"} />
</div>
</div>
<div className="d-flex align-items-center">
<ThemeSwitcher className="me-n2" />
<LocalePicker menuAlign="end" />
</div>
</div>
<div className="dropdown-divider" />
</div>
<a
href="?"
className="dropdown-item"

View File

@@ -176,21 +176,17 @@ const getMenuDropown = (item: MenuItem, onClick?: () => void) => {
};
export function SiteMenu() {
// This is hacky AF. But that's the price of using a non-react UI kit.
const closeMenus = () => {
const navMenus = document.querySelectorAll(".nav-item.dropdown");
navMenus.forEach((menu) => {
menu.classList.remove("show");
const dropdown = menu.querySelector(".dropdown-menu");
if (dropdown) {
dropdown.classList.remove("show");
}
});
};
const closeMenu = () => setTimeout(() => {
const navbarToggler = document.querySelector<HTMLElement>(".navbar-toggler");
const navbarMenu = document.querySelector("#navbar-menu");
if (navbarToggler && navbarMenu?.classList.contains("show")) {
navbarToggler.click();
}
}, 300);
return (
<header className="navbar-expand-md">
<div className="collapse navbar-collapse">
<div className="collapse navbar-collapse" id="navbar-menu">
<div className="navbar">
<div className="container-xl">
<div className="row flex-column flex-md-row flex-fill align-items-center">
@@ -198,7 +194,7 @@ export function SiteMenu() {
<ul className="navbar-nav">
{menuItems.length > 0 &&
menuItems.map((item) => {
return getMenuItem(item, closeMenus);
return getMenuItem(item, closeMenu);
})}
</ul>
</div>

View File

@@ -1,6 +1,6 @@
import cn from "classnames";
import { differenceInDays, isPast, parseISO } from "date-fns";
import { DateTimeFormat } from "src/locale";
import { differenceInDays, isPast } from "date-fns";
import { formatDateTime, parseDate } from "src/locale";
interface Props {
value: string;
@@ -8,11 +8,12 @@ interface Props {
highlistNearlyExpired?: boolean;
}
export function DateFormatter({ value, highlightPast, highlistNearlyExpired }: Props) {
const dateIsPast = isPast(parseISO(value));
const days = differenceInDays(parseISO(value), new Date());
const d = parseDate(value);
const dateIsPast = d ? isPast(d) : false;
const days = d ? differenceInDays(d, new Date()) : 0;
const cl = cn({
"text-danger": highlightPast && dateIsPast,
"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 type { ReactNode } from "react";
import { DateTimeFormat, T } from "src/locale";
import { formatDateTime, T } from "src/locale";
interface Props {
domains: string[];
@@ -10,35 +10,42 @@ interface Props {
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.
let onClick: ((e: React.MouseEvent) => void) | undefined;
if (domain.includes("*")) {
onClick = (e: React.MouseEvent) => e.preventDefault();
// Apparently the domain can be null or undefined sometimes.
// This try is just a safeguard to prevent the whole formatter from breaking.
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) {
const elms: ReactNode[] = [];
if (domains.length === 0 && !niceName) {
if ((!domains || domains.length === 0) && !niceName) {
elms.push(
<span key="nice-name" className="badge bg-danger-lt me-2">
Unknown
</span>,
);
}
if (niceName && provider !== "letsencrypt") {
if (!domains || (niceName && provider !== "letsencrypt")) {
elms.push(
<span key="nice-name" className="badge bg-info-lt me-2">
{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 (
<div className="flex-fill">
<div className="font-weight-medium">{...elms}</div>
{createdOn ? (
<div className="text-secondary mt-1">
<T id="created-on" data={{ date: DateTimeFormat(createdOn) }} />
<T id="created-on" data={{ date: formatDateTime(createdOn) }} />
</div>
) : null}
</div>

View File

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

View File

@@ -1,4 +1,4 @@
import { DateTimeFormat, T } from "src/locale";
import { formatDateTime, T } from "src/locale";
interface Props {
value: string;
@@ -13,7 +13,7 @@ export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) {
</div>
{createdOn ? (
<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>
) : null}
</div>

View File

@@ -12,10 +12,12 @@ interface TableLayoutProps<TFields> {
function TableLayout<TFields>(props: TableLayoutProps<TFields>) {
const hasRows = props.tableInstance.getRowModel().rows.length > 0;
return (
<table className="table table-vcenter table-selectable mb-0">
{hasRows ? <TableHeader tableInstance={props.tableInstance} /> : null}
<TableBody {...props} />
</table>
<div className="table-responsive">
<table className="table table-vcenter table-selectable mb-0">
{hasRows ? <TableHeader tableInstance={props.tableInstance} /> : null}
<TableBody {...props} />
</table>
</div>
);
}

View File

@@ -4,6 +4,7 @@ export * from "./useAuditLog";
export * from "./useAuditLogs";
export * from "./useCertificate";
export * from "./useCertificates";
export * from "./useCheckVersion";
export * from "./useDeadHost";
export * from "./useDeadHosts";
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 };

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

@@ -1,25 +1,61 @@
import { createIntl, createIntlCache } from "react-intl";
import langDe from "./lang/de.json";
import langEn from "./lang/en.json";
import langEs from "./lang/es.json";
import langIt from "./lang/it.json";
import langJa from "./lang/ja.json";
import langList from "./lang/lang-list.json";
import langNl from "./lang/nl.json";
import langPl from "./lang/pl.json";
import langRu from "./lang/ru.json";
import langSk from "./lang/sk.json";
import langVi from "./lang/vi.json";
import langZh from "./lang/zh.json";
import langBg from "./lang/bg.json";
// first item of each array should be the language code,
// not the country code
// Remember when adding to this list, also update check-locales.js script
const localeOptions = [["en", "en-US"]];
const localeOptions = [
["en", "en-US", langEn],
["de", "de-DE", langDe],
["es", "es-ES", langEs],
["ja", "ja-JP", langJa],
["it", "it-IT", langIt],
["nl", "nl-NL", langNl],
["pl", "pl-PL", langPl],
["ru", "ru-RU", langRu],
["sk", "sk-SK", langSk],
["vi", "vi-VN", langVi],
["zh", "zh-CN", langZh],
["bg", "bg-BG", langBg],
];
const loadMessages = (locale?: string): typeof langList & typeof langEn => {
const thisLocale = locale || "en";
switch (thisLocale.slice(0, 2)) {
default:
return Object.assign({}, langList, langEn);
const thisLocale = (locale || "en").slice(0, 2);
// ensure this lang exists in localeOptions above, otherwise fallback to en
if (thisLocale === "en" || !localeOptions.some(([code]) => code === thisLocale)) {
return Object.assign({}, langList, langEn);
}
return Object.assign({}, langList, langEn, localeOptions.find(([code]) => code === thisLocale)?.[2]);
};
const getFlagCodeForLocale = (locale?: string) => {
switch (locale) {
default:
return "EN";
const thisLocale = (locale || "en").slice(0, 2);
// only add to this if your flag is different from the locale code
const specialCases: Record<string, string> = {
ja: "jp", // Japan
zh: "cn", // China
vi: "vn", // Vietnam
};
if (specialCases[thisLocale]) {
return specialCases[thisLocale].toUpperCase();
}
return thisLocale.toUpperCase();
};
const getLocale = (short = false) => {
@@ -30,6 +66,10 @@ const getLocale = (short = false) => {
if (short) {
return loc.slice(0, 2);
}
// finally, fallback
if (!loc) {
loc = "en";
}
return loc;
};
@@ -76,4 +116,6 @@ const T = ({
);
};
console.log("L:", localeOptions);
export { localeOptions, getFlagCodeForLocale, getLocale, createIntl, changeLocale, intl, T };

View File

@@ -39,8 +39,10 @@ not be complete by the time you're reading this:
- frontend/src/locale/src/[yourlang].json
- frontend/src/locale/src/lang-list.json
- frontend/src/locale/src/HelpDoc/*
- frontend/src/locale/src/HelpDoc/[yourlang]/*
- frontend/src/locale/src/HelpDoc/index.tsx
- frontend/src/locale/IntlProvider.tsx
- frontend/check-locales.cjs
## 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 "./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 @@
## Какво представлява Списъкът за достъп?
Списъците за достъп предоставят черен или бял списък от конкретни клиентски IP адреси, както и удостоверяване за Прокси хостове чрез базова HTTP автентикация.
Можете да конфигурирате множество клиентски правила, потребителски имена и пароли в един Списък за достъп и след това да го приложите към един или повече _Прокси хостове_.
Това е най-полезно при препращани уеб услуги, които нямат вградени механизми за удостоверяване, или когато искате да защитите достъпа от неизвестни клиенти.

View File

@@ -0,0 +1,21 @@
## Помощ за сертификати
### HTTP сертификат
HTTP валидираният сертификат означава, че сървърите на Lets Encrypt ще се опитат да достигнат вашите домейни по HTTP (не по HTTPS!) и ако успеят, ще издадат сертификата.
За този метод трябва да имате създаден _Прокси хост_ за вашия/вашите домейни, който да е достъпен по HTTP и да сочи към тази Nginx инсталация. След като бъде издаден сертификат, можете да промените _Прокси хоста_ така, че да използва сертификата и за HTTPS връзки. Въпреки това, _Прокси хостът_ трябва да остане конфигуриран за достъп по HTTP, за да може сертификатът да се подновява.
Този процес _не_ поддържа wildcard домейни.
### DNS сертификат
DNS валидираният сертификат изисква използването на DNS Provider плъгин. Този DNS Provider ще бъде използван за временно създаване на записи във вашия домейн, след което Lets Encrypt ще ги провери, за да се увери, че сте собственикът, и при успех ще издаде сертификата.
Не е необходимо да имате _Прокси хост_, създаден предварително, за да заявите този тип сертификат. Нито е нужно вашият _Прокси хост_ да бъде конфигуриран за достъп по HTTP.
Този процес _поддържа_ wildcard домейни.
### Персонализиран сертификат
Използвайте тази опция, за да качите собствен SSL сертификат, предоставен от ваша сертификатна агенция.

View File

@@ -0,0 +1,10 @@
## Какво представлява 404 хост?
404 хост е просто конфигурация на хост, който показва страница с грешка 404.
Това може да е полезно, когато вашият домейн е индексиран в търсачките и искате
да предоставите по-приятна страница за грешка или да уведомите индексиращите системи,
че страниците на домейна вече не съществуват.
Допълнително предимство на този хост е възможността да проследявате логовете на заявките
към него и да виждате реферерите.

View File

@@ -0,0 +1,7 @@
## Какво представлява Прокси хост?
Прокси хост е входна точка за уеб услуга, която искате да препращате.
Той предоставя възможност за SSL терминaция на услуга, която може да няма вградена поддръжка на SSL.
Прокси хостовете са най-често използваната функция в Nginx Proxy Manager.

View File

@@ -0,0 +1,7 @@
## Какво представлява Хост за пренасочване?
Хостът за пренасочване пренасочва заявките от входящия домейн и прехвърля
потребителя към друг домейн.
Най-честата причина за използване на този тип хост е, когато вашият уебсайт
промени домейна си, но все още има линкове от търсачки или реферери, които сочат към стария домейн.

View File

@@ -0,0 +1,6 @@
## Какво представлява Потокът (Stream)?
Относително нова функция за 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 @@
## 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,17 +1,31 @@
// import * as de from "./de/index";
// import * as fa from "./fa/index";
import * as de from "./de/index";
import * as en from "./en/index";
import * as it from "./it/index";
import * as ja from "./ja/index";
import * as nl from "./nl/index";
import * as pl from "./pl/index";
import * as ru from "./ru/index";
import * as sk from "./sk/index";
import * as vi from "./vi/index";
import * as zh from "./zh/index";
import * as bg from "./bg/index";
const items: any = { en };
const items: any = { en, de, ja, sk, zh, pl, ru, it, vi, nl, bg };
const fallbackLang = "en";
export const getHelpFile = (lang: string, section: string): string => {
if (typeof items[lang] !== "undefined" && typeof items[lang][section] !== "undefined") {
if (
typeof items[lang] !== "undefined" &&
typeof items[lang][section] !== "undefined"
) {
return items[lang][section].default;
}
// Fallback to English
if (typeof items[fallbackLang] !== "undefined" && typeof items[fallbackLang][section] !== "undefined") {
if (
typeof items[fallbackLang] !== "undefined" &&
typeof items[fallbackLang][section] !== "undefined"
) {
return items[fallbackLang][section].default;
}
throw new Error(`Cannot load help doc for ${lang}-${section}`);

View File

@@ -0,0 +1,7 @@
## Che cos'è una Lista di Accesso?
La Lista di Accesso fornisce una blacklist o una whitelist di indirizzi IP specifici dei client insieme all'autenticazione per gli host proxy tramite autenticazione HTTP di base.
È possibile configurare più regole client, nomi utente e password per un singolo lista di accesso e quindi applicarlo a uno o più host proxy.
Ciò è particolarmente utile per i servizi web inoltrati che non dispongono di meccanismi di autenticazione integrati o quando si desidera proteggersi da client sconosciuti.

View File

@@ -0,0 +1,24 @@
## Guida sui Certificati
### Certificato HTTP
Un certificato convalidato HTTP significa che i server Let's Encrypttenteranno di raggiungere i tuoi domini tramite HTTP (non HTTPS!) e, in caso di esito positivo, emetteranno il tuo certificato.
Per questo metodo, dovrai creare un _Proxy Host_ per i tuoi domini chesia accessibile con HTTP e che punti a questa installazione Nginx.
Dopo che il certificato è stato rilasciato, puoi modificare il _Proxy Host_ per utilizzare questo certificato anche per le connessioni HTTPS.
Tuttavia, il _Proxy Host_ dovrà comunque essere configurato per l'accesso HTTP affinché il certificato possa essere rinnovato.
Questo processo _non_ supporta i domini wildcard.
### Certificato DNS
Un certificato convalidato dal DNS richiede l'uso di un plugin DNS Provider. Questo DNS Provider verrà utilizzato per creare record temporanei sul tuo dominio,
quindi Let's Encrypt interrogherà tali record per assicurarsi che tu sia il proprietario e, in caso di esito positivo,rilascerà il tuo certificato.
Non è necessario creare un _Proxy Host_ prima di richiedere questo tipo di certificato. Non è nemmeno necessario configurare il tuo _proxy host_ per l'accesso HTTP.
Questo processo _supporta_ i domini wildcard.
### Certificato personalizzato
Utilizza questa opzione per caricare il tuo certificato SSL, fornito dalla tua autorità di certificazione.

View File

@@ -0,0 +1,9 @@
## Che cos'è un Host 404?
Un Host 404 è semplicemente una configurazione host che mostra una pagina 404.
Questo può essere utile quando il tuo dominio è elencato nei motori di ricerca e desideri fornire una pagina di errore più gradevole o specificare agli
indicizzatori di ricerca che le pagine del dominio non esistono più.
Un altro vantaggio di avere questo host è quello di tracciare i log degli accessi e
visualizzare i referrer.

View File

@@ -0,0 +1,7 @@
## Che cos'è un Proxy Host?
Un host proxy è l'endpoint in entrata per un servizio web che si desidera inoltrare.
Fornisce la terminazione SSL opzionale per il servizio che potrebbe non avere il supporto SSL integrato.
Gli host proxy sono l'uso più comune per Nginx Proxy Manager.

View File

@@ -0,0 +1,7 @@
## Che cos'è un Host di reindirizzamento?
Un Host di reindirizzamento reindirizza le richieste provenienti dal dominio in entrata e indirizza il
visitatore verso un altro dominio.
Il motivo più comune per utilizzare questo tipo di host è quando il tuo sito web cambia
dominio, ma hai ancora link di motori di ricerca o referrer che puntano al vecchio dominio.

View File

@@ -0,0 +1,6 @@
## Che cos'è uno Stream?
Una funzionalità relativamente nuova per Nginx, uno Stream serve a inoltrare il traffico TCP/UDP
direttamente a un altro computer sulla rete.
Se gestisci server di gioco, FTP o SSH, questa funzionalità può rivelarsi molto utile.

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

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