Compare commits

..

5 Commits

Author SHA1 Message Date
Jamie Curnow
7214bcdacb Attempt to fix race condition with database instantiation 2025-11-06 12:56:07 +10:00
Jamie Curnow
9dc7dc28d4 Update sqlite3 package again 2025-11-06 12:55:48 +10:00
Jamie Curnow
36e040157f Remove references to pebble ca 2025-11-06 09:42:32 +10:00
Jamie Curnow
be2599872e Use new pebble image location 2025-11-06 08:14:27 +10:00
Jamie Curnow
d7ff736abf Revert sqlite package to same as 2.12.* 2025-11-06 07:57:14 +10:00
165 changed files with 752 additions and 9586 deletions

View File

@@ -1 +1 @@
2.13.5 2.13.1

285
Jenkinsfile vendored Normal file
View File

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

View File

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

View File

@@ -26,8 +26,8 @@
"azure": { "azure": {
"name": "Azure", "name": "Azure",
"package_name": "certbot-dns-azure", "package_name": "certbot-dns-azure",
"version": "~=2.6.1", "version": "~=1.2.0",
"dependencies": "azure-mgmt-dns==8.2.0", "dependencies": "",
"credentials": "# This plugin supported API authentication using either Service Principals or utilizing a Managed Identity assigned to the virtual machine.\n# Regardless which authentication method used, the identity will need the “DNS Zone Contributor” role assigned to it.\n# As multiple Azure DNS Zones in multiple resource groups can exist, the config file needs a mapping of zone to resource group ID. Multiple zones -> ID mappings can be listed by using the key dns_azure_zoneX where X is a unique number. At least 1 zone mapping is required.\n\n# Using a service principal (option 1)\ndns_azure_sp_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\ndns_azure_sp_client_secret = E-xqXU83Y-jzTI6xe9fs2YC~mck3ZzUih9\ndns_azure_tenant_id = ed1090f3-ab18-4b12-816c-599af8a88cf7\n\n# Using used assigned MSI (option 2)\n# dns_azure_msi_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\n\n# Using system assigned MSI (option 3)\n# dns_azure_msi_system_assigned = true\n\n# Zones (at least one always required)\ndns_azure_zone1 = example.com:/subscriptions/c135abce-d87d-48df-936c-15596c6968a5/resourceGroups/dns1\ndns_azure_zone2 = example.org:/subscriptions/99800903-fb14-4992-9aff-12eaf2744622/resourceGroups/dns2", "credentials": "# This plugin supported API authentication using either Service Principals or utilizing a Managed Identity assigned to the virtual machine.\n# Regardless which authentication method used, the identity will need the “DNS Zone Contributor” role assigned to it.\n# As multiple Azure DNS Zones in multiple resource groups can exist, the config file needs a mapping of zone to resource group ID. Multiple zones -> ID mappings can be listed by using the key dns_azure_zoneX where X is a unique number. At least 1 zone mapping is required.\n\n# Using a service principal (option 1)\ndns_azure_sp_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\ndns_azure_sp_client_secret = E-xqXU83Y-jzTI6xe9fs2YC~mck3ZzUih9\ndns_azure_tenant_id = ed1090f3-ab18-4b12-816c-599af8a88cf7\n\n# Using used assigned MSI (option 2)\n# dns_azure_msi_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\n\n# Using system assigned MSI (option 3)\n# dns_azure_msi_system_assigned = true\n\n# Zones (at least one always required)\ndns_azure_zone1 = example.com:/subscriptions/c135abce-d87d-48df-936c-15596c6968a5/resourceGroups/dns1\ndns_azure_zone2 = example.org:/subscriptions/99800903-fb14-4992-9aff-12eaf2744622/resourceGroups/dns2",
"full_plugin_name": "dns-azure" "full_plugin_name": "dns-azure"
}, },
@@ -255,14 +255,6 @@
"credentials": "dns_gcore_apitoken = 0123456789abcdef0123456789abcdef01234567", "credentials": "dns_gcore_apitoken = 0123456789abcdef0123456789abcdef01234567",
"full_plugin_name": "dns-gcore" "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": { "godaddy": {
"name": "GoDaddy", "name": "GoDaddy",
"package_name": "certbot-dns-godaddy", "package_name": "certbot-dns-godaddy",
@@ -378,7 +370,7 @@
"leaseweb": { "leaseweb": {
"name": "LeaseWeb", "name": "LeaseWeb",
"package_name": "certbot-dns-leaseweb", "package_name": "certbot-dns-leaseweb",
"version": "~=1.0.3", "version": "~=1.0.1",
"dependencies": "", "dependencies": "",
"credentials": "dns_leaseweb_api_token = 01234556789", "credentials": "dns_leaseweb_api_token = 01234556789",
"full_plugin_name": "dns-leaseweb" "full_plugin_name": "dns-leaseweb"
@@ -407,14 +399,6 @@
"credentials": "dns_luadns_email = user@example.com\ndns_luadns_token = 0123456789abcdef0123456789abcdef", "credentials": "dns_luadns_email = user@example.com\ndns_luadns_token = 0123456789abcdef0123456789abcdef",
"full_plugin_name": "dns-luadns" "full_plugin_name": "dns-luadns"
}, },
"mchost24": {
"name": "MC-HOST24",
"package_name": "certbot-dns-mchost24",
"version": "",
"dependencies": "",
"credentials": "# Obtain API token using https://github.com/JoeJoeTV/mchost24-api-python\ndns_mchost24_api_token=<insert obtained API token here>",
"full_plugin_name": "dns-mchost24"
},
"mijnhost": { "mijnhost": {
"name": "mijn.host", "name": "mijn.host",
"package_name": "certbot-dns-mijn-host", "package_name": "certbot-dns-mijn-host",
@@ -490,7 +474,7 @@
"porkbun": { "porkbun": {
"name": "Porkbun", "name": "Porkbun",
"package_name": "certbot-dns-porkbun", "package_name": "certbot-dns-porkbun",
"version": "~=0.11.0", "version": "~=0.9",
"dependencies": "", "dependencies": "",
"credentials": "dns_porkbun_key=your-porkbun-api-key\ndns_porkbun_secret=your-porkbun-api-secret", "credentials": "dns_porkbun_key=your-porkbun-api-key\ndns_porkbun_secret=your-porkbun-api-secret",
"full_plugin_name": "dns-porkbun" "full_plugin_name": "dns-porkbun"

View File

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

View File

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

View File

@@ -1,50 +0,0 @@
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

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

View File

@@ -1,40 +0,0 @@
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

@@ -1,23 +0,0 @@
{
"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

@@ -1,26 +0,0 @@
{
"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,11 +293,6 @@
"$ref": "./paths/tokens/post.json" "$ref": "./paths/tokens/post.json"
} }
}, },
"/version/check": {
"get": {
"$ref": "./paths/version/check/get.json"
}
},
"/users": { "/users": {
"get": { "get": {
"$ref": "./paths/users/get.json" "$ref": "./paths/users/get.json"

View File

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

View File

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

View File

@@ -1430,9 +1430,9 @@ isexe@^2.0.0:
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
js-yaml@^4.1.0: js-yaml@^4.1.0:
version "4.1.1" version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
dependencies: dependencies:
argparse "^2.0.1" argparse "^2.0.1"
@@ -1462,7 +1462,7 @@ jsonwebtoken@^9.0.2:
ms "^2.1.1" ms "^2.1.1"
semver "^7.5.4" semver "^7.5.4"
jwa@^1.4.2: jwa@^1.4.1:
version "1.4.2" version "1.4.2"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9" resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9"
integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw== integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==
@@ -1472,11 +1472,11 @@ jwa@^1.4.2:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
jws@^3.2.2: jws@^3.2.2:
version "3.2.3" version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.3.tgz#5ac0690b460900a27265de24520526853c0b8ca1" resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g== integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
dependencies: dependencies:
jwa "^1.4.2" jwa "^1.4.1"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
knex@2.4.2: knex@2.4.2:
@@ -1847,9 +1847,9 @@ netmask@^2.0.2:
integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==
node-abi@^3.3.0: node-abi@^3.3.0:
version "3.78.0" version "3.80.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.78.0.tgz#fd0ecbd0aa89857b98da06bd3909194abb0821ba" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.80.0.tgz#d7390951f27caa129cceeec01e1c20fc9f07581c"
integrity sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ== integrity sha512-LyPuZJcI9HVwzXK1GPxWNzrr+vr8Hp/3UqlmWxxh8p54U1ZbclOqbSog9lWHaCX+dBaiGi6n/hIX+mKu74GmPA==
dependencies: dependencies:
semver "^7.3.5" semver "^7.3.5"

View File

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

View File

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

View File

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

View File

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

2
frontend/.gitignore vendored
View File

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

View File

@@ -8,18 +8,7 @@
const allLocales = [ const allLocales = [
["en", "en-US"], ["en", "en-US"],
["de", "de-DE"], ["fa", "fa-IR"],
["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"],
["ko", "ko-KR"],
["bg", "bg-BG"],
]; ];
const ignoreUnused = [ const ignoreUnused = [

View File

@@ -5,7 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Nginx Proxy Manager</title> <title>Nginx Proxy Manager</title>
<meta name="description" content="In The Office Planner" /> <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 <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="180x180" sizes="180x180"

View File

@@ -13,15 +13,6 @@
--tblr-backdrop-opacity: 0.8 !important; --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 { .domain-name {
font-family: monospace; font-family: monospace;
} }
@@ -104,15 +95,3 @@ label.row {
border-radius: var(--tblr-border-radius) 0 0 var(--tblr-border-radius); 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

@@ -1,8 +0,0 @@
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,4 +1,3 @@
export * from "./checkVersion";
export * from "./createAccessList"; export * from "./createAccessList";
export * from "./createCertificate"; export * from "./createCertificate";
export * from "./createDeadHost"; export * from "./createDeadHost";

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import type { ReactNode } from "react";
import Select, { type ActionMeta, components, type OptionProps } from "react-select"; import Select, { type ActionMeta, components, type OptionProps } from "react-select";
import type { AccessList } from "src/api/backend"; import type { AccessList } from "src/api/backend";
import { useAccessLists } from "src/hooks"; import { useAccessLists } from "src/hooks";
import { formatDateTime, intl, T } from "src/locale"; import { DateTimeFormat, intl, T } from "src/locale";
interface AccessOption { interface AccessOption {
readonly value: number; readonly value: number;
@@ -48,7 +48,7 @@ export function AccessField({ name = "accessListId", label = "access-list", id =
{ {
users: item?.items?.length, users: item?.items?.length,
rules: item?.clients?.length, rules: item?.clients?.length,
date: item?.createdOn ? formatDateTime(item?.createdOn) : "N/A", date: item?.createdOn ? DateTimeFormat(item?.createdOn) : "N/A",
}, },
), ),
icon: <IconLock size={14} className="text-lime" />, 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 Select, { type ActionMeta } from "react-select";
import type { DNSProvider } from "src/api/backend"; import type { DNSProvider } from "src/api/backend";
import { useDnsProviders } from "src/hooks"; import { useDnsProviders } from "src/hooks";
import { intl, T } from "src/locale"; import { T } from "src/locale";
import styles from "./DNSProviderFields.module.css"; import styles from "./DNSProviderFields.module.css";
interface DNSProviderOption { interface DNSProviderOption {
@@ -57,7 +57,7 @@ export function DNSProviderFields({ showBoundaryBox = false }: Props) {
id="dnsProvider" id="dnsProvider"
closeMenuOnSelect={true} closeMenuOnSelect={true}
isClearable={false} isClearable={false}
placeholder={intl.formatMessage({ id: "certificates.dns.provider.placeholder" })} placeholder="Select a Provider..."
isLoading={isLoading} isLoading={isLoading}
isSearchable isSearchable
onChange={handleChange} onChange={handleChange}
@@ -116,7 +116,7 @@ export function DNSProviderFields({ showBoundaryBox = false }: Props) {
type="number" type="number"
className="form-control" className="form-control"
min={0} min={0}
max={7200} max={600}
{...field} {...field}
/> />
<small className="text-muted"> <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 Select, { type ActionMeta, components, type OptionProps } from "react-select";
import type { Certificate } from "src/api/backend"; import type { Certificate } from "src/api/backend";
import { useCertificates } from "src/hooks"; import { useCertificates } from "src/hooks";
import { formatDateTime, intl, T } from "src/locale"; import { DateTimeFormat, intl, T } from "src/locale";
interface CertOption { interface CertOption {
readonly value: number | "new"; readonly value: number | "new";
@@ -75,7 +75,7 @@ export function SSLCertificateField({
data?.map((cert: Certificate) => ({ data?.map((cert: Certificate) => ({
value: cert.id, value: cert.id,
label: cert.niceName, label: cert.niceName,
subLabel: `${cert.provider === "letsencrypt" ? intl.formatMessage({ id: "lets-encrypt" }) : cert.provider} ${intl.formatMessage({ id: "expires.on" }, { date: cert.expiresOn ? formatDateTime(cert.expiresOn) : "N/A" })}`, 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" })}`,
icon: <IconShield size={14} className="text-pink" />, icon: <IconShield size={14} className="text-pink" />,
})) || []; })) || [];

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ export function SiteHeader() {
> >
<span className="navbar-toggler-icon" /> <span className="navbar-toggler-icon" />
</button> </button>
<div className="navbar-brand navbar-brand-autodark pe-0 pe-md-3"> <div className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
<NavLink to="/"> <NavLink to="/">
<div className={styles.logo}> <div className={styles.logo}>
<img <img
@@ -48,11 +48,11 @@ export function SiteHeader() {
<ThemeSwitcher /> <ThemeSwitcher />
</div> </div>
</div> </div>
<div className="nav-item d-md-flex"> <div className="nav-item d-none d-md-flex me-3">
<div className="nav-item dropdown"> <div className="nav-item dropdown">
<a <a
href="/" href="/"
className="nav-link d-flex lh-1" className="nav-link d-flex lh-1 p-0 px-2"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-label="Open user menu" aria-label="Open user menu"
> >
@@ -70,22 +70,6 @@ export function SiteHeader() {
</div> </div>
</a> </a>
<div className="dropdown-menu dropdown-menu-end dropdown-menu-arrow"> <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 <a
href="?" href="?"
className="dropdown-item" className="dropdown-item"

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import cn from "classnames"; import cn from "classnames";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { formatDateTime, T } from "src/locale"; import { DateTimeFormat, T } from "src/locale";
interface Props { interface Props {
domains: string[]; domains: string[];
@@ -10,12 +10,8 @@ interface Props {
color?: string; color?: string;
} }
const DomainLink = ({ domain, color }: { domain?: string; color?: string }) => { const DomainLink = ({ domain, color }: { domain: string; color?: string }) => {
// when domain contains a wildcard, make the link go nowhere. // when domain contains a wildcard, make the link go nowhere.
// 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; let onClick: ((e: React.MouseEvent) => void) | undefined;
if (domain.includes("*")) { if (domain.includes("*")) {
onClick = (e: React.MouseEvent) => e.preventDefault(); onClick = (e: React.MouseEvent) => e.preventDefault();
@@ -31,21 +27,18 @@ const DomainLink = ({ domain, color }: { domain?: string; color?: string }) => {
{domain} {domain}
</a> </a>
); );
} catch {
return null;
}
}; };
export function DomainsFormatter({ domains, createdOn, niceName, provider, color }: Props) { export function DomainsFormatter({ domains, createdOn, niceName, provider, color }: Props) {
const elms: ReactNode[] = []; const elms: ReactNode[] = [];
if ((!domains || domains.length === 0) && !niceName) { if (domains.length === 0 && !niceName) {
elms.push( elms.push(
<span key="nice-name" className="badge bg-danger-lt me-2"> <span key="nice-name" className="badge bg-danger-lt me-2">
Unknown Unknown
</span>, </span>,
); );
} }
if (!domains || (niceName && provider !== "letsencrypt")) { if (niceName && provider !== "letsencrypt") {
elms.push( elms.push(
<span key="nice-name" className="badge bg-info-lt me-2"> <span key="nice-name" className="badge bg-info-lt me-2">
{niceName} {niceName}
@@ -53,16 +46,14 @@ export function DomainsFormatter({ domains, createdOn, niceName, provider, color
); );
} }
if (domains) {
domains.map((domain: string) => elms.push(<DomainLink key={domain} domain={domain} color={color} />)); domains.map((domain: string) => elms.push(<DomainLink key={domain} domain={domain} color={color} />));
}
return ( return (
<div className="flex-fill"> <div className="flex-fill">
<div className="font-weight-medium">{...elms}</div> <div className="font-weight-medium">{...elms}</div>
{createdOn ? ( {createdOn ? (
<div className="text-secondary mt-1"> <div className="text-secondary mt-1">
<T id="created-on" data={{ date: formatDateTime(createdOn) }} /> <T id="created-on" data={{ date: DateTimeFormat(createdOn) }} />
</div> </div>
) : null} ) : null}
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +0,0 @@
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

@@ -0,0 +1,15 @@
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 };

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

@@ -1,64 +1,25 @@
import { createIntl, createIntlCache } from "react-intl"; import { createIntl, createIntlCache } from "react-intl";
import langDe from "./lang/de.json";
import langEn from "./lang/en.json"; import langEn from "./lang/en.json";
import langEs from "./lang/es.json";
import langIt from "./lang/it.json";
import langJa from "./lang/ja.json";
import langList from "./lang/lang-list.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 langKo from "./lang/ko.json";
import langBg from "./lang/bg.json";
// first item of each array should be the language code, // first item of each array should be the language code,
// not the country code // not the country code
// Remember when adding to this list, also update check-locales.js script // Remember when adding to this list, also update check-locales.js script
const localeOptions = [ const localeOptions = [["en", "en-US"]];
["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],
["ko", "ko-KR", langKo],
["bg", "bg-BG", langBg],
];
const loadMessages = (locale?: string): typeof langList & typeof langEn => { const loadMessages = (locale?: string): typeof langList & typeof langEn => {
const thisLocale = (locale || "en").slice(0, 2); const thisLocale = locale || "en";
switch (thisLocale.slice(0, 2)) {
// ensure this lang exists in localeOptions above, otherwise fallback to en default:
if (thisLocale === "en" || !localeOptions.some(([code]) => code === thisLocale)) {
return Object.assign({}, langList, langEn); return Object.assign({}, langList, langEn);
} }
return Object.assign({}, langList, langEn, localeOptions.find(([code]) => code === thisLocale)?.[2]);
}; };
const getFlagCodeForLocale = (locale?: string) => { const getFlagCodeForLocale = (locale?: string) => {
const thisLocale = (locale || "en").slice(0, 2); switch (locale) {
default:
// only add to this if your flag is different from the locale code return "EN";
const specialCases: Record<string, string> = {
ja: "jp", // Japan
zh: "cn", // China
vi: "vn", // Vietnam
ko: "kr", // Korea
};
if (specialCases[thisLocale]) {
return specialCases[thisLocale].toUpperCase();
} }
return thisLocale.toUpperCase();
}; };
const getLocale = (short = false) => { const getLocale = (short = false) => {
@@ -69,10 +30,6 @@ const getLocale = (short = false) => {
if (short) { if (short) {
return loc.slice(0, 2); return loc.slice(0, 2);
} }
// finally, fallback
if (!loc) {
loc = "en";
}
return loc; return loc;
}; };
@@ -119,6 +76,4 @@ const T = ({
); );
}; };
console.log("L:", localeOptions);
export { localeOptions, getFlagCodeForLocale, getLocale, createIntl, changeLocale, intl, T }; export { localeOptions, getFlagCodeForLocale, getLocale, createIntl, changeLocale, intl, T };

View File

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

View File

@@ -1,74 +0,0 @@
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

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

View File

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

View File

@@ -0,0 +1,215 @@
{
"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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
## Какво представлява Потокът (Stream)?
Относително нова функция за Nginx, Потокът позволява препращане на TCP/UDP
трафик директно към друг компютър в мрежата.
Това е полезно, ако хоствате игрови сървъри, FTP или SSH сървъри.

View File

@@ -1,6 +0,0 @@
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,7 +0,0 @@
## 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

@@ -1,32 +0,0 @@
## 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

@@ -1,10 +0,0 @@
## 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

@@ -1,7 +0,0 @@
## 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

@@ -1,7 +0,0 @@
## 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

@@ -1,6 +0,0 @@
## 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

@@ -1,6 +0,0 @@
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,7 +0,0 @@
## ¿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

@@ -1,32 +0,0 @@
## 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

@@ -1,10 +0,0 @@
## ¿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

@@ -1,7 +0,0 @@
## ¿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

@@ -1,7 +0,0 @@
## ¿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

@@ -1,6 +0,0 @@
## ¿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

@@ -1,6 +0,0 @@
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,32 +1,17 @@
import * as de from "./de/index"; // import * as de from "./de/index";
// import * as fa from "./fa/index";
import * as en from "./en/index"; import * as en from "./en/index";
import * as 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 ko from "./ko/index";
import * as bg from "./bg/index";
const items: any = { en, de, ja, sk, zh, pl, ru, it, vi, nl, bg, ko }; const items: any = { en };
const fallbackLang = "en"; const fallbackLang = "en";
export const getHelpFile = (lang: string, section: string): string => { export const getHelpFile = (lang: string, section: string): string => {
if ( if (typeof items[lang] !== "undefined" && typeof items[lang][section] !== "undefined") {
typeof items[lang] !== "undefined" &&
typeof items[lang][section] !== "undefined"
) {
return items[lang][section].default; return items[lang][section].default;
} }
// Fallback to English // Fallback to English
if ( if (typeof items[fallbackLang] !== "undefined" && typeof items[fallbackLang][section] !== "undefined") {
typeof items[fallbackLang] !== "undefined" &&
typeof items[fallbackLang][section] !== "undefined"
) {
return items[fallbackLang][section].default; return items[fallbackLang][section].default;
} }
throw new Error(`Cannot load help doc for ${lang}-${section}`); throw new Error(`Cannot load help doc for ${lang}-${section}`);

View File

@@ -1,7 +0,0 @@
## 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

@@ -1,24 +0,0 @@
## 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

@@ -1,9 +0,0 @@
## 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

@@ -1,7 +0,0 @@
## 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

@@ -1,7 +0,0 @@
## 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

@@ -1,6 +0,0 @@
## 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
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,11 +0,0 @@
## 접근 정책이란?
접근 정책은 특정 클라이언트 IP 주소를 허용하거나 거부할 수 있으며,
프록시 호스트에 기본 HTTP 인증(Basic Auth) 을 적용할 수 있는 기능입니다.
하나의 접근 목록에 여러 클라이언트 규칙과 사용자 이름, 비밀번호를 추가한 뒤
이를 하나 이상의 프록시 호스트에 적용할 수 있습니다.
이 기능은 인증 기능이 없는 웹 서비스에 인증을 추가하거나,
알 수 없는 클라이언트로부터 서비스를 보호할 때 유용합니다.

View File

@@ -1,28 +0,0 @@
## 인증서 도움말
### HTTP 인증서
HTTP 검증 방식의 인증서는 Let's Encrypt 서버가 **HTTPS가 아닌 HTTP로** 해당 도메인에 접속을 시도해 응답이 확인되면 인증서를 발급하는 방식입니다.
이 방식을 사용하려면 도메인에 대한 **프록시 호스트가 미리 생성되어 있어야 하며**, HTTP로 접근할 수 있어야 하고 Nginx Proxy Manager가 설치된 서버를 가리켜야 합니다. 인증서가 발급된 이후에는 해당 프록시 호스트에 HTTPS용 인증서를 적용할 수 있습니다.
다만, **인증서 자동 갱신을 위해서는 HTTP 접근이 계속 필요합니다.**
이 방식은 **와일드카드 도메인을 지원하지 않습니다.**
---
### DNS 인증서
DNS 검증 방식의 인증서는 DNS 공급자 플러그인을 사용해야 합니다. 이 플러그인은 도메인에 임시 DNS 레코드를 생성하며, Let's Encrypt는 해당 레코드를 조회해 도메인 소유 여부를 확인합니다. 검증이 성공하면 인증서가 발급됩니다.
이 방식은 인증서를 요청하기 전에 **프록시 호스트를 생성할 필요가 없으며**, 프록시 호스트에 HTTP 접근을 설정할 필요도 없습니다.
이 방식은 **와일드카드 도메인을 지원합니다.**
---
### 사용자 지정 인증서
이 옵션을 사용하면 직접 보유한 인증 기관(CA)에서 발급한 SSL 인증서를 직접 업로드하여 사용할 수 있습니다.

View File

@@ -1,10 +0,0 @@
## 404 호스트란?
404 호스트는 404 오류 페이지를 표시하도록 구성된 호스트입니다.
이 기능은 도메인이 검색 엔진에 이미 색인되어 있을 때,
더 깔끔한 오류 페이지를 제공하거나 해당 페이지가 더 이상 존재하지 않음을
검색 엔진에게 명확하게 알려야 할 때 유용합니다.
또한 404 호스트를 사용하면 접근 로그를 확인하고, 어떤 경로(Referrer)를 통해 들어왔는지 추적할 수 있다는 장점도 있습니다.

View File

@@ -1,8 +0,0 @@
## 프록시 호스트란?
프록시 호스트는 외부에서 들어오는 웹 요청을 받아 지정한 전달 대상으로 전달하는 역할을 합니다.
원래 SSL을 지원하지 않는 대상이라도, 프록시 호스트를 통해 SSL(HTTPS) 연결을 적용할 수 있습니다.
프록시 호스트는 Nginx Proxy Manager에서 가장 일반적으로 사용되는 기능입니다.

View File

@@ -1,7 +0,0 @@
## 리다이렉션 호스트란?
리다이렉션 호스트는 외부에서 들어오는 도메인 요청을 다른 도메인으로 자동 이동(리다이렉트)시키는 역할을 합니다.
이 유형의 호스트는 주로 웹사이트의 도메인이 변경되었지만,
검색 엔진이나 다른 사이트에 이전 도메인 링크가 남아 있을 때 사용하면 가장 효과적입니다.

View File

@@ -1,7 +0,0 @@
## 호스트 스트림이란?
호스트 스트림은 비교적 최근에 Nginx에 추가된 기능으로,
TCP/UDP 트래픽을 네트워크 내의 다른 컴퓨터로 직접 전달하는 데 사용됩니다.
게임 서버나 FTP, SSH 서버 등을 운영할 때 유용하게 사용할 수 있습니다.

View File

@@ -1,7 +0,0 @@
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,7 +0,0 @@
## Wat is een Toegangslijst?
Toeganslijsten bieden een zwarte- of witte lijst van specifieke client IP-adressen samen met authenticatie voor de Proxy Hosts via Basic HTTP Authenticatie.
Je kan meerdere client regels, gebruikersnamen en wachtwoorden voor een enkele Toegangslijst configureren en toepassen op één of meerdere _Proxy Hosts_.
Dit is het meest nuttig voor doorgestuurde webdiensten die geen authenticatiemechanismen hebben of wanneer je wilt beveiligen tegen onbekende bezoekers.

View File

@@ -1,31 +0,0 @@
## Certificaten Hulp
### HTTP Certificaat
Een HTTP gevalideerd certificaat betekent dat Let's Encrypt servers
zullen proberen om over HTTP te bereiken (niet HTTPS!) en als dat gelukt is, zal
jouw certificaat worden uitgegeven.
Voor deze zal je een _Proxy Host_ moeten hebben die is toegankelijk via HTTP en
die naar deze Nginx installatie wijst. Nadat een certificaat is uitgegeven kan je
de _Proxy Host_ wijzigen om ook HTTPS toegang te geven. Maar de _Proxy Host_ zal
nog moeten worden geconfigureerd voor HTTP toegang om het certificaat te verlengen.
Dit proces ondersteunt geen domeinen met wildcards.
### DNS Certificaat
Een DNS gevalideerd certificaat zal gebruik maken van een DNS Provider plugin. De
DNS Provider zal tijdelijke records op jouw domein maken en Let's Encrypt zal deze
records opvragen om te controleren of je de eigenaar bent. Als dat is gecontroleerd
is zal Let's Encrypt het certificaat uitgeven.
Je hebt geen _Proxy Host_ nodig om dit soort certificaat aan te vragen. Je hebt dus
geen HTTP _Proxy Host_ nodig.
Dit proces ondersteunt _wel_ domeinen met wildcards.
### Aangepast Certificaat
Gebruik deze optie om jouw eigen SSL Certificaat te uploaden, zoals
geleverd door jouw eigen Certificate Authority.

View File

@@ -1,10 +0,0 @@
## Wat is een 404 Host?
Simpel gezegd is een 404 Host een host setup die een 404 pagina weergeeft.
Dit kan nuttig zijn wanneer jouw domein is opgegeven in zoekmachines en je wil
een betere foutpagina leveren of specifiek om te zeggen tegen de zoekmachines dat
het domein niet langer bestaat.
Een ander voordeel van het hebben van een 404 Host is om de logs voor bezoeken
te volgen en de referenties te bekijken.

View File

@@ -1,7 +0,0 @@
## Wat is een Proxy Host?
Een Proxy Host is de inkomende endpoint voor een webdienst dat je wilt doorsturen.
Het biedt optionele SSL voor je dienst die mogelijk geen SSL ondersteuning heeft.
Proxy Hosts worden het meest gebruikt in Nginx Proxy Manager.

View File

@@ -1,7 +0,0 @@
## Wat is een Redirection Host?
Een Redirection Host zal verzoeken van de inkomende domeinnaam doorsturen, en de bezoeker
omleiden naar een andere domeinnaam.
Het gebruik van een Redirection Host is vooral handig wanneer je jouw website verandert
maar je nog zoekmachines of referenties naar de oude domeinnaam hebben.

View File

@@ -1,6 +0,0 @@
## Wat is een Stream?
Streams zijn een nieuwe toevoeging aan Nginx, die toelaat om TCP/UDP
verkeer naar een ander computer op het netwerk te sturen.
Als je game servers, FTP of SSH servers draait kan dit handig zijn.

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