Merge branch 'develop' into lang-spanish

This commit is contained in:
Pablo Portas López
2025-11-12 02:14:09 +01:00
committed by GitHub
60 changed files with 1294 additions and 412 deletions

View File

@@ -1 +1 @@
2.13.1
2.13.3

285
Jenkinsfile vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,50 @@
import { migrate as logger } from "../logger.js";
const migrateName = "redirect_auto_scheme";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.table("redirection_host", async (table) => {
// change the column default from $scheme to auto
await table.string("forward_scheme").notNull().defaultTo("auto").alter();
await knex('redirection_host')
.where('forward_scheme', '$scheme')
.update({ forward_scheme: 'auto' });
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema
.table("redirection_host", async (table) => {
await table.string("forward_scheme").notNull().defaultTo("$scheme").alter();
await knex('redirection_host')
.where('forward_scheme', 'auto')
.update({ forward_scheme: '$scheme' });
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
});
};
export { up, down };

View File

@@ -10,7 +10,7 @@ import now from "./now_helper.js";
import ProxyHostModel from "./proxy_host.js";
import User from "./user.js";
Model.knex(db);
Model.knex(db());
const boolFields = ["is_deleted", "satisfy_any", "pass_auth"];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import cn from "classnames";
import type { ReactNode } from "react";
import { DateTimeFormat, T } from "src/locale";
import { formatDateTime, T } from "src/locale";
interface Props {
domains: string[];
@@ -53,7 +53,7 @@ export function DomainsFormatter({ domains, createdOn, niceName, provider, color
<div className="font-weight-medium">{...elms}</div>
{createdOn ? (
<div className="text-secondary mt-1">
<T id="created-on" data={{ date: DateTimeFormat(createdOn) }} />
<T id="created-on" data={{ date: formatDateTime(createdOn) }} />
</div>
) : null}
</div>

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import { createIntl, createIntlCache } from "react-intl";
import langEn from "./lang/en.json";
import langEs from "./lang/es.json";
import langDe from "./lang/de.json";
import langList from "./lang/lang-list.json";
// first item of each array should be the language code,
@@ -8,7 +9,8 @@ import langList from "./lang/lang-list.json";
// Remember when adding to this list, also update check-locales.js script
const localeOptions = [
["en", "en-US"],
["es", "es-ES"]
["es", "es-ES"],
["de", "de-DE"]
];
const loadMessages = (locale?: string): typeof langList & typeof langEn => {
@@ -16,6 +18,8 @@ const loadMessages = (locale?: string): typeof langList & typeof langEn => {
switch (thisLocale.slice(0, 2)) {
case "es":
return Object.assign({}, langList, langEs);
case "de":
return Object.assign({}, langList, langEn, langDe);
default:
return Object.assign({}, langList, langEn);
}
@@ -26,6 +30,9 @@ const getFlagCodeForLocale = (locale?: string) => {
case "es-ES":
case "es":
return "ES";
case "de-DE":
case "de":
return "DE";
default:
return "EN";
}
@@ -39,13 +46,20 @@ const getLocale = (short = false) => {
if (short) {
return loc.slice(0, 2);
}
// finally, fallback
if (!loc) {
loc = "en";
}
return loc;
};
const cache = createIntlCache();
const initialMessages = loadMessages(getLocale());
let intl = createIntl({ locale: getLocale(), messages: initialMessages }, cache);
let intl = createIntl(
{ locale: getLocale(), messages: initialMessages },
cache,
);
const changeLocale = (locale: string): void => {
const messages = loadMessages(locale);
@@ -85,4 +99,12 @@ const T = ({
);
};
export { localeOptions, getFlagCodeForLocale, getLocale, createIntl, changeLocale, intl, T };
export {
localeOptions,
getFlagCodeForLocale,
getLocale,
createIntl,
changeLocale,
intl,
T,
};

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,215 @@
{
"access-list": "Zugriffsliste",
"access-list.access-count": "{count} {count, plural, one {Regel} other {Regeln}}",
"access-list.auth-count": "{count} {count, plural, one {User} other {Users}}",
"access-list.help-rules-last": "Wenn mindestens eine Regel vorhanden ist, wird diese Regel zum Ablehnen aller Anfragen als letzte hinzugefügt.",
"access-list.help.rules-order": "Beachten Sie, dass die Anweisungen „Erlauben“ und „Verbieten“ in der Reihenfolge ihrer Definition angewendet werden.",
"access-list.pass-auth": "Authentifizierung an Upstream weiterleiten",
"access-list.public": "Öffentlich",
"access-list.public.subtitle": "Keine Authentifizierung erforderlich",
"access-list.satisfy-any": "Satisfy Any",
"access-list.subtitle": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Regel} other {Regeln}} - Erstellt: {date}",
"access-lists": "Zugrifflisten",
"action.add": "Hinzufügen",
"action.add-location": "Pfad Hinzufügen",
"action.close": "Schließen",
"action.delete": "Löschen",
"action.disable": "Deaktivieren",
"action.download": "Herunterladen",
"action.edit": "Bearbeiten",
"action.enable": "Aktivieren",
"action.permissions": "Berechtigungen",
"action.renew": "Erneuert",
"action.view-details": "Details",
"auditlogs": "Protokoll",
"cancel": "Abbrechen",
"certificate": "Zertifikat",
"certificate.custom-certificate": "Zertifikat",
"certificate.custom-certificate-key": "Privater Schlüssel",
"certificate.custom-intermediate": "Zwischen Zertifikat",
"certificate.in-use": "In Benutzung",
"certificate.none.subtitle": "Kein Zertifikat zugewiesen",
"certificate.none.subtitle.for-http": "Dieser Host verwendet kein HTTPS.",
"certificate.none.title": "Kein",
"certificate.not-in-use": "Nicht in Benutzung",
"certificates": "Zertifikate",
"certificates.custom": "Benutzerdefiniertes Zertifikat",
"certificates.custom.warning": "Mit einem Passwort geschützte Schlüsseldateien werden nicht unterstützt.",
"certificates.dns.credentials": "Inhalt der Anmeldedaten-Datei",
"certificates.dns.credentials-note": "Dieses Plugin erfordert eine Konfigurationsdatei, die einen API-Token oder andere Anmeldedaten für Ihren Anbieter enthält.",
"certificates.dns.credentials-warning": "Diese Daten werden als Klartext in der Datenbank und in einer Datei gespeichert!",
"certificates.dns.propagation-seconds": "Wartzeit in Sekunden",
"certificates.dns.propagation-seconds-note": "Leer lassen um die Standardwartezeit des Plugins zu nutzen",
"certificates.dns.provider": "DNS Provider",
"certificates.dns.warning": "Dieser Abschnitt erfordert einige Kenntnisse über Certbot und seine DNS-Plugins. Bitte konsultieren Sie die jeweilige Plugin-Dokumentation.",
"certificates.http.reachability-404": "Unter dieser Domain wurde ein Server gefunden, aber es scheint sich nicht um Nginx Proxy Manager zu handeln. Bitte stellen Sie sicher, dass Ihre Domain auf die IP-Adresse verweist, unter der Ihre NPM-Instanz ausgeführt wird.",
"certificates.http.reachability-failed-to-check": "Die Erreichbarkeit konnte aufgrund eines Kommunikationsfehlers mit site24x7.com nicht überprüft werden.",
"certificates.http.reachability-not-resolved": "Unter dieser Domain ist kein Server verfügbar. Bitte stellen Sie sicher, dass Ihre Domain existiert und auf die IP-Adresse verweist, unter der Ihre NPM-Instanz läuft, und dass gegebenenfalls Port 80 in Ihrem Router weitergeleitet wird.",
"certificates.http.reachability-ok": "Ihr Server ist erreichbar und die Erstellung von Zertifikaten sollte möglich sein.",
"certificates.http.reachability-other": "Unter dieser Domain wurde ein Server gefunden, der jedoch einen unerwarteten Statuscode {code} zurückgegeben hat. Handelt es sich um den NPM-Server? Bitte stellen Sie sicher, dass Ihre Domain auf die IP-Adresse verweist, unter der Ihre NPM-Instanz ausgeführt wird.",
"certificates.http.reachability-wrong-data": "Unter dieser Domain wurde ein Server gefunden, der jedoch unerwartete Daten zurückgegeben hat. Handelt es sich um den NPM-Server? Bitte stellen Sie sicher, dass Ihre Domain auf die IP-Adresse verweist, unter der Ihre NPM-Instanz ausgeführt wird.",
"certificates.http.test-results": "Test Ergeniss",
"certificates.http.warning": "Diese Domänen müssen bereits so konfiguriert sein, dass sie auf diese Installation verweisen.",
"certificates.request.subtitle": "Über Let's Encrypt",
"certificates.request.title": "Anfordern eines neuen Zertifikates",
"column.access": "Zugriff",
"column.authorization": "Genehmigung",
"column.authorizations": "Genehmigungen",
"column.custom-locations": "Benutzerdefinierte Pfad",
"column.destination": "Ziel",
"column.details": "Details",
"column.email": "Email",
"column.event": "Ereignis",
"column.expires": "Verfällt am",
"column.http-code": "HTTP Code",
"column.incoming-port": "Eingehender Port",
"column.name": "Name",
"column.protocol": "Protokoll",
"column.provider": "Provider",
"column.roles": "Rollen",
"column.rules": "Regeln",
"column.satisfy": "Satisfy",
"column.satisfy-all": "Alle",
"column.satisfy-any": "Jeder",
"column.scheme": "Schema",
"column.source": "Quelle",
"column.ssl": "SSL",
"column.status": "Status",
"created-on": "Erstelldatum: {date}",
"dashboard": "Dashboard",
"dead-host": "404 Host",
"dead-hosts": "404 Hosts",
"dead-hosts.count": "{count} {count, plural, one {404 Host} other {404 Hosts}}",
"disabled": "Deaktiviert",
"domain-names": "Domain Names",
"domain-names.max": "{count} Maximale Anzahl von Domainnamen",
"domain-names.placeholder": "Eintragen der Domain...",
"domain-names.wildcards-not-permitted": "Wildcards sind für diesen Typ nicht zulässig.",
"domain-names.wildcards-not-supported": "Wildcards werden für diese Zertifizierungsstelle nicht unterstützt.",
"domains.force-ssl": "Erzwinge SSL",
"domains.hsts-enabled": "HSTS aktiviert",
"domains.hsts-subdomains": "HSTS Sub-domains",
"domains.http2-support": "HTTP/2 Support",
"domains.use-dns": "Nutze DNS Challenge",
"email-address": "Email Addresse",
"empty-search": "Keine Ergebnisse gefunden",
"empty-subtitle": "Warum erstellen Sie nicht eine?",
"enabled": "aktiviert",
"error.access.at-least-one": "Entweder eine Genehmigung oder eine Zugriffsregel ist erforderlich.",
"error.access.duplicate-usernames": "Autorisierung Benutzernamen müssen eindeutig sein",
"error.invalid-auth": "Ungültige E-Mail-Adresse oder Passwort",
"error.invalid-domain": "Ungültige Domain: {domain}",
"error.invalid-email": "Ungültige E-Mail-Adresse",
"error.max-character-length": "Die maximale Länge beträgt {max} Zeichen{max, plural, one {} other {s}}",
"error.max-domains": "Zu viele Domains, maximal sind {max}",
"error.maximum": "Maximum ist {max}",
"error.min-character-length": "Die minimale Länge beträgt {min} Zeichen{min, plural, one {} other {s}}",
"error.minimum": "Minimum ist {min}",
"error.passwords-must-match": "Passwörter müssen übereinstimmen",
"error.required": "Dies ist erforderlich.",
"expires.on": "Ablauf am: {date}",
"footer.github-fork": "Fork me on Github",
"host.flags.block-exploits": "Gängige Exploits blockieren",
"host.flags.cache-assets": "Cache Assets",
"host.flags.preserve-path": "Pfad beibehalten",
"host.flags.protocols": "Protokole",
"host.flags.websockets-upgrade": "Websockets Support",
"host.forward-port": "Forward Port",
"host.forward-scheme": "Schema",
"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": "Laden…",
"login.title": "Account Login",
"nginx-config.label": "Benutzerdefinierte Nginx Konfiguration",
"nginx-config.placeholder": "# Geben Sie hier Ihre benutzerdefinierte Nginx-Konfiguration auf eigene Gefahr ein!",
"no-permission-error": "Sie haben keinen Zugriff, um dies anzuzeigen.",
"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} wurde gelöscht",
"notification.object-disabled": "{object} wurde deaktiviert",
"notification.object-enabled": "{object} wurde aktiviert",
"notification.object-renewed": "{object} wurde erneuert",
"notification.object-saved": "{object} wurde gespeichert",
"notification.success": "Erfolgreich",
"object.actions-title": "{object} #{id}",
"object.add": "{object} hinzufügen",
"object.delete": "{object} löschen",
"object.delete.content": "Bist du dir sicher das du diese(n) {object} löschen möchtest?",
"object.edit": "{object} bearbeiten",
"object.empty": "Keine {objects} vorhanden",
"object.event.created": "{object} erstellt",
"object.event.deleted": "{object} gelöscht",
"object.event.disabled": "{object} deaktiviert",
"object.event.enabled": "{object} aktiviert",
"object.event.renewed": "{object} erneuert",
"object.event.updated": "{object} aktualisiert",
"offline": "Offline",
"online": "Online",
"options": "Optionen",
"password": "Passwort",
"password.generate": "Zufälliges Passwort generieren",
"password.hide": "Passwort verstecken",
"password.show": "Passwort anzeigen",
"permissions.hidden": "Versteckt",
"permissions.manage": "Verwalten",
"permissions.view": "Nur anzeigen",
"permissions.visibility.all": "Alle Elemente",
"permissions.visibility.title": "Objekt Sichtbarkeit",
"permissions.visibility.user": "Nur erstellte Elemente",
"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": "Öffentlich",
"redirection-host": "Redirection Host",
"redirection-host.forward-domain": "Forward Domain",
"redirection-host.forward-http-code" : "HTTP Code",
"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": "Speichern",
"setting": "Einstellung",
"settings": "Einstellungen",
"settings.default-site": "Standard Seite",
"settings.default-site.404": "404 Page",
"settings.default-site.444": "No Response (444)",
"settings.default-site.congratulations": "Willkommensseite",
"settings.default-site.description": "Was angezeigt wird, wenn der Nginx eine unbekannte Webseitenanfrage bekommt",
"settings.default-site.html": "Benutzerdefinierte HTML",
"settings.default-site.html.placeholder": "<!-- Geben Sie hier Ihren benutzerdefinierten HTML-Inhalt ein. -->",
"settings.default-site.redirect": "Weiterleitung",
"setup.preamble": "Beginnen Sie mit der Erstellung Ihres Administratorkontos.",
"setup.title": "Willkommen!",
"sign-in": "Login",
"ssl-certificate": "SSL Zertifikate",
"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": "Passwort ändern",
"user.confirm-password": "Passwort wiederholen",
"user.current-password": "Aktuelles Passwort",
"user.edit-profile": "Profil bearbeiten",
"user.full-name": "Name",
"user.login-as": "Einloggen als {name}",
"user.logout": "Ausloggen",
"user.new-password": "Neues Password",
"user.nickname": "Nickname",
"user.set-password": "Passwort setzen",
"user.set-permissions": "Berechtigungen für {name} setzen",
"user.switch-dark": "Zum Dark Mode wechseln",
"user.switch-light": "Zum Light Mode wechslen",
"username": "Benutzername",
"users": "Benutzer"
}

View File

@@ -169,6 +169,7 @@
"public": "Public",
"redirection-host": "Redirection Host",
"redirection-host.forward-domain": "Forward Domain",
"redirection-host.forward-http-code": "HTTP Code",
"redirection-hosts": "Redirection Hosts",
"redirection-hosts.count": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}",
"role.admin": "Administrator",

View File

@@ -1,4 +1,5 @@
{
"locale-de-DE": "German",
"locale-en-US": "English",
"locale-es-ES": "Español"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,24 @@
// import * as de from "./de/index";
// import * as fa from "./fa/index";
import * as en from "./en/index";
import * as de from "./de/index";
const items: any = { en };
const items: any = { en, de };
const fallbackLang = "en";
export const getHelpFile = (lang: string, section: string): string => {
if (typeof items[lang] !== "undefined" && typeof items[lang][section] !== "undefined") {
if (
typeof items[lang] !== "undefined" &&
typeof items[lang][section] !== "undefined"
) {
return items[lang][section].default;
}
// Fallback to English
if (typeof items[fallbackLang] !== "undefined" && typeof items[fallbackLang][section] !== "undefined") {
if (
typeof items[fallbackLang] !== "undefined" &&
typeof items[fallbackLang][section] !== "undefined"
) {
return items[fallbackLang][section].default;
}
throw new Error(`Cannot load help doc for ${lang}-${section}`);

View File

@@ -0,0 +1,641 @@
{
"access-list": {
"defaultMessage": "Zugriffsliste"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {Regel} other {Regeln}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {User} other {Users}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Wenn mindestens eine Regel vorhanden ist, wird diese Regel zum Ablehnen aller Anfragen als letzte hinzugefügt."
},
"access-list.help.rules-order": {
"defaultMessage": "Beachten Sie, dass die Anweisungen „Erlauben“ und „Verbieten“ in der Reihenfolge ihrer Definition angewendet werden."
},
"access-list.pass-auth": {
"defaultMessage": "Authentifizierung an Upstream weiterleiten"
},
"access-list.public": {
"defaultMessage": "Öffentlich"
},
"access-list.public.subtitle": {
"defaultMessage": "Keine Authentifizierung erforderlich"
},
"access-list.satisfy-any": {
"defaultMessage": "Satisfy Any"
},
"access-list.subtitle": {
"defaultMessage": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Regel} other {Regeln}} - Erstellt: {date}"
},
"access-lists": {
"defaultMessage": "Zugrifflisten"
},
"action.add": {
"defaultMessage": "Hinzufügen"
},
"action.add-location": {
"defaultMessage": "Pfad Hinzufügen"
},
"action.close": {
"defaultMessage": "Schließen"
},
"action.delete": {
"defaultMessage": "Löschen"
},
"action.disable": {
"defaultMessage": "Deaktivieren"
},
"action.download": {
"defaultMessage": "Herunterladen"
},
"action.edit": {
"defaultMessage": "Bearbeiten"
},
"action.enable": {
"defaultMessage": "Aktivieren"
},
"action.permissions": {
"defaultMessage": "Berechtigungen"
},
"action.renew": {
"defaultMessage": "Erneuert"
},
"action.view-details": {
"defaultMessage": "Details"
},
"auditlogs": {
"defaultMessage": "Protokoll"
},
"cancel": {
"defaultMessage": "Abbrechen"
},
"certificate": {
"defaultMessage": "Zertifikat"
},
"certificate.custom-certificate": {
"defaultMessage": "Zertifikat"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Privater Schlüssel"
},
"certificate.custom-intermediate": {
"defaultMessage": "Zwischen Zertifikat"
},
"certificate.in-use": {
"defaultMessage": "In Benutzung"
},
"certificate.none.subtitle": {
"defaultMessage": "Kein Zertifikat zugewiesen"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Dieser Host verwendet kein HTTPS."
},
"certificate.none.title": {
"defaultMessage": "Kein"
},
"certificate.not-in-use": {
"defaultMessage": "Nicht in Benutzung"
},
"certificates": {
"defaultMessage": "Zertifikate"
},
"certificates.custom": {
"defaultMessage": "Benutzerdefiniertes Zertifikat"
},
"certificates.custom.warning": {
"defaultMessage": "Mit einem Passwort geschützte Schlüsseldateien werden nicht unterstützt."
},
"certificates.dns.credentials": {
"defaultMessage": "Inhalt der Anmeldedaten-Datei"
},
"certificates.dns.credentials-note": {
"defaultMessage": "Dieses Plugin erfordert eine Konfigurationsdatei, die einen API-Token oder andere Anmeldedaten für Ihren Anbieter enthält."
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Diese Daten werden als Klartext in der Datenbank und in einer Datei gespeichert!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Wartzeit in Sekunden"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Leer lassen um die Standardwartezeit des Plugins zu nutzen"
},
"certificates.dns.provider": {
"defaultMessage": "DNS Provider"
},
"certificates.dns.warning": {
"defaultMessage": "Dieser Abschnitt erfordert einige Kenntnisse über Certbot und seine DNS-Plugins. Bitte konsultieren Sie die jeweilige Plugin-Dokumentation."
},
"certificates.http.reachability-404": {
"defaultMessage": "Unter dieser Domain wurde ein Server gefunden, aber es scheint sich nicht um Nginx Proxy Manager zu handeln. Bitte stellen Sie sicher, dass Ihre Domain auf die IP-Adresse verweist, unter der Ihre NPM-Instanz ausgeführt wird."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Die Erreichbarkeit konnte aufgrund eines Kommunikationsfehlers mit site24x7.com nicht überprüft werden."
},
"certificates.http.reachability-not-resolved": {
"defaultMessage": "Unter dieser Domain ist kein Server verfügbar. Bitte stellen Sie sicher, dass Ihre Domain existiert und auf die IP-Adresse verweist, unter der Ihre NPM-Instanz läuft, und dass gegebenenfalls Port 80 in Ihrem Router weitergeleitet wird."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Ihr Server ist erreichbar und die Erstellung von Zertifikaten sollte möglich sein."
},
"certificates.http.reachability-other": {
"defaultMessage": "Unter dieser Domain wurde ein Server gefunden, der jedoch einen unerwarteten Statuscode {code} zurückgegeben hat. Handelt es sich um den NPM-Server? Bitte stellen Sie sicher, dass Ihre Domain auf die IP-Adresse verweist, unter der Ihre NPM-Instanz ausgeführt wird."
},
"certificates.http.reachability-wrong-data": {
"defaultMessage": "Unter dieser Domain wurde ein Server gefunden, der jedoch unerwartete Daten zurückgegeben hat. Handelt es sich um den NPM-Server? Bitte stellen Sie sicher, dass Ihre Domain auf die IP-Adresse verweist, unter der Ihre NPM-Instanz ausgeführt wird."
},
"certificates.http.test-results": {
"defaultMessage": "Test Ergeniss"
},
"certificates.http.warning": {
"defaultMessage": "Diese Domänen müssen bereits so konfiguriert sein, dass sie auf diese Installation verweisen."
},
"certificates.request.subtitle": {
"defaultMessage": "Über Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Anfordern eines neuen Zertifikates"
},
"column.access": {
"defaultMessage": "Zugriff"
},
"column.authorization": {
"defaultMessage": "Genehmigung"
},
"column.authorizations": {
"defaultMessage": "Genehmigungen"
},
"column.custom-locations": {
"defaultMessage": "Benutzerdefinierte Pfad"
},
"column.destination": {
"defaultMessage": "Ziel"
},
"column.details": {
"defaultMessage": "Details"
},
"column.email": {
"defaultMessage": "Email"
},
"column.event": {
"defaultMessage": "Ereignis"
},
"column.expires": {
"defaultMessage": "Verfällt am"
},
"column.http-code": {
"defaultMessage": "HTTP Code"
},
"column.incoming-port": {
"defaultMessage": "Eingehender Port"
},
"column.name": {
"defaultMessage": "Name"
},
"column.protocol": {
"defaultMessage": "Protokoll"
},
"column.provider": {
"defaultMessage": "Provider"
},
"column.roles": {
"defaultMessage": "Rollen"
},
"column.rules": {
"defaultMessage": "Regeln"
},
"column.satisfy": {
"defaultMessage": "Satisfy"
},
"column.satisfy-all": {
"defaultMessage": "Alle"
},
"column.satisfy-any": {
"defaultMessage": "Jeder"
},
"column.scheme": {
"defaultMessage": "Schema"
},
"column.source": {
"defaultMessage": "Quelle"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Status"
},
"created-on": {
"defaultMessage": "Erstelldatum: {date}"
},
"dashboard": {
"defaultMessage": "Dashboard"
},
"dead-host": {
"defaultMessage": "404 Host"
},
"dead-hosts": {
"defaultMessage": "404 Hosts"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {404 Host} other {404 Hosts}}"
},
"disabled": {
"defaultMessage": "Deaktiviert"
},
"domain-names": {
"defaultMessage": "Domain Names"
},
"domain-names.max": {
"defaultMessage": "{count} Maximale Anzahl von Domainnamen"
},
"domain-names.placeholder": {
"defaultMessage": "Eintragen der Domain..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Wildcards sind für diesen Typ nicht zulässig."
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Wildcards werden für diese Zertifizierungsstelle nicht unterstützt."
},
"domains.force-ssl": {
"defaultMessage": "Erzwinge SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS aktiviert"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS Sub-domains"
},
"domains.http2-support": {
"defaultMessage": "HTTP/2 Support"
},
"domains.use-dns": {
"defaultMessage": "Nutze DNS Challenge"
},
"email-address": {
"defaultMessage": "Email Addresse"
},
"empty-search": {
"defaultMessage": "Keine Ergebnisse gefunden"
},
"empty-subtitle": {
"defaultMessage": "Warum erstellen Sie nicht eine?"
},
"enabled": {
"defaultMessage": "aktiviert"
},
"error.access.at-least-one": {
"defaultMessage": "Entweder eine Genehmigung oder eine Zugriffsregel ist erforderlich."
},
"error.access.duplicate-usernames": {
"defaultMessage": "Autorisierung Benutzernamen müssen eindeutig sein"
},
"error.invalid-auth": {
"defaultMessage": "Ungültige E-Mail-Adresse oder Passwort"
},
"error.invalid-domain": {
"defaultMessage": "Ungültige Domain: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Ungültige E-Mail-Adresse"
},
"error.max-character-length": {
"defaultMessage": "Die maximale Länge beträgt {max} Zeichen{max, plural, one {} other {s}}"
},
"error.max-domains": {
"defaultMessage": "Zu viele Domains, maximal sind {max}"
},
"error.maximum": {
"defaultMessage": "Maximum ist {max}"
},
"error.min-character-length": {
"defaultMessage": "Die minimale Länge beträgt {min} Zeichen{min, plural, one {} other {s}}"
},
"error.minimum": {
"defaultMessage": "Minimum ist {min}"
},
"error.passwords-must-match": {
"defaultMessage": "Passwörter müssen übereinstimmen"
},
"error.required": {
"defaultMessage": "Dies ist erforderlich."
},
"expires.on": {
"defaultMessage": "Ablauf am: {date}"
},
"footer.github-fork": {
"defaultMessage": "Fork me on Github"
},
"host.flags.block-exploits": {
"defaultMessage": "Gängige Exploits blockieren"
},
"host.flags.cache-assets": {
"defaultMessage": "Cache Assets"
},
"host.flags.preserve-path": {
"defaultMessage": "Pfad beibehalten"
},
"host.flags.protocols": {
"defaultMessage": "Protokole"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Websockets Support"
},
"host.forward-port": {
"defaultMessage": "Forward Port"
},
"host.forward-scheme": {
"defaultMessage": "Schema"
},
"hosts": {
"defaultMessage": "Hosts"
},
"http-only": {
"defaultMessage": "HTTP Only"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt via DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt via HTTP"
},
"loading": {
"defaultMessage": "Laden…"
},
"login.title": {
"defaultMessage": "Account Login"
},
"nginx-config.label": {
"defaultMessage": "Benutzerdefinierte Nginx Konfiguration"
},
"nginx-config.placeholder": {
"defaultMessage": "# Geben Sie hier Ihre benutzerdefinierte Nginx-Konfiguration auf eigene Gefahr ein!"
},
"no-permission-error": {
"defaultMessage": "Sie haben keinen Zugriff, um dies anzuzeigen."
},
"notfound.action": {
"defaultMessage": "Take me home"
},
"notfound.content": {
"defaultMessage": "We are sorry but the page you are looking for was not found"
},
"notfound.title": {
"defaultMessage": "Oops… You just found an error page"
},
"notification.error": {
"defaultMessage": "Error"
},
"notification.object-deleted": {
"defaultMessage": "{object} wurde gelöscht"
},
"notification.object-disabled": {
"defaultMessage": "{object} wurde deaktiviert"
},
"notification.object-enabled": {
"defaultMessage": "{object} wurde aktiviert"
},
"notification.object-renewed": {
"defaultMessage": "{object} wurde erneuert"
},
"notification.object-saved": {
"defaultMessage": "{object} wurde gespeichert"
},
"notification.success": {
"defaultMessage": "Erfolgreich"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "{object} hinzufügen"
},
"object.delete": {
"defaultMessage": "{object} löschen"
},
"object.delete.content": {
"defaultMessage": "Bist du dir sicher das du diese(n) {object} löschen möchtest?"
},
"object.edit": {
"defaultMessage": "{object} bearbeiten"
},
"object.empty": {
"defaultMessage": "Keine {objects} vorhanden"
},
"object.event.created": {
"defaultMessage": "{object} erstellt"
},
"object.event.deleted": {
"defaultMessage": "{object} gelöscht"
},
"object.event.disabled": {
"defaultMessage": "{object} deaktiviert"
},
"object.event.enabled": {
"defaultMessage": "{object} aktiviert"
},
"object.event.renewed": {
"defaultMessage": "{object} erneuert"
},
"object.event.updated": {
"defaultMessage": "{object} aktualisiert"
},
"offline": {
"defaultMessage": "Offline"
},
"online": {
"defaultMessage": "Online"
},
"options": {
"defaultMessage": "Optionen"
},
"password": {
"defaultMessage": "Passwort"
},
"password.generate": {
"defaultMessage": "Zufälliges Passwort generieren"
},
"password.hide": {
"defaultMessage": "Passwort verstecken"
},
"password.show": {
"defaultMessage": "Passwort anzeigen"
},
"permissions.hidden": {
"defaultMessage": "Versteckt"
},
"permissions.manage": {
"defaultMessage": "Verwalten"
},
"permissions.view": {
"defaultMessage": "Nur anzeigen"
},
"permissions.visibility.all": {
"defaultMessage": "Alle Elemente"
},
"permissions.visibility.title": {
"defaultMessage": "Objekt Sichtbarkeit"
},
"permissions.visibility.user": {
"defaultMessage": "Nur erstellte Elemente"
},
"proxy-host": {
"defaultMessage": "Proxy Host"
},
"proxy-host.forward-host": {
"defaultMessage": "Forward Hostname / IP"
},
"proxy-hosts": {
"defaultMessage": "Proxy Hosts"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}"
},
"public": {
"defaultMessage": "Öffentlich"
},
"redirection-host": {
"defaultMessage": "Redirection Host"
},
"redirection-host.forward-domain": {
"defaultMessage": "Forward Domain"
},
"redirection-host.forward-http-code": {
"defaultMessage": "HTTP Code"
},
"redirection-hosts": {
"defaultMessage": "Redirection Hosts"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}"
},
"role.admin": {
"defaultMessage": "Administrator"
},
"role.standard-user": {
"defaultMessage": "Standard User"
},
"save": {
"defaultMessage": "Speichern"
},
"setting": {
"defaultMessage": "Einstellung"
},
"settings": {
"defaultMessage": "Einstellungen"
},
"settings.default-site": {
"defaultMessage": "Standard Seite"
},
"settings.default-site.404": {
"defaultMessage": "404 Page"
},
"settings.default-site.444": {
"defaultMessage": "No Response (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Willkommensseite"
},
"settings.default-site.description": {
"defaultMessage": "Was angezeigt wird, wenn der Nginx eine unbekannte Webseitenanfrage bekommt"
},
"settings.default-site.html": {
"defaultMessage": "Benutzerdefinierte HTML"
},
"settings.default-site.html.placeholder": {
"defaultMessage": "<!-- Geben Sie hier Ihren benutzerdefinierten HTML-Inhalt ein. -->"
},
"settings.default-site.redirect": {
"defaultMessage": "Weiterleitung"
},
"setup.preamble": {
"defaultMessage": "Beginnen Sie mit der Erstellung Ihres Administratorkontos."
},
"setup.title": {
"defaultMessage": "Willkommen!"
},
"sign-in": {
"defaultMessage": "Login"
},
"ssl-certificate": {
"defaultMessage": "SSL Zertifikate"
},
"stream": {
"defaultMessage": "Stream"
},
"stream.forward-host": {
"defaultMessage": "Forward Host"
},
"stream.incoming-port": {
"defaultMessage": "Incoming Port"
},
"streams": {
"defaultMessage": "Streams"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Test"
},
"user": {
"defaultMessage": "User"
},
"user.change-password": {
"defaultMessage": "Passwort ändern"
},
"user.confirm-password": {
"defaultMessage": "Passwort wiederholen"
},
"user.current-password": {
"defaultMessage": "Aktuelles Passwort"
},
"user.edit-profile": {
"defaultMessage": "Profil bearbeiten"
},
"user.full-name": {
"defaultMessage": "Name"
},
"user.login-as": {
"defaultMessage": "Einloggen als {name}"
},
"user.logout": {
"defaultMessage": "Ausloggen"
},
"user.new-password": {
"defaultMessage": "Neues Password"
},
"user.nickname": {
"defaultMessage": "Nickname"
},
"user.set-password": {
"defaultMessage": "Passwort setzen"
},
"user.set-permissions": {
"defaultMessage": "Berechtigungen für {name} setzen"
},
"user.switch-dark": {
"defaultMessage": "Zum Dark Mode wechseln"
},
"user.switch-light": {
"defaultMessage": "Zum Light Mode wechslen"
},
"username": {
"defaultMessage": "Benutzername"
},
"users": {
"defaultMessage": "Benutzer"
}
}

View File

@@ -509,6 +509,9 @@
"redirection-host.forward-domain": {
"defaultMessage": "Forward Domain"
},
"redirection-host.forward-http-code": {
"defaultMessage": "HTTP Code"
},
"redirection-hosts": {
"defaultMessage": "Redirection Hosts"
},

View File

@@ -4,5 +4,8 @@
},
"locale-es-ES": {
"defaultMessage": "Español"
},
"locale-de-DE": {
"defaultMessage": "German"
}
}

View File

@@ -162,7 +162,7 @@ const RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) =
required
{...field}
>
<option value="$scheme">Auto</option>
<option value="auto">Auto</option>
<option value="http">http</option>
<option value="https">https</option>
</select>
@@ -212,6 +212,36 @@ const RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) =
</Field>
</div>
</div>
<Field name="forwardHttpCode">
{({ field, form }: any) => (
<div className="mb-3">
<label className="form-label" htmlFor="forwardHttpCode">
<T id="redirection-host.forward-http-code" />
</label>
<select
id="forwardHttpCode"
className={`form-control ${form.errors.forwardHttpCode && form.touched.forwardHttpCode ? "is-invalid" : ""}`}
required
{...field}
>
<option value="300">300 Multiple choices</option>
<option value="301">301 Moved permanently</option>
<option value="302">302 Moved temporarily</option>
<option value="303">303 See other</option>
<option value="307">307 Temporary redirect</option>
<option value="308">308 Permanent redirect</option>
</select>
{form.errors.forwardHttpCode ? (
<div className="invalid-feedback">
{form.errors.forwardHttpCode &&
form.touched.forwardHttpCode
? form.errors.forwardHttpCode
: null}
</div>
) : null}
</div>
)}
</Field>
<div className="my-3">
<h4 className="py-2">
<T id="options" />

View File

@@ -44,7 +44,7 @@ const validateEmail = () => {
if (!value.length) {
return intl.formatMessage({ id: "error.required" });
}
if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)) {
if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+$/i.test(value)) {
return intl.formatMessage({ id: "error.invalid-email" });
}
};

View File

@@ -16,7 +16,7 @@ if hash docker 2>/dev/null; then
-e NODE_OPTIONS=--openssl-legacy-provider \
-v "$(pwd)/frontend:/app/frontend" \
-w /app/frontend "${DOCKER_IMAGE}" \
sh -c "yarn install && yarn lint && yarn build && chown -R $(id -u):$(id -g) /app/frontend"
sh -c "yarn install && yarn lint && yarn vitest run && yarn build && chown -R $(id -u):$(id -g) /app/frontend"
echo -e "${BLUE} ${GREEN}Building Frontend Complete${RESET}"
else