mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2026-01-21 19:25:43 +00:00
Compare commits
258 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3efaae320 | ||
|
|
7b3c1fd061 | ||
|
|
ee42202348 | ||
|
|
c1ad7788f1 | ||
|
|
d33bb02c74 | ||
|
|
462c134751 | ||
|
|
b7dfaddbb1 | ||
|
|
11ee4f0820 | ||
|
|
19970a4220 | ||
|
|
59bac3b468 | ||
|
|
48753fb101 | ||
|
|
2a3978ae3f | ||
|
|
4ce5da5930 | ||
|
|
89d3756ee6 | ||
|
|
58c63096e4 | ||
|
|
b01a22c393 | ||
|
|
9c25410331 | ||
|
|
b3a901bbc5 | ||
|
|
3e3396ba9a | ||
|
|
3eb493bb8b | ||
|
|
8c8221a352 | ||
|
|
582681e3ff | ||
|
|
52fae6d35f | ||
|
|
6c0ea835ce | ||
|
|
fb52655374 | ||
|
|
336726db8d | ||
|
|
4a7853163e | ||
|
|
b30f8e47e2 | ||
|
|
6fa30840be | ||
|
|
05726aaab9 | ||
|
|
f85bb79f13 | ||
|
|
471b62c7fe | ||
|
|
55a1e0a4e7 | ||
|
|
f25afa3590 | ||
|
|
9211ba6d1a | ||
|
|
aeb44244a7 | ||
|
|
d2d204ab8e | ||
|
|
427afa55b4 | ||
|
|
bbe98a639a | ||
|
|
f0c0b465d9 | ||
|
|
6c2f6a9d39 | ||
|
|
2f6e3ad804 | ||
|
|
5e6ead1eee | ||
|
|
da519e72ba | ||
|
|
b13ebb2247 | ||
|
|
6b322582b9 | ||
|
|
7fe5070337 | ||
|
|
1b8f1fbb79 | ||
|
|
4abea1247d | ||
|
|
073ee95e56 | ||
|
|
fec8b3b083 | ||
|
|
168078eb40 | ||
|
|
2c9f8f4d64 | ||
|
|
8403a0c761 | ||
|
|
d18c8cf4f1 | ||
|
|
bf4eab541a | ||
|
|
f9edcb10e6 | ||
|
|
ba43c144f6 | ||
|
|
896951f6cd | ||
|
|
865b566ea6 | ||
|
|
45bc44c6fa | ||
|
|
4ff402fff4 | ||
|
|
1c6f54fa3c | ||
|
|
e8ca72fb6a | ||
|
|
4712633568 | ||
|
|
a1fb54c394 | ||
|
|
927e57257b | ||
|
|
e353a66556 | ||
|
|
991bddf891 | ||
|
|
c076ad145c | ||
|
|
80cf4406d5 | ||
|
|
3cb124d5a0 | ||
|
|
03b0513a24 | ||
|
|
0528d65317 | ||
|
|
f9991084fc | ||
|
|
56875bba52 | ||
|
|
b55f51bd63 | ||
|
|
20e2d5ffb3 | ||
|
|
86b7394620 | ||
|
|
91a1f39c02 | ||
|
|
5c114e9db7 | ||
|
|
fec9bffe29 | ||
|
|
e3cdc8bb30 | ||
|
|
ba79eefe5e | ||
|
|
bb94ce75c1 | ||
|
|
847c58b170 | ||
|
|
89b8b747e1 | ||
|
|
3231023513 | ||
|
|
dc89635971 | ||
|
|
cfa98361d1 | ||
|
|
c2177abe39 | ||
|
|
2c6d614597 | ||
|
|
484ce8db3c | ||
|
|
2c11c0c7e2 | ||
|
|
f1039ce2ef | ||
|
|
d49ff6e7c2 | ||
|
|
a87f24c9dc | ||
|
|
decdfec447 | ||
|
|
32ab3faf57 | ||
|
|
c7f999fa7a | ||
|
|
de7d3b0d19 | ||
|
|
2d4b7399c0 | ||
|
|
316b758455 | ||
|
|
890d06c863 | ||
|
|
81f2aa17d4 | ||
|
|
9b4c34915c | ||
|
|
fce569ca21 | ||
|
|
87ec9c4bdf | ||
|
|
2650648d68 | ||
|
|
fdc0c29f28 | ||
|
|
6cae088432 | ||
|
|
9d8c4cc30b | ||
|
|
66ebecdb43 | ||
|
|
60f3ee03c0 | ||
|
|
a4d54a0291 | ||
|
|
7536b1b1c9 | ||
|
|
5288fbd7af | ||
|
|
2c630bbdca | ||
|
|
0ec1a09c30 | ||
|
|
118c4793e3 | ||
|
|
d7384c568f | ||
|
|
0bcfe0bba6 | ||
|
|
74cbfb2c58 | ||
|
|
8ef65caa5a | ||
|
|
bc341c1dff | ||
|
|
5fc9febf1f | ||
|
|
b23ceebfd8 | ||
|
|
c281fc54a1 | ||
|
|
d0f7dc5b48 | ||
|
|
fb53df862e | ||
|
|
8d8463ae41 | ||
|
|
8774cfe5f9 | ||
|
|
4ca5cadd19 | ||
|
|
45a8d50e03 | ||
|
|
960d4bfe6f | ||
|
|
8c3c964c52 | ||
|
|
afd6134a3e | ||
|
|
9b2d60e67b | ||
|
|
9807e25d45 | ||
|
|
824c895f52 | ||
|
|
7f9b9dfea4 | ||
|
|
d848ba9f65 | ||
|
|
47db5c9aa6 | ||
|
|
79a9653b26 | ||
|
|
e5aae1f365 | ||
|
|
8959190d32 | ||
|
|
7e875eb27a | ||
|
|
cf7306e766 | ||
|
|
1c442dcce6 | ||
|
|
dadd10f89b | ||
|
|
8838dabe8a | ||
|
|
75c012b558 | ||
|
|
9be1381ffe | ||
|
|
f40fe56572 | ||
|
|
b4fd242eb7 | ||
|
|
911476f82f | ||
|
|
963125f963 | ||
|
|
e86a34f2f3 | ||
|
|
6ce9567e48 | ||
|
|
f02145c5ef | ||
|
|
66fa08fd8e | ||
|
|
d783cc3b90 | ||
|
|
17cc75fe7d | ||
|
|
15394c6532 | ||
|
|
2d6252d75d | ||
|
|
adee0e39de | ||
|
|
5dde98cf3e | ||
|
|
c41451618e | ||
|
|
1a3d45f6bc | ||
|
|
2ea54975b6 | ||
|
|
0373017a9f | ||
|
|
b043e70fc0 | ||
|
|
2b5182d339 | ||
|
|
3c5ff81a54 | ||
|
|
8aa46c1f40 | ||
|
|
b26db50ae7 | ||
|
|
d66bb2104a | ||
|
|
8e900dbc92 | ||
|
|
66aac3eb3e | ||
|
|
221c3eddbc | ||
|
|
8460b28597 | ||
|
|
0344bb3c19 | ||
|
|
1a36bdce76 | ||
|
|
06d7db43f7 | ||
|
|
4557244744 | ||
|
|
f649288098 | ||
|
|
28df6db52b | ||
|
|
eee749652c | ||
|
|
f6aa25b9b3 | ||
|
|
40db26b686 | ||
|
|
f36d4e6906 | ||
|
|
86c7cbddab | ||
|
|
e52975bf6c | ||
|
|
ff792f76af | ||
|
|
711f312b71 | ||
|
|
9f0f89ff03 | ||
|
|
f3633cb696 | ||
|
|
8773ce25d7 | ||
|
|
c3954e9845 | ||
|
|
87eef10ff8 | ||
|
|
dc03ad8239 | ||
|
|
441a7262cd | ||
|
|
1600599410 | ||
|
|
74d381e7fa | ||
|
|
ae5faa75fa | ||
|
|
ba79bbc750 | ||
|
|
a7231777aa | ||
|
|
2578105f86 | ||
|
|
3a6b221b0c | ||
|
|
12b000abb9 | ||
|
|
39c9bbb167 | ||
|
|
30c2781a02 | ||
|
|
53e78dcc17 | ||
|
|
62092b2ddc | ||
|
|
2c26ed8b11 | ||
|
|
e3f5cd9a58 | ||
|
|
fba14817e7 | ||
|
|
6825a9773b | ||
|
|
8bc3078d87 | ||
|
|
8aeb2fa661 | ||
|
|
4bd545c88e | ||
|
|
7f0cce944d | ||
|
|
7cde6ee7ca | ||
|
|
df1b414c2e | ||
|
|
b6dbb68ef3 | ||
|
|
b434bba12f | ||
|
|
f1d7203212 | ||
|
|
990ba28831 | ||
|
|
311d6a1541 | ||
|
|
5e7276e65b | ||
|
|
2bcb942f93 | ||
|
|
b3dac3df08 | ||
|
|
64c5a863f8 | ||
|
|
cd94863850 | ||
|
|
fd1d33444a | ||
|
|
5aa56c63d4 | ||
|
|
8fdb6091f3 | ||
|
|
58182fcbdf | ||
|
|
b3b1e94b8c | ||
|
|
6fa2d6a98a | ||
|
|
3c252db46f | ||
|
|
8eba31913f | ||
|
|
e4e3415120 | ||
|
|
a03bb7ebce | ||
|
|
51e25d1a40 | ||
|
|
123f7d1999 | ||
|
|
9de40f067b | ||
|
|
b21d6d9d78 | ||
|
|
bf1ad15ed7 | ||
|
|
1209303a1d | ||
|
|
cd3a09ebf6 | ||
|
|
d0e20d4f1b | ||
|
|
ceb098fcfe | ||
|
|
639ba3a525 | ||
|
|
a85b5f664f | ||
|
|
7e28d8a5d6 | ||
|
|
8991e88ff3 | ||
|
|
e2a8ffa2d3 |
285
Jenkinsfile
vendored
285
Jenkinsfile
vendored
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://nginxproxymanager.com/github.png">
|
<img src="https://nginxproxymanager.com/github.png">
|
||||||
<br><br>
|
<br><br>
|
||||||
<img src="https://img.shields.io/badge/version-2.13.1-green.svg?style=for-the-badge">
|
<img src="https://img.shields.io/badge/version-2.13.6-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>
|
||||||
|
|||||||
@@ -26,8 +26,8 @@
|
|||||||
"azure": {
|
"azure": {
|
||||||
"name": "Azure",
|
"name": "Azure",
|
||||||
"package_name": "certbot-dns-azure",
|
"package_name": "certbot-dns-azure",
|
||||||
"version": "~=1.2.0",
|
"version": "~=2.6.1",
|
||||||
"dependencies": "",
|
"dependencies": "azure-mgmt-dns==8.2.0",
|
||||||
"credentials": "# This plugin supported API authentication using either Service Principals or utilizing a Managed Identity assigned to the virtual machine.\n# Regardless which authentication method used, the identity will need the “DNS Zone Contributor” role assigned to it.\n# As multiple Azure DNS Zones in multiple resource groups can exist, the config file needs a mapping of zone to resource group ID. Multiple zones -> ID mappings can be listed by using the key dns_azure_zoneX where X is a unique number. At least 1 zone mapping is required.\n\n# Using a service principal (option 1)\ndns_azure_sp_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\ndns_azure_sp_client_secret = E-xqXU83Y-jzTI6xe9fs2YC~mck3ZzUih9\ndns_azure_tenant_id = ed1090f3-ab18-4b12-816c-599af8a88cf7\n\n# Using used assigned MSI (option 2)\n# dns_azure_msi_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\n\n# Using system assigned MSI (option 3)\n# dns_azure_msi_system_assigned = true\n\n# Zones (at least one always required)\ndns_azure_zone1 = example.com:/subscriptions/c135abce-d87d-48df-936c-15596c6968a5/resourceGroups/dns1\ndns_azure_zone2 = example.org:/subscriptions/99800903-fb14-4992-9aff-12eaf2744622/resourceGroups/dns2",
|
"credentials": "# This plugin supported API authentication using either Service Principals or utilizing a Managed Identity assigned to the virtual machine.\n# Regardless which authentication method used, the identity will need the “DNS Zone Contributor” role assigned to it.\n# As multiple Azure DNS Zones in multiple resource groups can exist, the config file needs a mapping of zone to resource group ID. Multiple zones -> ID mappings can be listed by using the key dns_azure_zoneX where X is a unique number. At least 1 zone mapping is required.\n\n# Using a service principal (option 1)\ndns_azure_sp_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\ndns_azure_sp_client_secret = E-xqXU83Y-jzTI6xe9fs2YC~mck3ZzUih9\ndns_azure_tenant_id = ed1090f3-ab18-4b12-816c-599af8a88cf7\n\n# Using used assigned MSI (option 2)\n# dns_azure_msi_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\n\n# Using system assigned MSI (option 3)\n# dns_azure_msi_system_assigned = true\n\n# Zones (at least one always required)\ndns_azure_zone1 = example.com:/subscriptions/c135abce-d87d-48df-936c-15596c6968a5/resourceGroups/dns1\ndns_azure_zone2 = example.org:/subscriptions/99800903-fb14-4992-9aff-12eaf2744622/resourceGroups/dns2",
|
||||||
"full_plugin_name": "dns-azure"
|
"full_plugin_name": "dns-azure"
|
||||||
},
|
},
|
||||||
@@ -255,6 +255,14 @@
|
|||||||
"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",
|
||||||
@@ -287,6 +295,14 @@
|
|||||||
"credentials": "dns_he_user = Me\ndns_he_pass = my HE password",
|
"credentials": "dns_he_user = Me\ndns_he_pass = my HE password",
|
||||||
"full_plugin_name": "dns-he"
|
"full_plugin_name": "dns-he"
|
||||||
},
|
},
|
||||||
|
"he-ddns": {
|
||||||
|
"name": "Hurricane Electric - DDNS",
|
||||||
|
"package_name": "certbot-dns-he-ddns",
|
||||||
|
"version": "~=0.1.0",
|
||||||
|
"dependencies": "",
|
||||||
|
"credentials": "dns_he_ddns_password = verysecurepassword",
|
||||||
|
"full_plugin_name": "dns-he-ddns"
|
||||||
|
},
|
||||||
"hetzner": {
|
"hetzner": {
|
||||||
"name": "Hetzner",
|
"name": "Hetzner",
|
||||||
"package_name": "certbot-dns-hetzner",
|
"package_name": "certbot-dns-hetzner",
|
||||||
@@ -367,10 +383,18 @@
|
|||||||
"credentials": "dns_joker_username = <Dynamic DNS Authentication Username>\ndns_joker_password = <Dynamic DNS Authentication Password>\ndns_joker_domain = <Dynamic DNS Domain>",
|
"credentials": "dns_joker_username = <Dynamic DNS Authentication Username>\ndns_joker_password = <Dynamic DNS Authentication Password>\ndns_joker_domain = <Dynamic DNS Domain>",
|
||||||
"full_plugin_name": "dns-joker"
|
"full_plugin_name": "dns-joker"
|
||||||
},
|
},
|
||||||
|
"kas": {
|
||||||
|
"name": "All-Inkl",
|
||||||
|
"package_name": "certbot-dns-kas",
|
||||||
|
"version": "~=0.1.1",
|
||||||
|
"dependencies": "kasserver",
|
||||||
|
"credentials": "dns_kas_user = your_kas_user\ndns_kas_password = your_kas_password",
|
||||||
|
"full_plugin_name": "dns-kas"
|
||||||
|
},
|
||||||
"leaseweb": {
|
"leaseweb": {
|
||||||
"name": "LeaseWeb",
|
"name": "LeaseWeb",
|
||||||
"package_name": "certbot-dns-leaseweb",
|
"package_name": "certbot-dns-leaseweb",
|
||||||
"version": "~=1.0.1",
|
"version": "~=1.0.3",
|
||||||
"dependencies": "",
|
"dependencies": "",
|
||||||
"credentials": "dns_leaseweb_api_token = 01234556789",
|
"credentials": "dns_leaseweb_api_token = 01234556789",
|
||||||
"full_plugin_name": "dns-leaseweb"
|
"full_plugin_name": "dns-leaseweb"
|
||||||
@@ -399,6 +423,14 @@
|
|||||||
"credentials": "dns_luadns_email = user@example.com\ndns_luadns_token = 0123456789abcdef0123456789abcdef",
|
"credentials": "dns_luadns_email = user@example.com\ndns_luadns_token = 0123456789abcdef0123456789abcdef",
|
||||||
"full_plugin_name": "dns-luadns"
|
"full_plugin_name": "dns-luadns"
|
||||||
},
|
},
|
||||||
|
"mchost24": {
|
||||||
|
"name": "MC-HOST24",
|
||||||
|
"package_name": "certbot-dns-mchost24",
|
||||||
|
"version": "",
|
||||||
|
"dependencies": "",
|
||||||
|
"credentials": "# Obtain API token using https://github.com/JoeJoeTV/mchost24-api-python\ndns_mchost24_api_token=<insert obtained API token here>",
|
||||||
|
"full_plugin_name": "dns-mchost24"
|
||||||
|
},
|
||||||
"mijnhost": {
|
"mijnhost": {
|
||||||
"name": "mijn.host",
|
"name": "mijn.host",
|
||||||
"package_name": "certbot-dns-mijn-host",
|
"package_name": "certbot-dns-mijn-host",
|
||||||
@@ -474,7 +506,7 @@
|
|||||||
"porkbun": {
|
"porkbun": {
|
||||||
"name": "Porkbun",
|
"name": "Porkbun",
|
||||||
"package_name": "certbot-dns-porkbun",
|
"package_name": "certbot-dns-porkbun",
|
||||||
"version": "~=0.9",
|
"version": "~=0.11.0",
|
||||||
"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"
|
||||||
@@ -519,6 +551,14 @@
|
|||||||
"credentials": "[default]\naws_access_key_id=AKIAIOSFODNN7EXAMPLE\naws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
"credentials": "[default]\naws_access_key_id=AKIAIOSFODNN7EXAMPLE\naws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||||
"full_plugin_name": "dns-route53"
|
"full_plugin_name": "dns-route53"
|
||||||
},
|
},
|
||||||
|
"simply": {
|
||||||
|
"name": "Simply",
|
||||||
|
"package_name": "certbot-dns-simply",
|
||||||
|
"version": "~=0.1.2",
|
||||||
|
"dependencies": "",
|
||||||
|
"credentials": "dns_simply_account_name = UExxxxxx\ndns_simply_api_key = DsHJdsjh2812872sahj",
|
||||||
|
"full_plugin_name": "dns-simply"
|
||||||
|
},
|
||||||
"spaceship": {
|
"spaceship": {
|
||||||
"name": "Spaceship",
|
"name": "Spaceship",
|
||||||
"package_name": "certbot-dns-spaceship",
|
"package_name": "certbot-dns-spaceship",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import knex from "knex";
|
import knex from "knex";
|
||||||
import {configGet, configHas} from "./lib/config.js";
|
import {configGet, configHas} from "./lib/config.js";
|
||||||
|
|
||||||
|
let instance = null;
|
||||||
|
|
||||||
const generateDbConfig = () => {
|
const generateDbConfig = () => {
|
||||||
if (!configHas("database")) {
|
if (!configHas("database")) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -30,4 +32,11 @@ const generateDbConfig = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default knex(generateDbConfig());
|
const getInstance = () => {
|
||||||
|
if (!instance) {
|
||||||
|
instance = knex(generateDbConfig());
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getInstance;
|
||||||
|
|||||||
288
backend/internal/2fa.js
Normal file
288
backend/internal/2fa.js
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
import { authenticator } from "otplib";
|
||||||
|
import errs from "../lib/error.js";
|
||||||
|
import authModel from "../models/auth.js";
|
||||||
|
import internalUser from "./user.js";
|
||||||
|
|
||||||
|
const APP_NAME = "Nginx Proxy Manager";
|
||||||
|
const BACKUP_CODE_COUNT = 8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate backup codes
|
||||||
|
* @returns {Promise<{plain: string[], hashed: string[]}>}
|
||||||
|
*/
|
||||||
|
const generateBackupCodes = async () => {
|
||||||
|
const plain = [];
|
||||||
|
const hashed = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < BACKUP_CODE_COUNT; i++) {
|
||||||
|
const code = crypto.randomBytes(4).toString("hex").toUpperCase();
|
||||||
|
plain.push(code);
|
||||||
|
const hash = await bcrypt.hash(code, 10);
|
||||||
|
hashed.push(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { plain, hashed };
|
||||||
|
};
|
||||||
|
|
||||||
|
const internal2fa = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has 2FA enabled
|
||||||
|
* @param {number} userId
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
isEnabled: async (userId) => {
|
||||||
|
const auth = await internal2fa.getUserPasswordAuth(userId);
|
||||||
|
return auth?.meta?.totp_enabled === true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get 2FA status for user
|
||||||
|
* @param {Access} access
|
||||||
|
* @param {number} userId
|
||||||
|
* @returns {Promise<{enabled: boolean, backup_codes_remaining: number}>}
|
||||||
|
*/
|
||||||
|
getStatus: async (access, userId) => {
|
||||||
|
await access.can("users:password", userId);
|
||||||
|
await internalUser.get(access, { id: userId });
|
||||||
|
const auth = await internal2fa.getUserPasswordAuth(userId);
|
||||||
|
const enabled = auth?.meta?.totp_enabled === true;
|
||||||
|
let backup_codes_remaining = 0;
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
const backupCodes = auth.meta.backup_codes || [];
|
||||||
|
backup_codes_remaining = backupCodes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
backup_codes_remaining,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start 2FA setup - store pending secret
|
||||||
|
*
|
||||||
|
* @param {Access} access
|
||||||
|
* @param {number} userId
|
||||||
|
* @returns {Promise<{secret: string, otpauth_url: string}>}
|
||||||
|
*/
|
||||||
|
startSetup: async (access, userId) => {
|
||||||
|
await access.can("users:password", userId);
|
||||||
|
const user = await internalUser.get(access, { id: userId });
|
||||||
|
const secret = authenticator.generateSecret();
|
||||||
|
const otpauth_url = authenticator.keyuri(user.email, APP_NAME, secret);
|
||||||
|
const auth = await internal2fa.getUserPasswordAuth(userId);
|
||||||
|
|
||||||
|
// ensure user isn't already setup for 2fa
|
||||||
|
const enabled = auth?.meta?.totp_enabled === true;
|
||||||
|
if (enabled) {
|
||||||
|
throw new errs.ValidationError("2FA is already enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = auth.meta || {};
|
||||||
|
meta.totp_pending_secret = secret;
|
||||||
|
|
||||||
|
await authModel.query()
|
||||||
|
.where("id", auth.id)
|
||||||
|
.andWhere("user_id", userId)
|
||||||
|
.andWhere("type", "password")
|
||||||
|
.patch({ meta });
|
||||||
|
|
||||||
|
return { secret, otpauth_url };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable 2FA after verifying code
|
||||||
|
*
|
||||||
|
* @param {Access} access
|
||||||
|
* @param {number} userId
|
||||||
|
* @param {string} code
|
||||||
|
* @returns {Promise<{backup_codes: string[]}>}
|
||||||
|
*/
|
||||||
|
enable: async (access, userId, code) => {
|
||||||
|
await access.can("users:password", userId);
|
||||||
|
await internalUser.get(access, { id: userId });
|
||||||
|
const auth = await internal2fa.getUserPasswordAuth(userId);
|
||||||
|
const secret = auth?.meta?.totp_pending_secret || false;
|
||||||
|
|
||||||
|
if (!secret) {
|
||||||
|
throw new errs.ValidationError("No pending 2FA setup found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = authenticator.verify({ token: code, secret });
|
||||||
|
if (!valid) {
|
||||||
|
throw new errs.ValidationError("Invalid verification code");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { plain, hashed } = await generateBackupCodes();
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
...auth.meta,
|
||||||
|
totp_secret: secret,
|
||||||
|
totp_enabled: true,
|
||||||
|
totp_enabled_at: new Date().toISOString(),
|
||||||
|
backup_codes: hashed,
|
||||||
|
};
|
||||||
|
delete meta.totp_pending_secret;
|
||||||
|
|
||||||
|
await authModel
|
||||||
|
.query()
|
||||||
|
.where("id", auth.id)
|
||||||
|
.andWhere("user_id", userId)
|
||||||
|
.andWhere("type", "password")
|
||||||
|
.patch({ meta });
|
||||||
|
|
||||||
|
return { backup_codes: plain };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable 2FA
|
||||||
|
*
|
||||||
|
* @param {Access} access
|
||||||
|
* @param {number} userId
|
||||||
|
* @param {string} code
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
disable: async (access, userId, code) => {
|
||||||
|
await access.can("users:password", userId);
|
||||||
|
await internalUser.get(access, { id: userId });
|
||||||
|
const auth = await internal2fa.getUserPasswordAuth(userId);
|
||||||
|
|
||||||
|
const enabled = auth?.meta?.totp_enabled === true;
|
||||||
|
if (!enabled) {
|
||||||
|
throw new errs.ValidationError("2FA is not enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = authenticator.verify({
|
||||||
|
token: code,
|
||||||
|
secret: auth.meta.totp_secret,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new errs.AuthError("Invalid verification code");
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = { ...auth.meta };
|
||||||
|
delete meta.totp_secret;
|
||||||
|
delete meta.totp_enabled;
|
||||||
|
delete meta.totp_enabled_at;
|
||||||
|
delete meta.backup_codes;
|
||||||
|
|
||||||
|
await authModel
|
||||||
|
.query()
|
||||||
|
.where("id", auth.id)
|
||||||
|
.andWhere("user_id", userId)
|
||||||
|
.andWhere("type", "password")
|
||||||
|
.patch({ meta });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify 2FA code for login
|
||||||
|
*
|
||||||
|
* @param {number} userId
|
||||||
|
* @param {string} token
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
verifyForLogin: async (userId, token) => {
|
||||||
|
const auth = await internal2fa.getUserPasswordAuth(userId);
|
||||||
|
const secret = auth?.meta?.totp_secret || false;
|
||||||
|
|
||||||
|
if (!secret) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try TOTP code first
|
||||||
|
const valid = authenticator.verify({
|
||||||
|
token,
|
||||||
|
secret,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (valid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try backup codes
|
||||||
|
const backupCodes = auth?.meta?.backup_codes || [];
|
||||||
|
for (let i = 0; i < backupCodes.length; i++) {
|
||||||
|
const match = await bcrypt.compare(code.toUpperCase(), backupCodes[i]);
|
||||||
|
if (match) {
|
||||||
|
// Remove used backup code
|
||||||
|
const updatedCodes = [...backupCodes];
|
||||||
|
updatedCodes.splice(i, 1);
|
||||||
|
const meta = { ...auth.meta, backup_codes: updatedCodes };
|
||||||
|
await authModel
|
||||||
|
.query()
|
||||||
|
.where("id", auth.id)
|
||||||
|
.andWhere("user_id", userId)
|
||||||
|
.andWhere("type", "password")
|
||||||
|
.patch({ meta });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate backup codes
|
||||||
|
*
|
||||||
|
* @param {Access} access
|
||||||
|
* @param {number} userId
|
||||||
|
* @param {string} token
|
||||||
|
* @returns {Promise<{backup_codes: string[]}>}
|
||||||
|
*/
|
||||||
|
regenerateBackupCodes: async (access, userId, token) => {
|
||||||
|
await access.can("users:password", userId);
|
||||||
|
await internalUser.get(access, { id: userId });
|
||||||
|
const auth = await internal2fa.getUserPasswordAuth(userId);
|
||||||
|
const enabled = auth?.meta?.totp_enabled === true;
|
||||||
|
const secret = auth?.meta?.totp_secret || false;
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
throw new errs.ValidationError("2FA is not enabled");
|
||||||
|
}
|
||||||
|
if (!secret) {
|
||||||
|
throw new errs.ValidationError("No 2FA secret found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = authenticator.verify({
|
||||||
|
token,
|
||||||
|
secret,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new errs.ValidationError("Invalid verification code");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { plain, hashed } = await generateBackupCodes();
|
||||||
|
|
||||||
|
const meta = { ...auth.meta, backup_codes: hashed };
|
||||||
|
await authModel
|
||||||
|
.query()
|
||||||
|
.where("id", auth.id)
|
||||||
|
.andWhere("user_id", userId)
|
||||||
|
.andWhere("type", "password")
|
||||||
|
.patch({ meta });
|
||||||
|
|
||||||
|
return { backup_codes: plain };
|
||||||
|
},
|
||||||
|
|
||||||
|
getUserPasswordAuth: async (userId) => {
|
||||||
|
const auth = await authModel
|
||||||
|
.query()
|
||||||
|
.where("user_id", userId)
|
||||||
|
.andWhere("type", "password")
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!auth) {
|
||||||
|
throw new errs.ItemNotFoundError("Auth not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default internal2fa;
|
||||||
@@ -798,6 +798,11 @@ const internalCertificate = {
|
|||||||
certificate.domain_names.join(","),
|
certificate.domain_names.join(","),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Add key-type parameter if specified
|
||||||
|
if (certificate.meta?.key_type) {
|
||||||
|
args.push("--key-type", certificate.meta.key_type);
|
||||||
|
}
|
||||||
|
|
||||||
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id);
|
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id);
|
||||||
args.push(...adds.args);
|
args.push(...adds.args);
|
||||||
|
|
||||||
@@ -858,6 +863,11 @@ const internalCertificate = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add key-type parameter if specified
|
||||||
|
if (certificate.meta?.key_type) {
|
||||||
|
args.push("--key-type", certificate.meta.key_type);
|
||||||
|
}
|
||||||
|
|
||||||
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);
|
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);
|
||||||
args.push(...adds.args);
|
args.push(...adds.args);
|
||||||
|
|
||||||
@@ -938,6 +948,11 @@ const internalCertificate = {
|
|||||||
"--disable-hook-validation",
|
"--disable-hook-validation",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Add key-type parameter if specified
|
||||||
|
if (certificate.meta?.key_type) {
|
||||||
|
args.push("--key-type", certificate.meta.key_type);
|
||||||
|
}
|
||||||
|
|
||||||
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);
|
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);
|
||||||
args.push(...adds.args);
|
args.push(...adds.args);
|
||||||
|
|
||||||
@@ -979,6 +994,11 @@ const internalCertificate = {
|
|||||||
"--no-random-sleep-on-renew",
|
"--no-random-sleep-on-renew",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Add key-type parameter if specified
|
||||||
|
if (certificate.meta?.key_type) {
|
||||||
|
args.push("--key-type", certificate.meta.key_type);
|
||||||
|
}
|
||||||
|
|
||||||
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);
|
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);
|
||||||
args.push(...adds.args);
|
args.push(...adds.args);
|
||||||
|
|
||||||
|
|||||||
@@ -216,6 +216,11 @@ const internalNginx = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For redirection hosts, if the scheme is not http or https, set it to $scheme
|
||||||
|
if (nice_host_type === "redirection_host" && ['http', 'https'].indexOf(host.forward_scheme.toLowerCase()) === -1) {
|
||||||
|
host.forward_scheme = "$scheme";
|
||||||
|
}
|
||||||
|
|
||||||
if (host.locations) {
|
if (host.locations) {
|
||||||
//logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2));
|
//logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2));
|
||||||
origLocations = [].concat(host.locations);
|
origLocations = [].concat(host.locations);
|
||||||
|
|||||||
84
backend/internal/remote-version.js
Normal file
84
backend/internal/remote-version.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import https from "node:https";
|
||||||
|
import { ProxyAgent } from "proxy-agent";
|
||||||
|
import { debug, remoteVersion as logger } from "../logger.js";
|
||||||
|
import pjson from "../package.json" with { type: "json" };
|
||||||
|
|
||||||
|
const VERSION_URL = "https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager/releases/latest";
|
||||||
|
|
||||||
|
const internalRemoteVersion = {
|
||||||
|
cache_timeout: 1000 * 60 * 15, // 15 minutes
|
||||||
|
last_result: null,
|
||||||
|
last_fetch_time: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the latest version info, using a cached result if within the cache timeout period.
|
||||||
|
* @return {Promise<{current: string, latest: string, update_available: boolean}>} Version info
|
||||||
|
*/
|
||||||
|
get: async () => {
|
||||||
|
if (
|
||||||
|
!internalRemoteVersion.last_result ||
|
||||||
|
!internalRemoteVersion.last_fetch_time ||
|
||||||
|
Date.now() - internalRemoteVersion.last_fetch_time > internalRemoteVersion.cache_timeout
|
||||||
|
) {
|
||||||
|
const raw = await internalRemoteVersion.fetchUrl(VERSION_URL);
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
internalRemoteVersion.last_result = data;
|
||||||
|
internalRemoteVersion.last_fetch_time = Date.now();
|
||||||
|
} else {
|
||||||
|
debug(logger, "Using cached remote version result");
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestVersion = internalRemoteVersion.last_result.tag_name;
|
||||||
|
const version = pjson.version.split("-").shift().split(".");
|
||||||
|
const currentVersion = `v${version[0]}.${version[1]}.${version[2]}`;
|
||||||
|
return {
|
||||||
|
current: currentVersion,
|
||||||
|
latest: latestVersion,
|
||||||
|
update_available: internalRemoteVersion.compareVersions(currentVersion, latestVersion),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchUrl: (url) => {
|
||||||
|
const agent = new ProxyAgent();
|
||||||
|
const headers = {
|
||||||
|
"User-Agent": `NginxProxyManager v${pjson.version}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
logger.info(`Fetching ${url}`);
|
||||||
|
return https
|
||||||
|
.get(url, { agent, headers }, (res) => {
|
||||||
|
res.setEncoding("utf8");
|
||||||
|
let raw_data = "";
|
||||||
|
res.on("data", (chunk) => {
|
||||||
|
raw_data += chunk;
|
||||||
|
});
|
||||||
|
res.on("end", () => {
|
||||||
|
resolve(raw_data);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on("error", (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
compareVersions: (current, latest) => {
|
||||||
|
const cleanCurrent = current.replace(/^v/, "");
|
||||||
|
const cleanLatest = latest.replace(/^v/, "");
|
||||||
|
|
||||||
|
const currentParts = cleanCurrent.split(".").map(Number);
|
||||||
|
const latestParts = cleanLatest.split(".").map(Number);
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
|
||||||
|
const curr = currentParts[i] || 0;
|
||||||
|
const lat = latestParts[i] || 0;
|
||||||
|
|
||||||
|
if (lat > curr) return true;
|
||||||
|
if (lat < curr) return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default internalRemoteVersion;
|
||||||
@@ -15,10 +15,10 @@ const internalReport = {
|
|||||||
const userId = access.token.getUserId(1);
|
const userId = access.token.getUserId(1);
|
||||||
|
|
||||||
const promises = [
|
const promises = [
|
||||||
internalProxyHost.getCount(userId, access_data.visibility),
|
internalProxyHost.getCount(userId, access_data.permission_visibility),
|
||||||
internalRedirectionHost.getCount(userId, access_data.visibility),
|
internalRedirectionHost.getCount(userId, access_data.permission_visibility),
|
||||||
internalStream.getCount(userId, access_data.visibility),
|
internalStream.getCount(userId, access_data.permission_visibility),
|
||||||
internalDeadHost.getCount(userId, access_data.visibility),
|
internalDeadHost.getCount(userId, access_data.permission_visibility),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import { parseDatePeriod } from "../lib/helpers.js";
|
|||||||
import authModel from "../models/auth.js";
|
import authModel from "../models/auth.js";
|
||||||
import TokenModel from "../models/token.js";
|
import TokenModel from "../models/token.js";
|
||||||
import userModel from "../models/user.js";
|
import userModel from "../models/user.js";
|
||||||
|
import twoFactor from "./2fa.js";
|
||||||
|
|
||||||
const ERROR_MESSAGE_INVALID_AUTH = "Invalid email or password";
|
const ERROR_MESSAGE_INVALID_AUTH = "Invalid email or password";
|
||||||
const ERROR_MESSAGE_INVALID_AUTH_I18N = "error.invalid-auth";
|
const ERROR_MESSAGE_INVALID_AUTH_I18N = "error.invalid-auth";
|
||||||
|
const ERROR_MESSAGE_INVALID_2FA = "Invalid verification code";
|
||||||
|
const ERROR_MESSAGE_INVALID_2FA_I18N = "error.invalid-2fa";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
@@ -59,6 +62,25 @@ export default {
|
|||||||
throw new errs.AuthError(`Invalid scope: ${data.scope}`);
|
throw new errs.AuthError(`Invalid scope: ${data.scope}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if 2FA is enabled
|
||||||
|
const has2FA = await twoFactor.isEnabled(user.id);
|
||||||
|
if (has2FA) {
|
||||||
|
// Return challenge token instead of full token
|
||||||
|
const challengeToken = await Token.create({
|
||||||
|
iss: issuer || "api",
|
||||||
|
attrs: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
scope: ["2fa-challenge"],
|
||||||
|
expiresIn: "5m",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
requires_2fa: true,
|
||||||
|
challenge_token: challengeToken.token,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Create a moment of the expiry expression
|
// Create a moment of the expiry expression
|
||||||
const expiry = parseDatePeriod(data.expiry);
|
const expiry = parseDatePeriod(data.expiry);
|
||||||
if (expiry === null) {
|
if (expiry === null) {
|
||||||
@@ -129,6 +151,65 @@ export default {
|
|||||||
throw new error.AssertionFailedError("Existing token contained invalid user data");
|
throw new error.AssertionFailedError("Existing token contained invalid user data");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify 2FA code and return full token
|
||||||
|
* @param {string} challengeToken
|
||||||
|
* @param {string} code
|
||||||
|
* @param {string} [expiry]
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
verify2FA: async (challengeToken, code, expiry) => {
|
||||||
|
const Token = TokenModel();
|
||||||
|
const tokenExpiry = expiry || "1d";
|
||||||
|
|
||||||
|
// Verify challenge token
|
||||||
|
let tokenData;
|
||||||
|
try {
|
||||||
|
tokenData = await Token.load(challengeToken);
|
||||||
|
} catch {
|
||||||
|
throw new errs.AuthError("Invalid or expired challenge token");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check scope
|
||||||
|
if (!tokenData.scope || tokenData.scope[0] !== "2fa-challenge") {
|
||||||
|
throw new errs.AuthError("Invalid challenge token");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = tokenData.attrs?.id;
|
||||||
|
if (!userId) {
|
||||||
|
throw new errs.AuthError("Invalid challenge token");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify 2FA code
|
||||||
|
const valid = await twoFactor.verifyForLogin(userId, code);
|
||||||
|
if (!valid) {
|
||||||
|
throw new errs.AuthError(
|
||||||
|
ERROR_MESSAGE_INVALID_2FA,
|
||||||
|
ERROR_MESSAGE_INVALID_2FA_I18N,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create full token
|
||||||
|
const expiryDate = parseDatePeriod(tokenExpiry);
|
||||||
|
if (expiryDate === null) {
|
||||||
|
throw new errs.AuthError(`Invalid expiry time: ${tokenExpiry}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const signed = await Token.create({
|
||||||
|
iss: "api",
|
||||||
|
attrs: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
scope: ["user"],
|
||||||
|
expiresIn: tokenExpiry,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: signed.token,
|
||||||
|
expires: expiryDate.toISOString(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} user
|
* @param {Object} user
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const certbot = new signale.Signale({ scope: "Certbot ", ...opts });
|
|||||||
const importer = new signale.Signale({ scope: "Importer ", ...opts });
|
const importer = new signale.Signale({ scope: "Importer ", ...opts });
|
||||||
const setup = new signale.Signale({ scope: "Setup ", ...opts });
|
const setup = new signale.Signale({ scope: "Setup ", ...opts });
|
||||||
const ipRanges = new signale.Signale({ scope: "IP Ranges", ...opts });
|
const ipRanges = new signale.Signale({ scope: "IP Ranges", ...opts });
|
||||||
|
const remoteVersion = new signale.Signale({ scope: "Remote Version", ...opts });
|
||||||
|
|
||||||
const debug = (logger, ...args) => {
|
const debug = (logger, ...args) => {
|
||||||
if (isDebugMode()) {
|
if (isDebugMode()) {
|
||||||
@@ -22,4 +23,4 @@ const debug = (logger, ...args) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { debug, global, migrate, express, access, nginx, ssl, certbot, importer, setup, ipRanges };
|
export { debug, global, migrate, express, access, nginx, ssl, certbot, importer, setup, ipRanges, remoteVersion };
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import db from "./db.js";
|
|||||||
import { migrate as logger } from "./logger.js";
|
import { migrate as logger } from "./logger.js";
|
||||||
|
|
||||||
const migrateUp = async () => {
|
const migrateUp = async () => {
|
||||||
const version = await db.migrate.currentVersion();
|
const version = await db().migrate.currentVersion();
|
||||||
logger.info("Current database version:", version);
|
logger.info("Current database version:", version);
|
||||||
return await db.migrate.latest({
|
return await db().migrate.latest({
|
||||||
tableName: "migrations",
|
tableName: "migrations",
|
||||||
directory: "migrations",
|
directory: "migrations",
|
||||||
});
|
});
|
||||||
|
|||||||
50
backend/migrations/20251111090000_redirect_auto_scheme.js
Normal file
50
backend/migrations/20251111090000_redirect_auto_scheme.js
Normal 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 };
|
||||||
@@ -10,7 +10,7 @@ import now from "./now_helper.js";
|
|||||||
import ProxyHostModel from "./proxy_host.js";
|
import ProxyHostModel from "./proxy_host.js";
|
||||||
import User from "./user.js";
|
import User from "./user.js";
|
||||||
|
|
||||||
Model.knex(db);
|
Model.knex(db());
|
||||||
|
|
||||||
const boolFields = ["is_deleted", "satisfy_any", "pass_auth"];
|
const boolFields = ["is_deleted", "satisfy_any", "pass_auth"];
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import db from "../db.js";
|
|||||||
import accessListModel from "./access_list.js";
|
import accessListModel from "./access_list.js";
|
||||||
import now from "./now_helper.js";
|
import now from "./now_helper.js";
|
||||||
|
|
||||||
Model.knex(db);
|
Model.knex(db());
|
||||||
|
|
||||||
class AccessListAuth extends Model {
|
class AccessListAuth extends Model {
|
||||||
$beforeInsert() {
|
$beforeInsert() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import db from "../db.js";
|
|||||||
import accessListModel from "./access_list.js";
|
import accessListModel from "./access_list.js";
|
||||||
import now from "./now_helper.js";
|
import now from "./now_helper.js";
|
||||||
|
|
||||||
Model.knex(db);
|
Model.knex(db());
|
||||||
|
|
||||||
class AccessListClient extends Model {
|
class AccessListClient extends Model {
|
||||||
$beforeInsert() {
|
$beforeInsert() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import db from "../db.js";
|
|||||||
import now from "./now_helper.js";
|
import now from "./now_helper.js";
|
||||||
import User from "./user.js";
|
import User from "./user.js";
|
||||||
|
|
||||||
Model.knex(db);
|
Model.knex(db());
|
||||||
|
|
||||||
class AuditLog extends Model {
|
class AuditLog extends Model {
|
||||||
$beforeInsert() {
|
$beforeInsert() {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.j
|
|||||||
import now from "./now_helper.js";
|
import now from "./now_helper.js";
|
||||||
import User from "./user.js";
|
import User from "./user.js";
|
||||||
|
|
||||||
Model.knex(db);
|
Model.knex(db());
|
||||||
|
|
||||||
const boolFields = ["is_deleted"];
|
const boolFields = ["is_deleted"];
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import redirectionHostModel from "./redirection_host.js";
|
|||||||
import streamModel from "./stream.js";
|
import streamModel from "./stream.js";
|
||||||
import userModel from "./user.js";
|
import userModel from "./user.js";
|
||||||
|
|
||||||
Model.knex(db);
|
Model.knex(db());
|
||||||
|
|
||||||
const boolFields = ["is_deleted"];
|
const boolFields = ["is_deleted"];
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Certificate from "./certificate.js";
|
|||||||
import now from "./now_helper.js";
|
import now from "./now_helper.js";
|
||||||
import User from "./user.js";
|
import User from "./user.js";
|
||||||
|
|
||||||
Model.knex(db);
|
Model.knex(db());
|
||||||
|
|
||||||
const boolFields = ["is_deleted", "ssl_forced", "http2_support", "enabled", "hsts_enabled", "hsts_subdomains"];
|
const boolFields = ["is_deleted", "ssl_forced", "http2_support", "enabled", "hsts_enabled", "hsts_subdomains"];
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Model } from "objection";
|
|||||||
import db from "../db.js";
|
import db from "../db.js";
|
||||||
import { isSqlite } from "../lib/config.js";
|
import { isSqlite } from "../lib/config.js";
|
||||||
|
|
||||||
Model.knex(db);
|
Model.knex(db());
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
if (isSqlite()) {
|
if (isSqlite()) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Certificate from "./certificate.js";
|
|||||||
import now from "./now_helper.js";
|
import now from "./now_helper.js";
|
||||||
import User from "./user.js";
|
import User from "./user.js";
|
||||||
|
|
||||||
Model.knex(db);
|
Model.knex(db());
|
||||||
|
|
||||||
const boolFields = [
|
const boolFields = [
|
||||||
"is_deleted",
|
"is_deleted",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Certificate from "./certificate.js";
|
|||||||
import now from "./now_helper.js";
|
import now from "./now_helper.js";
|
||||||
import User from "./user.js";
|
import User from "./user.js";
|
||||||
|
|
||||||
Model.knex(db);
|
Model.knex(db());
|
||||||
|
|
||||||
const boolFields = [
|
const boolFields = [
|
||||||
"is_deleted",
|
"is_deleted",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { Model } from "objection";
|
import { Model } from "objection";
|
||||||
import db from "../db.js";
|
import db from "../db.js";
|
||||||
|
|
||||||
Model.knex(db);
|
Model.knex(db());
|
||||||
|
|
||||||
class Setting extends Model {
|
class Setting extends Model {
|
||||||
$beforeInsert () {
|
$beforeInsert () {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Certificate from "./certificate.js";
|
|||||||
import now from "./now_helper.js";
|
import now from "./now_helper.js";
|
||||||
import User from "./user.js";
|
import User from "./user.js";
|
||||||
|
|
||||||
Model.knex(db);
|
Model.knex(db());
|
||||||
|
|
||||||
const boolFields = ["is_deleted", "enabled", "tcp_forwarding", "udp_forwarding"];
|
const boolFields = ["is_deleted", "enabled", "tcp_forwarding", "udp_forwarding"];
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.j
|
|||||||
import now from "./now_helper.js";
|
import now from "./now_helper.js";
|
||||||
import UserPermission from "./user_permission.js";
|
import UserPermission from "./user_permission.js";
|
||||||
|
|
||||||
Model.knex(db);
|
Model.knex(db());
|
||||||
|
|
||||||
const boolFields = ["is_deleted", "is_disabled"];
|
const boolFields = ["is_deleted", "is_disabled"];
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Model } from "objection";
|
|||||||
import db from "../db.js";
|
import db from "../db.js";
|
||||||
import now from "./now_helper.js";
|
import now from "./now_helper.js";
|
||||||
|
|
||||||
Model.knex(db);
|
Model.knex(db());
|
||||||
|
|
||||||
class UserPermission extends Model {
|
class UserPermission extends Model {
|
||||||
$beforeInsert () {
|
$beforeInsert () {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"bcrypt": "^5.0.0",
|
"bcrypt": "^5.0.0",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.3",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"express": "^4.20.0",
|
"express": "^4.22.0",
|
||||||
"express-fileupload": "^1.5.2",
|
"express-fileupload": "^1.5.2",
|
||||||
"gravatar": "^1.8.2",
|
"gravatar": "^1.8.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
"mysql2": "^3.15.3",
|
"mysql2": "^3.15.3",
|
||||||
"node-rsa": "^1.1.1",
|
"node-rsa": "^1.1.1",
|
||||||
"objection": "3.0.1",
|
"objection": "3.0.1",
|
||||||
|
"otplib": "^12.0.1",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"proxy-agent": "^6.5.0",
|
"proxy-agent": "^6.5.0",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import schemaRoutes from "./schema.js";
|
|||||||
import settingsRoutes from "./settings.js";
|
import settingsRoutes from "./settings.js";
|
||||||
import tokensRoutes from "./tokens.js";
|
import tokensRoutes from "./tokens.js";
|
||||||
import usersRoutes from "./users.js";
|
import usersRoutes from "./users.js";
|
||||||
|
import versionRoutes from "./version.js";
|
||||||
|
|
||||||
const router = express.Router({
|
const router = express.Router({
|
||||||
caseSensitive: true,
|
caseSensitive: true,
|
||||||
@@ -46,6 +47,7 @@ router.use("/users", usersRoutes);
|
|||||||
router.use("/audit-log", auditLogRoutes);
|
router.use("/audit-log", auditLogRoutes);
|
||||||
router.use("/reports", reportsRoutes);
|
router.use("/reports", reportsRoutes);
|
||||||
router.use("/settings", settingsRoutes);
|
router.use("/settings", settingsRoutes);
|
||||||
|
router.use("/version", versionRoutes);
|
||||||
router.use("/nginx/proxy-hosts", proxyHostsRoutes);
|
router.use("/nginx/proxy-hosts", proxyHostsRoutes);
|
||||||
router.use("/nginx/redirection-hosts", redirectionHostsRoutes);
|
router.use("/nginx/redirection-hosts", redirectionHostsRoutes);
|
||||||
router.use("/nginx/dead-hosts", deadHostsRoutes);
|
router.use("/nginx/dead-hosts", deadHostsRoutes);
|
||||||
|
|||||||
@@ -53,4 +53,26 @@ router
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router
|
||||||
|
.route("/2fa")
|
||||||
|
.options((_, res) => {
|
||||||
|
res.sendStatus(204);
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /tokens/2fa
|
||||||
|
*
|
||||||
|
* Verify 2FA code and get full token
|
||||||
|
*/
|
||||||
|
.post(async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { challenge_token, code } = await apiValidator(getValidationSchema("/tokens/2fa", "post"), req.body);
|
||||||
|
const result = await internalToken.verify2FA(challenge_token, code);
|
||||||
|
res.status(200).send(result);
|
||||||
|
} catch (err) {
|
||||||
|
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
|
import internal2FA from "../internal/2fa.js";
|
||||||
import internalUser from "../internal/user.js";
|
import internalUser from "../internal/user.js";
|
||||||
import Access from "../lib/access.js";
|
import Access from "../lib/access.js";
|
||||||
import { isCI } from "../lib/config.js";
|
import { isCI } from "../lib/config.js";
|
||||||
@@ -325,4 +326,130 @@ router
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User 2FA status
|
||||||
|
*
|
||||||
|
* /api/users/123/2fa
|
||||||
|
*/
|
||||||
|
router
|
||||||
|
.route("/:user_id/2fa")
|
||||||
|
.options((_, res) => {
|
||||||
|
res.sendStatus(204);
|
||||||
|
})
|
||||||
|
.all(jwtdecode())
|
||||||
|
.all(userIdFromMe)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/users/123/2fa
|
||||||
|
*
|
||||||
|
* Start 2FA setup, returns QR code URL
|
||||||
|
*/
|
||||||
|
.post(async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const result = await internal2FA.startSetup(res.locals.access, req.params.user_id);
|
||||||
|
res.status(200).send(result);
|
||||||
|
} catch (err) {
|
||||||
|
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/users/123/2fa
|
||||||
|
*
|
||||||
|
* Get 2FA status for a user
|
||||||
|
*/
|
||||||
|
.get(async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const status = await internal2FA.getStatus(res.locals.access, req.params.user_id);
|
||||||
|
res.status(200).send(status);
|
||||||
|
} catch (err) {
|
||||||
|
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/users/123/2fa?code=XXXXXX
|
||||||
|
*
|
||||||
|
* Disable 2FA for a user
|
||||||
|
*/
|
||||||
|
.delete(async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const code = typeof req.query.code === "string" ? req.query.code : null;
|
||||||
|
if (!code) {
|
||||||
|
throw new errs.ValidationError("Missing required parameter: code");
|
||||||
|
}
|
||||||
|
await internal2FA.disable(res.locals.access, req.params.user_id, code);
|
||||||
|
res.status(200).send(true);
|
||||||
|
} catch (err) {
|
||||||
|
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User 2FA enable
|
||||||
|
*
|
||||||
|
* /api/users/123/2fa/enable
|
||||||
|
*/
|
||||||
|
router
|
||||||
|
.route("/:user_id/2fa/enable")
|
||||||
|
.options((_, res) => {
|
||||||
|
res.sendStatus(204);
|
||||||
|
})
|
||||||
|
.all(jwtdecode())
|
||||||
|
.all(userIdFromMe)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/users/123/2fa/enable
|
||||||
|
*
|
||||||
|
* Verify code and enable 2FA
|
||||||
|
*/
|
||||||
|
.post(async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { code } = await apiValidator(
|
||||||
|
getValidationSchema("/users/{userID}/2fa/enable", "post"),
|
||||||
|
req.body,
|
||||||
|
);
|
||||||
|
const result = await internal2FA.enable(res.locals.access, req.params.user_id, code);
|
||||||
|
res.status(200).send(result);
|
||||||
|
} catch (err) {
|
||||||
|
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User 2FA backup codes
|
||||||
|
*
|
||||||
|
* /api/users/123/2fa/backup-codes
|
||||||
|
*/
|
||||||
|
router
|
||||||
|
.route("/:user_id/2fa/backup-codes")
|
||||||
|
.options((_, res) => {
|
||||||
|
res.sendStatus(204);
|
||||||
|
})
|
||||||
|
.all(jwtdecode())
|
||||||
|
.all(userIdFromMe)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/users/123/2fa/backup-codes
|
||||||
|
*
|
||||||
|
* Regenerate backup codes
|
||||||
|
*/
|
||||||
|
.post(async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { code } = await apiValidator(
|
||||||
|
getValidationSchema("/users/{userID}/2fa/backup-codes", "post"),
|
||||||
|
req.body,
|
||||||
|
);
|
||||||
|
const result = await internal2FA.regenerateBackupCodes(res.locals.access, req.params.user_id, code);
|
||||||
|
res.status(200).send(result);
|
||||||
|
} catch (err) {
|
||||||
|
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
40
backend/routes/version.js
Normal file
40
backend/routes/version.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import express from "express";
|
||||||
|
import internalRemoteVersion from "../internal/remote-version.js";
|
||||||
|
import { debug, express as logger } from "../logger.js";
|
||||||
|
|
||||||
|
const router = express.Router({
|
||||||
|
caseSensitive: true,
|
||||||
|
strict: true,
|
||||||
|
mergeParams: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /api/version/check
|
||||||
|
*/
|
||||||
|
router
|
||||||
|
.route("/check")
|
||||||
|
.options((_, res) => {
|
||||||
|
res.sendStatus(204);
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/version/check
|
||||||
|
*
|
||||||
|
* Check for available updates
|
||||||
|
*/
|
||||||
|
.get(async (req, res, _next) => {
|
||||||
|
try {
|
||||||
|
const data = await internalRemoteVersion.get();
|
||||||
|
res.status(200).send(data);
|
||||||
|
} catch (error) {
|
||||||
|
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${error}`);
|
||||||
|
// Send 200 even though there's an error to avoid triggering update checks repeatedly
|
||||||
|
res.status(200).send({
|
||||||
|
current: null,
|
||||||
|
latest: null,
|
||||||
|
update_available: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -71,6 +71,11 @@
|
|||||||
"propagation_seconds": {
|
"propagation_seconds": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 0
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"key_type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["rsa", "ecdsa"],
|
||||||
|
"default": "rsa"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"example": {
|
"example": {
|
||||||
|
|||||||
23
backend/schema/components/check-version-object.json
Normal file
23
backend/schema/components/check-version-object.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"description": "Check Version object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["current", "latest", "update_available"],
|
||||||
|
"properties": {
|
||||||
|
"current": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"description": "Current version string",
|
||||||
|
"example": "v2.10.1"
|
||||||
|
},
|
||||||
|
"latest": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"description": "Latest version string",
|
||||||
|
"example": "v2.13.4"
|
||||||
|
},
|
||||||
|
"update_available": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether there's an update available",
|
||||||
|
"example": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
backend/schema/components/token-challenge.json
Normal file
18
backend/schema/components/token-challenge.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"description": "Token object",
|
||||||
|
"required": ["requires_2fa", "challenge_token"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"requires_2fa": {
|
||||||
|
"description": "Whether this token request requires two-factor authentication",
|
||||||
|
"example": true,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"challenge_token": {
|
||||||
|
"description": "Challenge Token used in subsequent 2FA verification",
|
||||||
|
"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
backend/schema/paths/tokens/2fa/post.json
Normal file
55
backend/schema/paths/tokens/2fa/post.json
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"operationId": "loginWith2FA",
|
||||||
|
"summary": "Verify 2FA code and get full token",
|
||||||
|
"tags": ["tokens"],
|
||||||
|
"requestBody": {
|
||||||
|
"description": "2fa Challenge Payload",
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"challenge_token": {
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string",
|
||||||
|
"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
|
||||||
|
},
|
||||||
|
"code": {
|
||||||
|
"minLength": 6,
|
||||||
|
"maxLength": 6,
|
||||||
|
"type": "string",
|
||||||
|
"example": "012345"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["challenge_token", "code"],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"challenge_token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
|
||||||
|
"code": "012345"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"examples": {
|
||||||
|
"default": {
|
||||||
|
"value": {
|
||||||
|
"expires": "2025-02-04T20:40:46.340Z",
|
||||||
|
"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"$ref": "../../../components/token-object.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "200 response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,7 +50,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "../../components/token-object.json"
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "../../components/token-object.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "../../components/token-challenge.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
92
backend/schema/paths/users/userID/2fa/backup-codes/post.json
Normal file
92
backend/schema/paths/users/userID/2fa/backup-codes/post.json
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"operationId": "regenUser2faCodes",
|
||||||
|
"summary": "Regenerate 2FA backup codes",
|
||||||
|
"tags": ["users"],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "userID",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"description": "User ID",
|
||||||
|
"example": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"description": "Verififcation Payload",
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"minLength": 6,
|
||||||
|
"maxLength": 6,
|
||||||
|
"type": "string",
|
||||||
|
"example": "123456"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["code"],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"code": "123456"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"examples": {
|
||||||
|
"default": {
|
||||||
|
"value": {
|
||||||
|
"backup_codes": [
|
||||||
|
"6CD7CB06",
|
||||||
|
"495302F3",
|
||||||
|
"D8037852",
|
||||||
|
"A6FFC956",
|
||||||
|
"BC1A1851",
|
||||||
|
"A05E644F",
|
||||||
|
"A406D2E8",
|
||||||
|
"0AE3C522"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["backup_codes"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"backup_codes": {
|
||||||
|
"description": "Backup codes",
|
||||||
|
"example": [
|
||||||
|
"6CD7CB06",
|
||||||
|
"495302F3",
|
||||||
|
"D8037852",
|
||||||
|
"A6FFC956",
|
||||||
|
"BC1A1851",
|
||||||
|
"A05E644F",
|
||||||
|
"A406D2E8",
|
||||||
|
"0AE3C522"
|
||||||
|
],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "6CD7CB06"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "200 response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
backend/schema/paths/users/userID/2fa/delete.json
Normal file
48
backend/schema/paths/users/userID/2fa/delete.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"operationId": "disableUser2fa",
|
||||||
|
"summary": "Disable 2fa for user",
|
||||||
|
"tags": ["users"],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "userID",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"description": "User ID",
|
||||||
|
"example": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "code",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 6,
|
||||||
|
"maxLength": 6,
|
||||||
|
"example": "012345"
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"description": "2fa Code",
|
||||||
|
"example": "012345"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"examples": {
|
||||||
|
"default": {
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "200 response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
92
backend/schema/paths/users/userID/2fa/enable/post.json
Normal file
92
backend/schema/paths/users/userID/2fa/enable/post.json
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"operationId": "enableUser2fa",
|
||||||
|
"summary": "Verify code and enable 2FA",
|
||||||
|
"tags": ["users"],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "userID",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"description": "User ID",
|
||||||
|
"example": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"description": "Verififcation Payload",
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"minLength": 6,
|
||||||
|
"maxLength": 6,
|
||||||
|
"type": "string",
|
||||||
|
"example": "123456"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["code"],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"code": "123456"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"examples": {
|
||||||
|
"default": {
|
||||||
|
"value": {
|
||||||
|
"backup_codes": [
|
||||||
|
"6CD7CB06",
|
||||||
|
"495302F3",
|
||||||
|
"D8037852",
|
||||||
|
"A6FFC956",
|
||||||
|
"BC1A1851",
|
||||||
|
"A05E644F",
|
||||||
|
"A406D2E8",
|
||||||
|
"0AE3C522"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["backup_codes"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"backup_codes": {
|
||||||
|
"description": "Backup codes",
|
||||||
|
"example": [
|
||||||
|
"6CD7CB06",
|
||||||
|
"495302F3",
|
||||||
|
"D8037852",
|
||||||
|
"A6FFC956",
|
||||||
|
"BC1A1851",
|
||||||
|
"A05E644F",
|
||||||
|
"A406D2E8",
|
||||||
|
"0AE3C522"
|
||||||
|
],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "6CD7CB06"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "200 response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
backend/schema/paths/users/userID/2fa/get.json
Normal file
57
backend/schema/paths/users/userID/2fa/get.json
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"operationId": "getUser2faStatus",
|
||||||
|
"summary": "Get user 2fa Status",
|
||||||
|
"tags": ["users"],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "userID",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"description": "User ID",
|
||||||
|
"example": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "200 response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"examples": {
|
||||||
|
"default": {
|
||||||
|
"value": {
|
||||||
|
"enabled": false,
|
||||||
|
"backup_codes_remaining": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["enabled", "backup_codes_remaining"],
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Is 2FA enabled for this user",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"backup_codes_remaining": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of remaining backup codes for this user",
|
||||||
|
"example": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
backend/schema/paths/users/userID/2fa/post.json
Normal file
52
backend/schema/paths/users/userID/2fa/post.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"operationId": "setupUser2fa",
|
||||||
|
"summary": "Start 2FA setup, returns QR code URL",
|
||||||
|
"tags": ["users"],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "userID",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"description": "User ID",
|
||||||
|
"example": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"examples": {
|
||||||
|
"default": {
|
||||||
|
"value": {
|
||||||
|
"secret": "JZYCEBIEEJYUGPQM",
|
||||||
|
"otpauth_url": "otpauth://totp/Nginx%20Proxy%20Manager:jc%40jc21.com?secret=JZYCEBIEEJYUGPQM&period=30&digits=6&algorithm=SHA1&issuer=Nginx%20Proxy%20Manager"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["secret", "otpauth_url"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"secret": {
|
||||||
|
"description": "TOTP Secret",
|
||||||
|
"example": "JZYCEBIEEJYUGPQM",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"otpauth_url": {
|
||||||
|
"description": "OTP Auth URL for QR Code generation",
|
||||||
|
"example": "otpauth://totp/Nginx%20Proxy%20Manager:jc%40jc21.com?secret=JZYCEBIEEJYUGPQM&period=30&digits=6&algorithm=SHA1&issuer=Nginx%20Proxy%20Manager",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "200 response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
backend/schema/paths/version/check/get.json
Normal file
26
backend/schema/paths/version/check/get.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"operationId": "checkVersion",
|
||||||
|
"summary": "Returns any new version data from github",
|
||||||
|
"tags": ["public"],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "200 response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"examples": {
|
||||||
|
"default": {
|
||||||
|
"value": {
|
||||||
|
"current": "v2.12.0",
|
||||||
|
"latest": "v2.13.4",
|
||||||
|
"update_available": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"$ref": "../../../components/check-version-object.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -293,6 +293,16 @@
|
|||||||
"$ref": "./paths/tokens/post.json"
|
"$ref": "./paths/tokens/post.json"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/tokens/2fa": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/tokens/2fa/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"
|
||||||
@@ -312,6 +322,27 @@
|
|||||||
"$ref": "./paths/users/userID/delete.json"
|
"$ref": "./paths/users/userID/delete.json"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/users/{userID}/2fa": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/users/userID/2fa/post.json"
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"$ref": "./paths/users/userID/2fa/get.json"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"$ref": "./paths/users/userID/2fa/delete.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/users/{userID}/2fa/enable": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/users/userID/2fa/enable/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/users/{userID}/2fa/backup-codes": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "./paths/users/userID/2fa/backup-codes/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
"/users/{userID}/auth": {
|
"/users/{userID}/auth": {
|
||||||
"put": {
|
"put": {
|
||||||
"$ref": "./paths/users/userID/auth/put.json"
|
"$ref": "./paths/users/userID/auth/put.json"
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const setupDefaultUser = async () => {
|
|||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
is_deleted: 0,
|
is_deleted: 0,
|
||||||
email: email,
|
email: initialAdminEmail,
|
||||||
name: "Administrator",
|
name: "Administrator",
|
||||||
nickname: "Admin",
|
nickname: "Admin",
|
||||||
avatar: "",
|
avatar: "",
|
||||||
@@ -53,7 +53,7 @@ const setupDefaultUser = async () => {
|
|||||||
.insert({
|
.insert({
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
type: "password",
|
type: "password",
|
||||||
secret: password,
|
secret: initialAdminPassword,
|
||||||
meta: {},
|
meta: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
auth_basic "Authorization required";
|
auth_basic "Authorization required";
|
||||||
auth_basic_user_file /data/access/{{ access_list_id }};
|
auth_basic_user_file /data/access/{{ access_list_id }};
|
||||||
|
|
||||||
{% if access_list.pass_auth == 0 or access_list.pass_auth == true %}
|
{% if access_list.pass_auth == 0 or access_list.pass_auth == false %}
|
||||||
proxy_set_header Authorization "";
|
proxy_set_header Authorization "";
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ server {
|
|||||||
|
|
||||||
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
|
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
|
||||||
|
|
||||||
|
access_log /data/logs/stream-{{ id }}_access.log stream;
|
||||||
|
error_log /data/logs/stream-{{ id }}_error.log warn;
|
||||||
|
|
||||||
# Custom
|
# Custom
|
||||||
include /data/nginx/custom/server_stream[.]conf;
|
include /data/nginx/custom/server_stream[.]conf;
|
||||||
include /data/nginx/custom/server_stream_tcp[.]conf;
|
include /data/nginx/custom/server_stream_tcp[.]conf;
|
||||||
@@ -25,9 +28,12 @@ server {
|
|||||||
|
|
||||||
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
|
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
|
||||||
|
|
||||||
|
access_log /data/logs/stream-{{ id }}_access.log stream;
|
||||||
|
error_log /data/logs/stream-{{ id }}_error.log warn;
|
||||||
|
|
||||||
# Custom
|
# Custom
|
||||||
include /data/nginx/custom/server_stream[.]conf;
|
include /data/nginx/custom/server_stream[.]conf;
|
||||||
include /data/nginx/custom/server_stream_udp[.]conf;
|
include /data/nginx/custom/server_stream_udp[.]conf;
|
||||||
}
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -138,6 +138,44 @@
|
|||||||
mkdirp "^1.0.4"
|
mkdirp "^1.0.4"
|
||||||
rimraf "^3.0.2"
|
rimraf "^3.0.2"
|
||||||
|
|
||||||
|
"@otplib/core@^12.0.1":
|
||||||
|
version "12.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@otplib/core/-/core-12.0.1.tgz#73720a8cedce211fe5b3f683cd5a9c098eaf0f8d"
|
||||||
|
integrity sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==
|
||||||
|
|
||||||
|
"@otplib/plugin-crypto@^12.0.1":
|
||||||
|
version "12.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz#2b42c624227f4f9303c1c041fca399eddcbae25e"
|
||||||
|
integrity sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==
|
||||||
|
dependencies:
|
||||||
|
"@otplib/core" "^12.0.1"
|
||||||
|
|
||||||
|
"@otplib/plugin-thirty-two@^12.0.1":
|
||||||
|
version "12.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz#5cc9b56e6e89f2a1fe4a2b38900ca4e11c87aa9e"
|
||||||
|
integrity sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==
|
||||||
|
dependencies:
|
||||||
|
"@otplib/core" "^12.0.1"
|
||||||
|
thirty-two "^1.0.2"
|
||||||
|
|
||||||
|
"@otplib/preset-default@^12.0.1":
|
||||||
|
version "12.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@otplib/preset-default/-/preset-default-12.0.1.tgz#cb596553c08251e71b187ada4a2246ad2a3165ba"
|
||||||
|
integrity sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==
|
||||||
|
dependencies:
|
||||||
|
"@otplib/core" "^12.0.1"
|
||||||
|
"@otplib/plugin-crypto" "^12.0.1"
|
||||||
|
"@otplib/plugin-thirty-two" "^12.0.1"
|
||||||
|
|
||||||
|
"@otplib/preset-v11@^12.0.1":
|
||||||
|
version "12.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@otplib/preset-v11/-/preset-v11-12.0.1.tgz#4c7266712e7230500b421ba89252963c838fc96d"
|
||||||
|
integrity sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==
|
||||||
|
dependencies:
|
||||||
|
"@otplib/core" "^12.0.1"
|
||||||
|
"@otplib/plugin-crypto" "^12.0.1"
|
||||||
|
"@otplib/plugin-thirty-two" "^12.0.1"
|
||||||
|
|
||||||
"@tootallnate/once@1":
|
"@tootallnate/once@1":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
||||||
@@ -389,23 +427,23 @@ blueimp-md5@^2.16.0:
|
|||||||
resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.19.0.tgz#b53feea5498dcb53dc6ec4b823adb84b729c4af0"
|
resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.19.0.tgz#b53feea5498dcb53dc6ec4b823adb84b729c4af0"
|
||||||
integrity sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==
|
integrity sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==
|
||||||
|
|
||||||
body-parser@1.20.3, body-parser@^1.20.3:
|
body-parser@^1.20.3, body-parser@~1.20.3:
|
||||||
version "1.20.3"
|
version "1.20.4"
|
||||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6"
|
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.4.tgz#f8e20f4d06ca8a50a71ed329c15dccad1cdc547f"
|
||||||
integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==
|
integrity sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes "3.1.2"
|
bytes "~3.1.2"
|
||||||
content-type "~1.0.5"
|
content-type "~1.0.5"
|
||||||
debug "2.6.9"
|
debug "2.6.9"
|
||||||
depd "2.0.0"
|
depd "2.0.0"
|
||||||
destroy "1.2.0"
|
destroy "~1.2.0"
|
||||||
http-errors "2.0.0"
|
http-errors "~2.0.1"
|
||||||
iconv-lite "0.4.24"
|
iconv-lite "~0.4.24"
|
||||||
on-finished "2.4.1"
|
on-finished "~2.4.1"
|
||||||
qs "6.13.0"
|
qs "~6.14.0"
|
||||||
raw-body "2.5.2"
|
raw-body "~2.5.3"
|
||||||
type-is "~1.6.18"
|
type-is "~1.6.18"
|
||||||
unpipe "1.0.0"
|
unpipe "~1.0.0"
|
||||||
|
|
||||||
brace-expansion@^1.1.7:
|
brace-expansion@^1.1.7:
|
||||||
version "1.1.12"
|
version "1.1.12"
|
||||||
@@ -454,7 +492,7 @@ busboy@^1.6.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
streamsearch "^1.1.0"
|
streamsearch "^1.1.0"
|
||||||
|
|
||||||
bytes@3.1.2:
|
bytes@3.1.2, bytes@~3.1.2:
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
||||||
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
|
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
|
||||||
@@ -649,7 +687,7 @@ console-control-strings@^1.0.0, console-control-strings@^1.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
|
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
|
||||||
integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
|
integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
|
||||||
|
|
||||||
content-disposition@0.5.4:
|
content-disposition@~0.5.4:
|
||||||
version "0.5.4"
|
version "0.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
|
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
|
||||||
integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
|
integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
|
||||||
@@ -661,15 +699,15 @@ content-type@~1.0.4, content-type@~1.0.5:
|
|||||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
|
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
|
||||||
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
|
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
|
||||||
|
|
||||||
cookie-signature@1.0.6:
|
cookie-signature@~1.0.6:
|
||||||
version "1.0.6"
|
version "1.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.7.tgz#ab5dd7ab757c54e60f37ef6550f481c426d10454"
|
||||||
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
|
integrity sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==
|
||||||
|
|
||||||
cookie@0.7.1:
|
cookie@~0.7.1:
|
||||||
version "0.7.1"
|
version "0.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9"
|
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
|
||||||
integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==
|
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
|
||||||
|
|
||||||
core-util-is@~1.0.0:
|
core-util-is@~1.0.0:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
@@ -706,10 +744,10 @@ debug@2.6.9:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "2.0.0"
|
ms "2.0.0"
|
||||||
|
|
||||||
debug@4, debug@^4.3.3:
|
debug@4, debug@^4.3.3, debug@^4.3.4:
|
||||||
version "4.4.1"
|
version "4.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
|
||||||
integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
|
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
|
||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.1.3"
|
ms "^2.1.3"
|
||||||
|
|
||||||
@@ -727,13 +765,6 @@ debug@^3.2.7:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.1.1"
|
ms "^2.1.1"
|
||||||
|
|
||||||
debug@^4.3.4:
|
|
||||||
version "4.4.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
|
|
||||||
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
|
|
||||||
dependencies:
|
|
||||||
ms "^2.1.3"
|
|
||||||
|
|
||||||
decamelize@^1.2.0:
|
decamelize@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||||
@@ -770,12 +801,12 @@ denque@^2.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
|
resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
|
||||||
integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
|
integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
|
||||||
|
|
||||||
depd@2.0.0:
|
depd@2.0.0, depd@~2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
|
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
|
||||||
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
|
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
|
||||||
|
|
||||||
destroy@1.2.0:
|
destroy@1.2.0, destroy@~1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
|
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
|
||||||
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
|
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
|
||||||
@@ -816,11 +847,6 @@ emoji-regex@^8.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||||
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
||||||
|
|
||||||
encodeurl@~1.0.2:
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
|
||||||
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
|
|
||||||
|
|
||||||
encodeurl@~2.0.0:
|
encodeurl@~2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
|
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
|
||||||
@@ -937,39 +963,39 @@ express-fileupload@^1.5.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
busboy "^1.6.0"
|
busboy "^1.6.0"
|
||||||
|
|
||||||
express@^4.20.0:
|
express@^4.22.0:
|
||||||
version "4.21.2"
|
version "4.22.0"
|
||||||
resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32"
|
resolved "https://registry.yarnpkg.com/express/-/express-4.22.0.tgz#a9d7abdce6d774ed1b4479019387763d1798bd03"
|
||||||
integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==
|
integrity sha512-c2iPh3xp5vvCLgaHK03+mWLFPhox7j1LwyxcZwFVApEv5i0X+IjPpbT50SJJwwLpdBVfp45AkK/v+AFgv/XlfQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
accepts "~1.3.8"
|
accepts "~1.3.8"
|
||||||
array-flatten "1.1.1"
|
array-flatten "1.1.1"
|
||||||
body-parser "1.20.3"
|
body-parser "~1.20.3"
|
||||||
content-disposition "0.5.4"
|
content-disposition "~0.5.4"
|
||||||
content-type "~1.0.4"
|
content-type "~1.0.4"
|
||||||
cookie "0.7.1"
|
cookie "~0.7.1"
|
||||||
cookie-signature "1.0.6"
|
cookie-signature "~1.0.6"
|
||||||
debug "2.6.9"
|
debug "2.6.9"
|
||||||
depd "2.0.0"
|
depd "2.0.0"
|
||||||
encodeurl "~2.0.0"
|
encodeurl "~2.0.0"
|
||||||
escape-html "~1.0.3"
|
escape-html "~1.0.3"
|
||||||
etag "~1.8.1"
|
etag "~1.8.1"
|
||||||
finalhandler "1.3.1"
|
finalhandler "~1.3.1"
|
||||||
fresh "0.5.2"
|
fresh "~0.5.2"
|
||||||
http-errors "2.0.0"
|
http-errors "~2.0.0"
|
||||||
merge-descriptors "1.0.3"
|
merge-descriptors "1.0.3"
|
||||||
methods "~1.1.2"
|
methods "~1.1.2"
|
||||||
on-finished "2.4.1"
|
on-finished "~2.4.1"
|
||||||
parseurl "~1.3.3"
|
parseurl "~1.3.3"
|
||||||
path-to-regexp "0.1.12"
|
path-to-regexp "~0.1.12"
|
||||||
proxy-addr "~2.0.7"
|
proxy-addr "~2.0.7"
|
||||||
qs "6.13.0"
|
qs "~6.14.0"
|
||||||
range-parser "~1.2.1"
|
range-parser "~1.2.1"
|
||||||
safe-buffer "5.2.1"
|
safe-buffer "5.2.1"
|
||||||
send "0.19.0"
|
send "~0.19.0"
|
||||||
serve-static "1.16.2"
|
serve-static "~1.16.2"
|
||||||
setprototypeof "1.2.0"
|
setprototypeof "1.2.0"
|
||||||
statuses "2.0.1"
|
statuses "~2.0.1"
|
||||||
type-is "~1.6.18"
|
type-is "~1.6.18"
|
||||||
utils-merge "1.0.1"
|
utils-merge "1.0.1"
|
||||||
vary "~1.1.2"
|
vary "~1.1.2"
|
||||||
@@ -1003,17 +1029,17 @@ fill-range@^7.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range "^5.0.1"
|
to-regex-range "^5.0.1"
|
||||||
|
|
||||||
finalhandler@1.3.1:
|
finalhandler@~1.3.1:
|
||||||
version "1.3.1"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019"
|
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.2.tgz#1ebc2228fc7673aac4a472c310cc05b77d852b88"
|
||||||
integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==
|
integrity sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==
|
||||||
dependencies:
|
dependencies:
|
||||||
debug "2.6.9"
|
debug "2.6.9"
|
||||||
encodeurl "~2.0.0"
|
encodeurl "~2.0.0"
|
||||||
escape-html "~1.0.3"
|
escape-html "~1.0.3"
|
||||||
on-finished "2.4.1"
|
on-finished "~2.4.1"
|
||||||
parseurl "~1.3.3"
|
parseurl "~1.3.3"
|
||||||
statuses "2.0.1"
|
statuses "~2.0.2"
|
||||||
unpipe "~1.0.0"
|
unpipe "~1.0.0"
|
||||||
|
|
||||||
find-up@^2.0.0:
|
find-up@^2.0.0:
|
||||||
@@ -1036,7 +1062,7 @@ forwarded@0.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
||||||
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
|
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
|
||||||
|
|
||||||
fresh@0.5.2:
|
fresh@~0.5.2:
|
||||||
version "0.5.2"
|
version "0.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
||||||
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
|
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
|
||||||
@@ -1228,16 +1254,16 @@ http-cache-semantics@^4.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz#205f4db64f8562b76a4ff9235aa5279839a09dd5"
|
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz#205f4db64f8562b76a4ff9235aa5279839a09dd5"
|
||||||
integrity sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==
|
integrity sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==
|
||||||
|
|
||||||
http-errors@2.0.0:
|
http-errors@~2.0.0, http-errors@~2.0.1:
|
||||||
version "2.0.0"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
|
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b"
|
||||||
integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
|
integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
depd "2.0.0"
|
depd "~2.0.0"
|
||||||
inherits "2.0.4"
|
inherits "~2.0.4"
|
||||||
setprototypeof "1.2.0"
|
setprototypeof "~1.2.0"
|
||||||
statuses "2.0.1"
|
statuses "~2.0.2"
|
||||||
toidentifier "1.0.1"
|
toidentifier "~1.0.1"
|
||||||
|
|
||||||
http-proxy-agent@^4.0.1:
|
http-proxy-agent@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
@@ -1279,13 +1305,6 @@ humanize-ms@^1.2.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.0.0"
|
ms "^2.0.0"
|
||||||
|
|
||||||
iconv-lite@0.4.24:
|
|
||||||
version "0.4.24"
|
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
|
||||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
|
||||||
dependencies:
|
|
||||||
safer-buffer ">= 2.1.2 < 3"
|
|
||||||
|
|
||||||
iconv-lite@^0.6.2:
|
iconv-lite@^0.6.2:
|
||||||
version "0.6.3"
|
version "0.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
|
||||||
@@ -1300,6 +1319,13 @@ iconv-lite@^0.7.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer ">= 2.1.2 < 3.0.0"
|
safer-buffer ">= 2.1.2 < 3.0.0"
|
||||||
|
|
||||||
|
iconv-lite@~0.4.24:
|
||||||
|
version "0.4.24"
|
||||||
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||||
|
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||||
|
dependencies:
|
||||||
|
safer-buffer ">= 2.1.2 < 3"
|
||||||
|
|
||||||
ieee754@^1.1.13:
|
ieee754@^1.1.13:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||||
@@ -1333,7 +1359,7 @@ inflight@^1.0.4:
|
|||||||
once "^1.3.0"
|
once "^1.3.0"
|
||||||
wrappy "1"
|
wrappy "1"
|
||||||
|
|
||||||
inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
|
inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4:
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||||
@@ -1430,9 +1456,9 @@ isexe@^2.0.0:
|
|||||||
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
||||||
|
|
||||||
js-yaml@^4.1.0:
|
js-yaml@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
|
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
|
||||||
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
|
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
|
||||||
dependencies:
|
dependencies:
|
||||||
argparse "^2.0.1"
|
argparse "^2.0.1"
|
||||||
|
|
||||||
@@ -1462,7 +1488,7 @@ jsonwebtoken@^9.0.2:
|
|||||||
ms "^2.1.1"
|
ms "^2.1.1"
|
||||||
semver "^7.5.4"
|
semver "^7.5.4"
|
||||||
|
|
||||||
jwa@^1.4.1:
|
jwa@^1.4.2:
|
||||||
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 +1498,11 @@ jwa@^1.4.1:
|
|||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
jws@^3.2.2:
|
jws@^3.2.2:
|
||||||
version "3.2.2"
|
version "3.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
|
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.3.tgz#5ac0690b460900a27265de24520526853c0b8ca1"
|
||||||
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
|
integrity sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==
|
||||||
dependencies:
|
dependencies:
|
||||||
jwa "^1.4.1"
|
jwa "^1.4.2"
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
knex@2.4.2:
|
knex@2.4.2:
|
||||||
@@ -1959,7 +1985,7 @@ objection@3.0.1:
|
|||||||
ajv "^8.6.2"
|
ajv "^8.6.2"
|
||||||
db-errors "^0.2.3"
|
db-errors "^0.2.3"
|
||||||
|
|
||||||
on-finished@2.4.1:
|
on-finished@~2.4.1:
|
||||||
version "2.4.1"
|
version "2.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
|
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
|
||||||
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
|
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
|
||||||
@@ -1978,6 +2004,15 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
wrappy "1"
|
wrappy "1"
|
||||||
|
|
||||||
|
otplib@^12.0.1:
|
||||||
|
version "12.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/otplib/-/otplib-12.0.1.tgz#c1d3060ab7aadf041ed2960302f27095777d1f73"
|
||||||
|
integrity sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==
|
||||||
|
dependencies:
|
||||||
|
"@otplib/core" "^12.0.1"
|
||||||
|
"@otplib/preset-default" "^12.0.1"
|
||||||
|
"@otplib/preset-v11" "^12.0.1"
|
||||||
|
|
||||||
p-limit@^1.1.0:
|
p-limit@^1.1.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
|
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
|
||||||
@@ -2078,7 +2113,7 @@ path-parse@^1.0.7:
|
|||||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
||||||
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
||||||
|
|
||||||
path-to-regexp@0.1.12:
|
path-to-regexp@~0.1.12:
|
||||||
version "0.1.12"
|
version "0.1.12"
|
||||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
|
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
|
||||||
integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==
|
integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==
|
||||||
@@ -2273,12 +2308,12 @@ pump@^3.0.0:
|
|||||||
end-of-stream "^1.1.0"
|
end-of-stream "^1.1.0"
|
||||||
once "^1.3.1"
|
once "^1.3.1"
|
||||||
|
|
||||||
qs@6.13.0:
|
qs@~6.14.0:
|
||||||
version "6.13.0"
|
version "6.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.1.tgz#a41d85b9d3902f31d27861790506294881871159"
|
||||||
integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
|
integrity sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
side-channel "^1.0.6"
|
side-channel "^1.1.0"
|
||||||
|
|
||||||
querystring@0.2.0:
|
querystring@0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
@@ -2290,15 +2325,15 @@ range-parser@~1.2.1:
|
|||||||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||||
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
||||||
|
|
||||||
raw-body@2.5.2:
|
raw-body@~2.5.3:
|
||||||
version "2.5.2"
|
version "2.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
|
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.3.tgz#11c6650ee770a7de1b494f197927de0c923822e2"
|
||||||
integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
|
integrity sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes "3.1.2"
|
bytes "~3.1.2"
|
||||||
http-errors "2.0.0"
|
http-errors "~2.0.1"
|
||||||
iconv-lite "0.4.24"
|
iconv-lite "~0.4.24"
|
||||||
unpipe "1.0.0"
|
unpipe "~1.0.0"
|
||||||
|
|
||||||
rc@^1.2.7:
|
rc@^1.2.7:
|
||||||
version "1.2.8"
|
version "1.2.8"
|
||||||
@@ -2429,46 +2464,46 @@ semver@~7.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
|
||||||
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
|
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
|
||||||
|
|
||||||
send@0.19.0:
|
send@~0.19.0, send@~0.19.1:
|
||||||
version "0.19.0"
|
version "0.19.2"
|
||||||
resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8"
|
resolved "https://registry.yarnpkg.com/send/-/send-0.19.2.tgz#59bc0da1b4ea7ad42736fd642b1c4294e114ff29"
|
||||||
integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==
|
integrity sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==
|
||||||
dependencies:
|
dependencies:
|
||||||
debug "2.6.9"
|
debug "2.6.9"
|
||||||
depd "2.0.0"
|
depd "2.0.0"
|
||||||
destroy "1.2.0"
|
destroy "1.2.0"
|
||||||
encodeurl "~1.0.2"
|
encodeurl "~2.0.0"
|
||||||
escape-html "~1.0.3"
|
escape-html "~1.0.3"
|
||||||
etag "~1.8.1"
|
etag "~1.8.1"
|
||||||
fresh "0.5.2"
|
fresh "~0.5.2"
|
||||||
http-errors "2.0.0"
|
http-errors "~2.0.1"
|
||||||
mime "1.6.0"
|
mime "1.6.0"
|
||||||
ms "2.1.3"
|
ms "2.1.3"
|
||||||
on-finished "2.4.1"
|
on-finished "~2.4.1"
|
||||||
range-parser "~1.2.1"
|
range-parser "~1.2.1"
|
||||||
statuses "2.0.1"
|
statuses "~2.0.2"
|
||||||
|
|
||||||
seq-queue@^0.0.5:
|
seq-queue@^0.0.5:
|
||||||
version "0.0.5"
|
version "0.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e"
|
resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e"
|
||||||
integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==
|
integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==
|
||||||
|
|
||||||
serve-static@1.16.2:
|
serve-static@~1.16.2:
|
||||||
version "1.16.2"
|
version "1.16.3"
|
||||||
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296"
|
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.3.tgz#a97b74d955778583f3862a4f0b841eb4d5d78cf9"
|
||||||
integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==
|
integrity sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==
|
||||||
dependencies:
|
dependencies:
|
||||||
encodeurl "~2.0.0"
|
encodeurl "~2.0.0"
|
||||||
escape-html "~1.0.3"
|
escape-html "~1.0.3"
|
||||||
parseurl "~1.3.3"
|
parseurl "~1.3.3"
|
||||||
send "0.19.0"
|
send "~0.19.1"
|
||||||
|
|
||||||
set-blocking@^2.0.0:
|
set-blocking@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||||
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
|
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
|
||||||
|
|
||||||
setprototypeof@1.2.0:
|
setprototypeof@1.2.0, setprototypeof@~1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
|
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
|
||||||
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
|
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
|
||||||
@@ -2502,7 +2537,7 @@ side-channel-weakmap@^1.0.2:
|
|||||||
object-inspect "^1.13.3"
|
object-inspect "^1.13.3"
|
||||||
side-channel-map "^1.0.1"
|
side-channel-map "^1.0.1"
|
||||||
|
|
||||||
side-channel@^1.0.6:
|
side-channel@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9"
|
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9"
|
||||||
integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==
|
integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==
|
||||||
@@ -2613,10 +2648,10 @@ ssri@^8.0.0, ssri@^8.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minipass "^3.1.1"
|
minipass "^3.1.1"
|
||||||
|
|
||||||
statuses@2.0.1:
|
statuses@~2.0.1, statuses@~2.0.2:
|
||||||
version "2.0.1"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
|
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382"
|
||||||
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
|
integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==
|
||||||
|
|
||||||
streamsearch@^1.1.0:
|
streamsearch@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
@@ -2736,6 +2771,11 @@ temp-write@^4.0.0:
|
|||||||
temp-dir "^1.0.0"
|
temp-dir "^1.0.0"
|
||||||
uuid "^3.3.2"
|
uuid "^3.3.2"
|
||||||
|
|
||||||
|
thirty-two@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a"
|
||||||
|
integrity sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==
|
||||||
|
|
||||||
tildify@2.0.0:
|
tildify@2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a"
|
resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a"
|
||||||
@@ -2748,7 +2788,7 @@ to-regex-range@^5.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-number "^7.0.0"
|
is-number "^7.0.0"
|
||||||
|
|
||||||
toidentifier@1.0.1:
|
toidentifier@~1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||||
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
||||||
@@ -2802,7 +2842,7 @@ unique-slug@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
imurmurhash "^0.1.4"
|
imurmurhash "^0.1.4"
|
||||||
|
|
||||||
unpipe@1.0.0, unpipe@~1.0.0:
|
unpipe@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||||
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
|
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
# This file assumes that the frontend has been built using ./scripts/frontend-build
|
# This file assumes that the frontend has been built using ./scripts/frontend-build
|
||||||
|
|
||||||
FROM nginxproxymanager/testca AS testca
|
FROM nginxproxymanager/testca AS testca
|
||||||
FROM letsencrypt/pebble AS pebbleca
|
|
||||||
FROM nginxproxymanager/nginx-full:certbot-node
|
FROM nginxproxymanager/nginx-full:certbot-node
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
@@ -46,7 +45,6 @@ RUN yarn install \
|
|||||||
|
|
||||||
# add late to limit cache-busting by modifications
|
# add late to limit cache-busting by modifications
|
||||||
COPY docker/rootfs /
|
COPY docker/rootfs /
|
||||||
COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem
|
|
||||||
COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
|
COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
|
||||||
|
|
||||||
# Remove frontend service not required for prod, dev nginx config as well
|
# Remove frontend service not required for prod, dev nginx config as well
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
AUTHENTIK_SECRET_KEY=gl8woZe8L6IIX8SC0c5Ocsj0xPkX5uJo5DVZCFl+L/QGbzuplfutYuua2ODNLEiDD3aFd9H2ylJmrke0
|
AUTHENTIK_SECRET_KEY=gl8woZe8L6IIX8SC0c5Ocsj0xPkX5uJo5DVZCFl+L/QGbzuplfutYuua2ODNLEiDD3aFd9H2ylJmrke0
|
||||||
AUTHENTIK_REDIS__HOST=authentik-redis
|
AUTHENTIK_REDIS__HOST=authentik-redis
|
||||||
AUTHENTIK_POSTGRESQL__HOST=db-postgres
|
AUTHENTIK_POSTGRESQL__HOST=pgdb.internal
|
||||||
AUTHENTIK_POSTGRESQL__USER=authentik
|
AUTHENTIK_POSTGRESQL__USER=authentik
|
||||||
AUTHENTIK_POSTGRESQL__NAME=authentik
|
AUTHENTIK_POSTGRESQL__NAME=authentik
|
||||||
AUTHENTIK_POSTGRESQL__PASSWORD=07EKS5NLI6Tpv68tbdvrxfvj
|
AUTHENTIK_POSTGRESQL__PASSWORD=07EKS5NLI6Tpv68tbdvrxfvj
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
FROM nginxproxymanager/testca AS testca
|
FROM nginxproxymanager/testca AS testca
|
||||||
FROM letsencrypt/pebble AS pebbleca
|
|
||||||
FROM nginxproxymanager/nginx-full:certbot-node
|
FROM nginxproxymanager/nginx-full:certbot-node
|
||||||
LABEL maintainer="Jamie Curnow <jc@jc21.com>"
|
LABEL maintainer="Jamie Curnow <jc@jc21.com>"
|
||||||
|
|
||||||
@@ -33,7 +32,6 @@ RUN rm -f /etc/nginx/conf.d/production.conf \
|
|||||||
&& chmod 644 -R /root/.cache
|
&& chmod 644 -R /root/.cache
|
||||||
|
|
||||||
# Certs for testing purposes
|
# Certs for testing purposes
|
||||||
COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem
|
|
||||||
COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
|
COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
|
||||||
|
|
||||||
EXPOSE 80 81 443
|
EXPOSE 80 81 443
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ services:
|
|||||||
|
|
||||||
fullstack:
|
fullstack:
|
||||||
environment:
|
environment:
|
||||||
DB_POSTGRES_HOST: "db-postgres"
|
DB_POSTGRES_HOST: "pgdb.internal"
|
||||||
DB_POSTGRES_PORT: "5432"
|
DB_POSTGRES_PORT: "5432"
|
||||||
DB_POSTGRES_USER: "npm"
|
DB_POSTGRES_USER: "npm"
|
||||||
DB_POSTGRES_PASSWORD: "npmpass"
|
DB_POSTGRES_PASSWORD: "npmpass"
|
||||||
@@ -27,7 +27,9 @@ services:
|
|||||||
- psql_vol:/var/lib/postgresql/data
|
- psql_vol:/var/lib/postgresql/data
|
||||||
- ./ci/postgres:/docker-entrypoint-initdb.d
|
- ./ci/postgres:/docker-entrypoint-initdb.d
|
||||||
networks:
|
networks:
|
||||||
- fulltest
|
fulltest:
|
||||||
|
aliases:
|
||||||
|
- pgdb.internal
|
||||||
|
|
||||||
authentik-redis:
|
authentik-redis:
|
||||||
image: "redis:alpine"
|
image: "redis:alpine"
|
||||||
@@ -41,6 +43,8 @@ services:
|
|||||||
timeout: 3s
|
timeout: 3s
|
||||||
volumes:
|
volumes:
|
||||||
- redis_vol:/data
|
- redis_vol:/data
|
||||||
|
networks:
|
||||||
|
- fulltest
|
||||||
|
|
||||||
authentik:
|
authentik:
|
||||||
image: ghcr.io/goauthentik/server:2024.10.1
|
image: ghcr.io/goauthentik/server:2024.10.1
|
||||||
@@ -51,6 +55,8 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- authentik-redis
|
- authentik-redis
|
||||||
- db-postgres
|
- db-postgres
|
||||||
|
networks:
|
||||||
|
- fulltest
|
||||||
|
|
||||||
authentik-worker:
|
authentik-worker:
|
||||||
image: ghcr.io/goauthentik/server:2024.10.1
|
image: ghcr.io/goauthentik/server:2024.10.1
|
||||||
@@ -61,6 +67,8 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- authentik-redis
|
- authentik-redis
|
||||||
- db-postgres
|
- db-postgres
|
||||||
|
networks:
|
||||||
|
- fulltest
|
||||||
|
|
||||||
authentik-ldap:
|
authentik-ldap:
|
||||||
image: ghcr.io/goauthentik/ldap:2024.10.1
|
image: ghcr.io/goauthentik/ldap:2024.10.1
|
||||||
@@ -71,6 +79,8 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- authentik
|
- authentik
|
||||||
|
networks:
|
||||||
|
- fulltest
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
psql_vol:
|
psql_vol:
|
||||||
|
|||||||
@@ -3,31 +3,34 @@
|
|||||||
# This is a base compose file, it should be extended with a
|
# This is a base compose file, it should be extended with a
|
||||||
# docker-compose.ci.*.yml file
|
# docker-compose.ci.*.yml file
|
||||||
services:
|
services:
|
||||||
|
|
||||||
fullstack:
|
fullstack:
|
||||||
image: "${IMAGE}:${BRANCH_LOWER}-ci-${BUILD_NUMBER}"
|
image: "${IMAGE}:${BRANCH_LOWER}-ci-${BUILD_NUMBER}"
|
||||||
environment:
|
environment:
|
||||||
TZ: "${TZ:-Australia/Brisbane}"
|
TZ: "${TZ:-Australia/Brisbane}"
|
||||||
DEBUG: 'true'
|
DEBUG: "true"
|
||||||
CI: 'true'
|
CI: "true"
|
||||||
FORCE_COLOR: 1
|
FORCE_COLOR: 1
|
||||||
# Required for DNS Certificate provisioning in CI
|
# Required for DNS Certificate provisioning in CI
|
||||||
LE_SERVER: 'https://ca.internal/acme/acme/directory'
|
LE_SERVER: "https://ca.internal/acme/acme/directory"
|
||||||
REQUESTS_CA_BUNDLE: '/etc/ssl/certs/NginxProxyManager.crt'
|
REQUESTS_CA_BUNDLE: "/etc/ssl/certs/NginxProxyManager.crt"
|
||||||
volumes:
|
volumes:
|
||||||
- 'npm_data_ci:/data'
|
- "npm_data_ci:/data"
|
||||||
- 'npm_le_ci:/etc/letsencrypt'
|
- "npm_le_ci:/etc/letsencrypt"
|
||||||
- './dev/letsencrypt.ini:/etc/letsencrypt.ini:ro'
|
- "./dev/letsencrypt.ini:/etc/letsencrypt.ini:ro"
|
||||||
- './dev/resolv.conf:/etc/resolv.conf:ro'
|
- "./dev/resolv.conf:/etc/resolv.conf:ro"
|
||||||
- '/etc/localtime:/etc/localtime:ro'
|
- "/etc/localtime:/etc/localtime:ro"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "/usr/bin/check-health"]
|
test: ["CMD", "/usr/bin/check-health"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
expose:
|
expose:
|
||||||
- '80-81/tcp'
|
- "80/tcp"
|
||||||
- '443/tcp'
|
- "81/tcp"
|
||||||
- '1500-1503/tcp'
|
- "443/tcp"
|
||||||
|
- "1500/tcp"
|
||||||
|
- "1501/tcp"
|
||||||
|
- "1502/tcp"
|
||||||
|
- "1503/tcp"
|
||||||
networks:
|
networks:
|
||||||
fulltest:
|
fulltest:
|
||||||
aliases:
|
aliases:
|
||||||
@@ -38,8 +41,8 @@ services:
|
|||||||
stepca:
|
stepca:
|
||||||
image: jc21/testca
|
image: jc21/testca
|
||||||
volumes:
|
volumes:
|
||||||
- './dev/resolv.conf:/etc/resolv.conf:ro'
|
- "./dev/resolv.conf:/etc/resolv.conf:ro"
|
||||||
- '/etc/localtime:/etc/localtime:ro'
|
- "/etc/localtime:/etc/localtime:ro"
|
||||||
networks:
|
networks:
|
||||||
fulltest:
|
fulltest:
|
||||||
aliases:
|
aliases:
|
||||||
@@ -48,18 +51,18 @@ services:
|
|||||||
pdns:
|
pdns:
|
||||||
image: pschiffe/pdns-mysql:4.8
|
image: pschiffe/pdns-mysql:4.8
|
||||||
volumes:
|
volumes:
|
||||||
- '/etc/localtime:/etc/localtime:ro'
|
- "/etc/localtime:/etc/localtime:ro"
|
||||||
environment:
|
environment:
|
||||||
PDNS_master: 'yes'
|
PDNS_master: "yes"
|
||||||
PDNS_api: 'yes'
|
PDNS_api: "yes"
|
||||||
PDNS_api_key: 'npm'
|
PDNS_api_key: "npm"
|
||||||
PDNS_webserver: 'yes'
|
PDNS_webserver: "yes"
|
||||||
PDNS_webserver_address: '0.0.0.0'
|
PDNS_webserver_address: "0.0.0.0"
|
||||||
PDNS_webserver_password: 'npm'
|
PDNS_webserver_password: "npm"
|
||||||
PDNS_webserver-allow-from: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8'
|
PDNS_webserver-allow-from: "127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8"
|
||||||
PDNS_version_string: 'anonymous'
|
PDNS_version_string: "anonymous"
|
||||||
PDNS_default_ttl: 1500
|
PDNS_default_ttl: 1500
|
||||||
PDNS_allow_axfr_ips: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8'
|
PDNS_allow_axfr_ips: "127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8"
|
||||||
PDNS_gmysql_host: pdns-db
|
PDNS_gmysql_host: pdns-db
|
||||||
PDNS_gmysql_port: 3306
|
PDNS_gmysql_port: 3306
|
||||||
PDNS_gmysql_user: pdns
|
PDNS_gmysql_user: pdns
|
||||||
@@ -76,14 +79,14 @@ services:
|
|||||||
pdns-db:
|
pdns-db:
|
||||||
image: mariadb
|
image: mariadb
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: 'pdns'
|
MYSQL_ROOT_PASSWORD: "pdns"
|
||||||
MYSQL_DATABASE: 'pdns'
|
MYSQL_DATABASE: "pdns"
|
||||||
MYSQL_USER: 'pdns'
|
MYSQL_USER: "pdns"
|
||||||
MYSQL_PASSWORD: 'pdns'
|
MYSQL_PASSWORD: "pdns"
|
||||||
volumes:
|
volumes:
|
||||||
- 'pdns_mysql_vol:/var/lib/mysql'
|
- "pdns_mysql_vol:/var/lib/mysql"
|
||||||
- '/etc/localtime:/etc/localtime:ro'
|
- "/etc/localtime:/etc/localtime:ro"
|
||||||
- './dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro'
|
- "./dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro"
|
||||||
networks:
|
networks:
|
||||||
- fulltest
|
- fulltest
|
||||||
|
|
||||||
@@ -100,12 +103,12 @@ services:
|
|||||||
context: ../
|
context: ../
|
||||||
dockerfile: test/cypress/Dockerfile
|
dockerfile: test/cypress/Dockerfile
|
||||||
environment:
|
environment:
|
||||||
HTTP_PROXY: 'squid:3128'
|
HTTP_PROXY: "squid:3128"
|
||||||
HTTPS_PROXY: 'squid:3128'
|
HTTPS_PROXY: "squid:3128"
|
||||||
volumes:
|
volumes:
|
||||||
- 'cypress_logs:/test/results'
|
- "cypress_logs:/test/results"
|
||||||
- './dev/resolv.conf:/etc/resolv.conf:ro'
|
- "./dev/resolv.conf:/etc/resolv.conf:ro"
|
||||||
- '/etc/localtime:/etc/localtime:ro'
|
- "/etc/localtime:/etc/localtime:ro"
|
||||||
command: cypress run --browser chrome --config-file=cypress/config/ci.js
|
command: cypress run --browser chrome --config-file=cypress/config/ci.js
|
||||||
networks:
|
networks:
|
||||||
- fulltest
|
- fulltest
|
||||||
@@ -113,9 +116,9 @@ services:
|
|||||||
squid:
|
squid:
|
||||||
image: ubuntu/squid
|
image: ubuntu/squid
|
||||||
volumes:
|
volumes:
|
||||||
- './dev/squid.conf:/etc/squid/squid.conf:ro'
|
- "./dev/squid.conf:/etc/squid/squid.conf:ro"
|
||||||
- './dev/resolv.conf:/etc/resolv.conf:ro'
|
- "./dev/resolv.conf:/etc/resolv.conf:ro"
|
||||||
- '/etc/localtime:/etc/localtime:ro'
|
- "/etc/localtime:/etc/localtime:ro"
|
||||||
networks:
|
networks:
|
||||||
- fulltest
|
- fulltest
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ services:
|
|||||||
# DB_MYSQL_PASSWORD: 'npm'
|
# DB_MYSQL_PASSWORD: 'npm'
|
||||||
# DB_MYSQL_NAME: 'npm'
|
# DB_MYSQL_NAME: 'npm'
|
||||||
# db-postgres:
|
# db-postgres:
|
||||||
DB_POSTGRES_HOST: "db-postgres"
|
DB_POSTGRES_HOST: "pgdb.internal"
|
||||||
DB_POSTGRES_PORT: "5432"
|
DB_POSTGRES_PORT: "5432"
|
||||||
DB_POSTGRES_USER: "npm"
|
DB_POSTGRES_USER: "npm"
|
||||||
DB_POSTGRES_PASSWORD: "npmpass"
|
DB_POSTGRES_PASSWORD: "npmpass"
|
||||||
@@ -81,8 +81,6 @@ services:
|
|||||||
db-postgres:
|
db-postgres:
|
||||||
image: postgres:17
|
image: postgres:17
|
||||||
container_name: npm2dev.db-postgres
|
container_name: npm2dev.db-postgres
|
||||||
networks:
|
|
||||||
- nginx_proxy_manager
|
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: "npm"
|
POSTGRES_USER: "npm"
|
||||||
POSTGRES_PASSWORD: "npmpass"
|
POSTGRES_PASSWORD: "npmpass"
|
||||||
@@ -90,6 +88,10 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- psql_data:/var/lib/postgresql/data
|
- psql_data:/var/lib/postgresql/data
|
||||||
- ./ci/postgres:/docker-entrypoint-initdb.d
|
- ./ci/postgres:/docker-entrypoint-initdb.d
|
||||||
|
networks:
|
||||||
|
nginx_proxy_manager:
|
||||||
|
aliases:
|
||||||
|
- pgdb.internal
|
||||||
|
|
||||||
stepca:
|
stepca:
|
||||||
image: jc21/testca
|
image: jc21/testca
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ server {
|
|||||||
set $port "80";
|
set $port "80";
|
||||||
|
|
||||||
server_name localhost-nginx-proxy-manager;
|
server_name localhost-nginx-proxy-manager;
|
||||||
access_log /data/logs/fallback_access.log standard;
|
access_log /data/logs/fallback_http_access.log standard;
|
||||||
error_log /data/logs/fallback_error.log warn;
|
error_log /data/logs/fallback_http_error.log warn;
|
||||||
include conf.d/include/assets.conf;
|
include conf.d/include/assets.conf;
|
||||||
include conf.d/include/block-exploits.conf;
|
include conf.d/include/block-exploits.conf;
|
||||||
include conf.d/include/letsencrypt-acme-challenge.conf;
|
include conf.d/include/letsencrypt-acme-challenge.conf;
|
||||||
@@ -30,7 +30,7 @@ server {
|
|||||||
set $port "443";
|
set $port "443";
|
||||||
|
|
||||||
server_name localhost;
|
server_name localhost;
|
||||||
access_log /data/logs/fallback_access.log standard;
|
access_log /data/logs/fallback_http_access.log standard;
|
||||||
error_log /dev/null crit;
|
error_log /dev/null crit;
|
||||||
include conf.d/include/ssl-ciphers.conf;
|
include conf.d/include/ssl-ciphers.conf;
|
||||||
ssl_reject_handshake on;
|
ssl_reject_handshake on;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
log_format proxy '[$time_local] $upstream_cache_status $upstream_status $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] [Sent-to $server] "$http_user_agent" "$http_referer"';
|
log_format proxy '[$time_local] $upstream_cache_status $upstream_status $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] [Sent-to $server] "$http_user_agent" "$http_referer"';
|
||||||
log_format standard '[$time_local] $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] "$http_user_agent" "$http_referer"';
|
log_format standard '[$time_local] $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] "$http_user_agent" "$http_referer"';
|
||||||
|
|
||||||
access_log /data/logs/fallback_access.log proxy;
|
access_log /data/logs/fallback_http_access.log proxy;
|
||||||
3
docker/rootfs/etc/nginx/conf.d/include/log-stream.conf
Normal file
3
docker/rootfs/etc/nginx/conf.d/include/log-stream.conf
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
log_format stream '[$time_local] [Client $remote_addr:$remote_port] $protocol $status $bytes_sent $bytes_received $session_time [Sent-to $upstream_addr] [Sent $upstream_bytes_sent] [Received $upstream_bytes_received] [Time $upstream_connect_time] $ssl_protocol $ssl_cipher';
|
||||||
|
|
||||||
|
access_log /data/logs/fallback_stream_access.log stream;
|
||||||
@@ -47,7 +47,7 @@ http {
|
|||||||
proxy_cache_path /var/lib/nginx/cache/private levels=1:2 keys_zone=private-cache:5m max_size=1024m;
|
proxy_cache_path /var/lib/nginx/cache/private levels=1:2 keys_zone=private-cache:5m max_size=1024m;
|
||||||
|
|
||||||
# Log format and fallback log file
|
# Log format and fallback log file
|
||||||
include /etc/nginx/conf.d/include/log.conf;
|
include /etc/nginx/conf.d/include/log-proxy.conf;
|
||||||
|
|
||||||
# Dynamically generated resolvers file
|
# Dynamically generated resolvers file
|
||||||
include /etc/nginx/conf.d/include/resolvers.conf;
|
include /etc/nginx/conf.d/include/resolvers.conf;
|
||||||
@@ -85,6 +85,9 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stream {
|
stream {
|
||||||
|
# Log format and fallback log file
|
||||||
|
include /etc/nginx/conf.d/include/log-stream.conf;
|
||||||
|
|
||||||
# Files generated by NPM
|
# Files generated by NPM
|
||||||
include /data/nginx/stream/*.conf;
|
include /data/nginx/stream/*.conf;
|
||||||
|
|
||||||
|
|||||||
946
docs/yarn.lock
946
docs/yarn.lock
File diff suppressed because it is too large
Load Diff
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
src/locale/lang
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -8,7 +8,19 @@
|
|||||||
|
|
||||||
const allLocales = [
|
const allLocales = [
|
||||||
["en", "en-US"],
|
["en", "en-US"],
|
||||||
["fa", "fa-IR"],
|
["de", "de-DE"],
|
||||||
|
["es", "es-ES"],
|
||||||
|
["it", "it-IT"],
|
||||||
|
["ja", "ja-JP"],
|
||||||
|
["nl", "nl-NL"],
|
||||||
|
["pl", "pl-PL"],
|
||||||
|
["ru", "ru-RU"],
|
||||||
|
["sk", "sk-SK"],
|
||||||
|
["vi", "vi-VN"],
|
||||||
|
["zh", "zh-CN"],
|
||||||
|
["ko", "ko-KR"],
|
||||||
|
["bg", "bg-BG"],
|
||||||
|
["id", "id-ID"],
|
||||||
];
|
];
|
||||||
|
|
||||||
const ignoreUnused = [
|
const ignoreUnused = [
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<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"
|
||||||
|
|||||||
@@ -29,9 +29,9 @@
|
|||||||
"generate-password-browser": "^1.1.0",
|
"generate-password-browser": "^1.1.0",
|
||||||
"humps": "^2.0.1",
|
"humps": "^2.0.1",
|
||||||
"query-string": "^9.3.1",
|
"query-string": "^9.3.1",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.3",
|
||||||
"react-bootstrap": "^2.10.10",
|
"react-bootstrap": "^2.10.10",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.3",
|
||||||
"react-intl": "^7.1.14",
|
"react-intl": "^7.1.14",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-dom": "^7.9.5",
|
"react-router-dom": "^7.9.5",
|
||||||
@@ -48,10 +48,10 @@
|
|||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@types/country-flag-icons": "^1.2.2",
|
"@types/country-flag-icons": "^1.2.2",
|
||||||
"@types/humps": "^2.0.6",
|
"@types/humps": "^2.0.6",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/react-table": "^7.7.20",
|
"@types/react-table": "^7.7.20",
|
||||||
"@vitejs/plugin-react": "^5.1.0",
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
"happy-dom": "^20.0.10",
|
"happy-dom": "^20.0.10",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
|
|||||||
@@ -13,6 +13,15 @@
|
|||||||
--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;
|
||||||
}
|
}
|
||||||
@@ -95,3 +104,15 @@ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -156,7 +156,6 @@ export async function del({ url, params }: DeleteArgs, abortController?: AbortCo
|
|||||||
const method = "DELETE";
|
const method = "DELETE";
|
||||||
const headers = {
|
const headers = {
|
||||||
...buildAuthHeader(),
|
...buildAuthHeader(),
|
||||||
[contentTypeHeader]: "application/json",
|
|
||||||
};
|
};
|
||||||
const signal = abortController?.signal;
|
const signal = abortController?.signal;
|
||||||
const response = await fetch(apiUrl, { method, headers, signal });
|
const response = await fetch(apiUrl, { method, headers, signal });
|
||||||
|
|||||||
8
frontend/src/api/backend/checkVersion.ts
Normal file
8
frontend/src/api/backend/checkVersion.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import * as api from "./base";
|
||||||
|
import type { VersionCheckResponse } from "./responseTypes";
|
||||||
|
|
||||||
|
export async function checkVersion(): Promise<VersionCheckResponse> {
|
||||||
|
return await api.get({
|
||||||
|
url: "/version/check",
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,9 +1,22 @@
|
|||||||
import * as api from "./base";
|
import * as api from "./base";
|
||||||
import type { TokenResponse } from "./responseTypes";
|
import type { TokenResponse, TwoFactorChallengeResponse } from "./responseTypes";
|
||||||
|
|
||||||
export async function getToken(identity: string, secret: string): Promise<TokenResponse> {
|
export type LoginResponse = TokenResponse | TwoFactorChallengeResponse;
|
||||||
|
|
||||||
|
export function isTwoFactorChallenge(response: LoginResponse): response is TwoFactorChallengeResponse {
|
||||||
|
return "requires2fa" in response && response.requires2fa === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getToken(identity: string, secret: string): Promise<LoginResponse> {
|
||||||
return await api.post({
|
return await api.post({
|
||||||
url: "/tokens",
|
url: "/tokens",
|
||||||
data: { identity, secret },
|
data: { identity, secret },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function verify2FA(challengeToken: string, code: string): Promise<TokenResponse> {
|
||||||
|
return await api.post({
|
||||||
|
url: "/tokens/2fa",
|
||||||
|
data: { challengeToken, code },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from "./checkVersion";
|
||||||
export * from "./createAccessList";
|
export * from "./createAccessList";
|
||||||
export * from "./createCertificate";
|
export * from "./createCertificate";
|
||||||
export * from "./createDeadHost";
|
export * from "./createDeadHost";
|
||||||
@@ -59,3 +60,4 @@ export * from "./updateStream";
|
|||||||
export * from "./updateUser";
|
export * from "./updateUser";
|
||||||
export * from "./uploadCertificate";
|
export * from "./uploadCertificate";
|
||||||
export * from "./validateCertificate";
|
export * from "./validateCertificate";
|
||||||
|
export * from "./twoFactor";
|
||||||
|
|||||||
@@ -19,3 +19,28 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TwoFactorChallengeResponse {
|
||||||
|
requires2fa: boolean;
|
||||||
|
challengeToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TwoFactorStatusResponse {
|
||||||
|
enabled: boolean;
|
||||||
|
backupCodesRemaining: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TwoFactorSetupResponse {
|
||||||
|
secret: string;
|
||||||
|
otpauthUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TwoFactorEnableResponse {
|
||||||
|
backupCodes: string[];
|
||||||
|
}
|
||||||
|
|||||||
37
frontend/src/api/backend/twoFactor.ts
Normal file
37
frontend/src/api/backend/twoFactor.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import * as api from "./base";
|
||||||
|
import type { TwoFactorEnableResponse, TwoFactorSetupResponse, TwoFactorStatusResponse } from "./responseTypes";
|
||||||
|
|
||||||
|
export async function get2FAStatus(userId: number | "me"): Promise<TwoFactorStatusResponse> {
|
||||||
|
return await api.get({
|
||||||
|
url: `/users/${userId}/2fa`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function start2FASetup(userId: number | "me"): Promise<TwoFactorSetupResponse> {
|
||||||
|
return await api.post({
|
||||||
|
url: `/users/${userId}/2fa`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function enable2FA(userId: number | "me", code: string): Promise<TwoFactorEnableResponse> {
|
||||||
|
return await api.post({
|
||||||
|
url: `/users/${userId}/2fa/enable`,
|
||||||
|
data: { code },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function disable2FA(userId: number | "me", code: string): Promise<boolean> {
|
||||||
|
return await api.del({
|
||||||
|
url: `/users/${userId}/2fa`,
|
||||||
|
params: {
|
||||||
|
code,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function regenerateBackupCodes(userId: number | "me", code: string): Promise<TwoFactorEnableResponse> {
|
||||||
|
return await api.post({
|
||||||
|
url: `/users/${userId}/2fa/backup-codes`,
|
||||||
|
data: { code },
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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 { T } from "src/locale";
|
import { intl, 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">Allow</option>
|
<option value="allow"><T id="action.allow" /></option>
|
||||||
<option value="deny">Deny</option>
|
<option value="deny"><T id="action.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="192.168.1.100 or 192.168.1.0/24 or 2001:0db8::/32"
|
placeholder={intl.formatMessage({ id: "access-list.rule-source.placeholder" })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,7 +112,7 @@ export function AccessClientFields({ initialValues, name = "clients" }: Props) {
|
|||||||
value="deny"
|
value="deny"
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
<option value="deny">Deny</option>
|
<option value="deny"><T id="action.deny" /></option>
|
||||||
</select>
|
</select>
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { Field, useFormikContext } from "formik";
|
|||||||
import type { ReactNode } from "react";
|
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 { useLocaleState } from "src/context";
|
||||||
import { useAccessLists } from "src/hooks";
|
import { useAccessLists } from "src/hooks";
|
||||||
import { DateTimeFormat, intl, T } from "src/locale";
|
import { formatDateTime, intl, T } from "src/locale";
|
||||||
|
|
||||||
interface AccessOption {
|
interface AccessOption {
|
||||||
readonly value: number;
|
readonly value: number;
|
||||||
@@ -32,6 +33,7 @@ interface Props {
|
|||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
export function AccessField({ name = "accessListId", label = "access-list", id = "accessListId" }: Props) {
|
export function AccessField({ name = "accessListId", label = "access-list", id = "accessListId" }: Props) {
|
||||||
|
const { locale } = useLocaleState();
|
||||||
const { isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]);
|
const { isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]);
|
||||||
const { setFieldValue } = useFormikContext();
|
const { setFieldValue } = useFormikContext();
|
||||||
|
|
||||||
@@ -48,7 +50,7 @@ export function AccessField({ name = "accessListId", label = "access-list", id =
|
|||||||
{
|
{
|
||||||
users: item?.items?.length,
|
users: item?.items?.length,
|
||||||
rules: item?.clients?.length,
|
rules: item?.clients?.length,
|
||||||
date: item?.createdOn ? DateTimeFormat(item?.createdOn) : "N/A",
|
date: item?.createdOn ? formatDateTime(item?.createdOn, locale) : "N/A",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
icon: <IconLock size={14} className="text-lime" />,
|
icon: <IconLock size={14} className="text-lime" />,
|
||||||
|
|||||||
@@ -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 { T } from "src/locale";
|
import { intl, 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="Select a Provider..."
|
placeholder={intl.formatMessage({ id: "certificates.dns.provider.placeholder" })}
|
||||||
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={600}
|
max={7200}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
<small className="text-muted">
|
<small className="text-muted">
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { IconShield } from "@tabler/icons-react";
|
|||||||
import { Field, useFormikContext } from "formik";
|
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 { useLocaleState } from "src/context";
|
||||||
import { useCertificates } from "src/hooks";
|
import { useCertificates } from "src/hooks";
|
||||||
import { DateTimeFormat, intl, T } from "src/locale";
|
import { formatDateTime, intl, T } from "src/locale";
|
||||||
|
|
||||||
interface CertOption {
|
interface CertOption {
|
||||||
readonly value: number | "new";
|
readonly value: number | "new";
|
||||||
@@ -41,6 +42,7 @@ export function SSLCertificateField({
|
|||||||
allowNew,
|
allowNew,
|
||||||
forHttp = true,
|
forHttp = true,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const { locale } = useLocaleState();
|
||||||
const { isLoading, isError, error, data } = useCertificates();
|
const { isLoading, isError, error, data } = useCertificates();
|
||||||
const { values, setFieldValue } = useFormikContext();
|
const { values, setFieldValue } = useFormikContext();
|
||||||
const v: any = values || {};
|
const v: any = values || {};
|
||||||
@@ -75,7 +77,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 ? 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, locale) : "N/A" })}`,
|
||||||
icon: <IconShield size={14} className="text-pink" />,
|
icon: <IconShield size={14} className="text-pink" />,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ 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";
|
||||||
|
|
||||||
function LocalePicker() {
|
interface Props {
|
||||||
|
menuAlign?: "start" | "end";
|
||||||
|
}
|
||||||
|
|
||||||
|
function LocalePicker({ menuAlign = "start" }: Props) {
|
||||||
const { locale, setLocale } = useLocaleState();
|
const { locale, setLocale } = useLocaleState();
|
||||||
const { getTheme } = useTheme();
|
const { getTheme } = useTheme();
|
||||||
|
|
||||||
@@ -23,22 +27,24 @@ function LocalePicker() {
|
|||||||
<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 className="dropdown-menu">
|
<div
|
||||||
{localeOptions.map((item) => {
|
className={cn("dropdown-menu", {
|
||||||
return (
|
"dropdown-menu-end": menuAlign === "end",
|
||||||
<a
|
|
||||||
className="dropdown-item"
|
|
||||||
href={`/locale/${item[0]}`}
|
|
||||||
key={`locale-${item[0]}`}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
changeTo(item[0]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flag countryCode={getFlagCodeForLocale(item[0])} /> <T id={`locale-${item[1]}`} />
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
|
>
|
||||||
|
{localeOptions.map((item: any) => (
|
||||||
|
<a
|
||||||
|
className="dropdown-item"
|
||||||
|
href={`/locale/${item[0]}`}
|
||||||
|
key={`locale-${item[0]}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
changeTo(item[0]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flag countryCode={getFlagCodeForLocale(item[0])} /> <T id={`locale-${item[1]}`} />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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">{children}</div>;
|
return <div className="container-xl py-3 min-w-0 overflow-x-auto">{children}</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useHealth } from "src/hooks";
|
import { useCheckVersion, useHealth } from "src/hooks";
|
||||||
import { T } from "src/locale";
|
import { T } from "src/locale";
|
||||||
|
|
||||||
export function SiteFooter() {
|
export function SiteFooter() {
|
||||||
const health = useHealth();
|
const health = useHealth();
|
||||||
|
const { data: versionData } = useCheckVersion();
|
||||||
|
|
||||||
const getVersion = () => {
|
const getVersion = () => {
|
||||||
if (!health.data) {
|
if (!health.data) {
|
||||||
@@ -55,6 +56,19 @@ export function SiteFooter() {
|
|||||||
{getVersion()}{" "}
|
{getVersion()}{" "}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{versionData?.updateAvailable && versionData?.latest && (
|
||||||
|
<li className="list-inline-item">
|
||||||
|
<a
|
||||||
|
href={`https://github.com/NginxProxyManager/nginx-proxy-manager/releases/tag/${versionData.latest}`}
|
||||||
|
className="link-warning fw-bold"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
title={`New version ${versionData.latest} is available`}
|
||||||
|
>
|
||||||
|
<T id="update-available" data={{ latestVersion: versionData.latest }} />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { IconLock, IconLogout, IconUser } from "@tabler/icons-react";
|
import { IconLock, IconLogout, IconShieldLock, IconUser } from "@tabler/icons-react";
|
||||||
import { LocalePicker, NavLink, ThemeSwitcher } from "src/components";
|
import { LocalePicker, NavLink, ThemeSwitcher } from "src/components";
|
||||||
import { useAuthState } from "src/context";
|
import { useAuthState } from "src/context";
|
||||||
import { useUser } from "src/hooks";
|
import { useUser } from "src/hooks";
|
||||||
import { T } from "src/locale";
|
import { T } from "src/locale";
|
||||||
import { showChangePasswordModal, showUserModal } from "src/modals";
|
import { showChangePasswordModal, showTwoFactorModal, showUserModal } from "src/modals";
|
||||||
import styles from "./SiteHeader.module.css";
|
import styles from "./SiteHeader.module.css";
|
||||||
|
|
||||||
export function SiteHeader() {
|
export function SiteHeader() {
|
||||||
@@ -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 d-none-navbar-horizontal pe-0 pe-md-3">
|
<div className="navbar-brand navbar-brand-autodark 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-none d-md-flex me-3">
|
<div className="nav-item d-md-flex">
|
||||||
<div className="nav-item dropdown">
|
<div className="nav-item dropdown">
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
className="nav-link d-flex lh-1 p-0 px-2"
|
className="nav-link d-flex lh-1"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
aria-label="Open user menu"
|
aria-label="Open user menu"
|
||||||
>
|
>
|
||||||
@@ -70,6 +70,22 @@ 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"
|
||||||
@@ -92,6 +108,17 @@ export function SiteHeader() {
|
|||||||
<IconLock width={18} />
|
<IconLock width={18} />
|
||||||
<T id="user.change-password" />
|
<T id="user.change-password" />
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
href="?"
|
||||||
|
className="dropdown-item"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
showTwoFactorModal("me");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconShieldLock width={18} />
|
||||||
|
<T id="user.two-factor" />
|
||||||
|
</a>
|
||||||
<div className="dropdown-divider" />
|
<div className="dropdown-divider" />
|
||||||
<a
|
<a
|
||||||
href="?"
|
href="?"
|
||||||
|
|||||||
@@ -176,21 +176,17 @@ const getMenuDropown = (item: MenuItem, onClick?: () => void) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function SiteMenu() {
|
export function SiteMenu() {
|
||||||
// This is hacky AF. But that's the price of using a non-react UI kit.
|
const closeMenu = () => setTimeout(() => {
|
||||||
const closeMenus = () => {
|
const navbarToggler = document.querySelector<HTMLElement>(".navbar-toggler");
|
||||||
const navMenus = document.querySelectorAll(".nav-item.dropdown");
|
const navbarMenu = document.querySelector("#navbar-menu");
|
||||||
navMenus.forEach((menu) => {
|
if (navbarToggler && navbarMenu?.classList.contains("show")) {
|
||||||
menu.classList.remove("show");
|
navbarToggler.click();
|
||||||
const dropdown = menu.querySelector(".dropdown-menu");
|
}
|
||||||
if (dropdown) {
|
}, 300);
|
||||||
dropdown.classList.remove("show");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="navbar-expand-md">
|
<header className="navbar-expand-md">
|
||||||
<div className="collapse navbar-collapse">
|
<div className="collapse navbar-collapse" id="navbar-menu">
|
||||||
<div className="navbar">
|
<div className="navbar">
|
||||||
<div className="container-xl">
|
<div className="container-xl">
|
||||||
<div className="row flex-column flex-md-row flex-fill align-items-center">
|
<div className="row flex-column flex-md-row flex-fill align-items-center">
|
||||||
@@ -198,7 +194,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, closeMenus);
|
return getMenuItem(item, closeMenu);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { differenceInDays, isPast, parseISO } from "date-fns";
|
import { differenceInDays, isPast } from "date-fns";
|
||||||
import { DateTimeFormat } from "src/locale";
|
import { useLocaleState } from "src/context";
|
||||||
|
import { formatDateTime, parseDate } from "src/locale";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -8,11 +9,13 @@ interface Props {
|
|||||||
highlistNearlyExpired?: boolean;
|
highlistNearlyExpired?: boolean;
|
||||||
}
|
}
|
||||||
export function DateFormatter({ value, highlightPast, highlistNearlyExpired }: Props) {
|
export function DateFormatter({ value, highlightPast, highlistNearlyExpired }: Props) {
|
||||||
const dateIsPast = isPast(parseISO(value));
|
const { locale } = useLocaleState();
|
||||||
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({
|
const cl = cn({
|
||||||
"text-danger": highlightPast && dateIsPast,
|
"text-danger": highlightPast && dateIsPast,
|
||||||
"text-warning": highlistNearlyExpired && !dateIsPast && days <= 30 && days >= 0,
|
"text-warning": highlistNearlyExpired && !dateIsPast && days <= 30 && days >= 0,
|
||||||
});
|
});
|
||||||
return <span className={cl}>{DateTimeFormat(value)}</span>;
|
return <span className={cl}>{formatDateTime(value, locale)}</span>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { DateTimeFormat, T } from "src/locale";
|
import { useLocaleState } from "src/context";
|
||||||
|
import { formatDateTime, T } from "src/locale";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
domains: string[];
|
domains: string[];
|
||||||
@@ -10,35 +11,44 @@ interface Props {
|
|||||||
color?: string;
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DomainLink = ({ domain, color }: { domain: string; color?: string }) => {
|
const DomainLink = ({ domain, color }: { domain?: string; color?: string }) => {
|
||||||
// when domain contains a wildcard, make the link go nowhere.
|
// when domain contains a wildcard, make the link go nowhere.
|
||||||
let onClick: ((e: React.MouseEvent) => void) | undefined;
|
// Apparently the domain can be null or undefined sometimes.
|
||||||
if (domain.includes("*")) {
|
// This try is just a safeguard to prevent the whole formatter from breaking.
|
||||||
onClick = (e: React.MouseEvent) => e.preventDefault();
|
if (!domain) return null;
|
||||||
|
try {
|
||||||
|
let onClick: ((e: React.MouseEvent) => void) | undefined;
|
||||||
|
if (domain.includes("*")) {
|
||||||
|
onClick = (e: React.MouseEvent) => e.preventDefault();
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={domain}
|
||||||
|
href={`http://${domain}`}
|
||||||
|
target="_blank"
|
||||||
|
onClick={onClick}
|
||||||
|
className={cn("badge", color ? `bg-${color}-lt` : null, "domain-name", "me-2")}
|
||||||
|
>
|
||||||
|
{domain}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<a
|
|
||||||
key={domain}
|
|
||||||
href={`http://${domain}`}
|
|
||||||
target="_blank"
|
|
||||||
onClick={onClick}
|
|
||||||
className={cn("badge", color ? `bg-${color}-lt` : null, "domain-name", "me-2")}
|
|
||||||
>
|
|
||||||
{domain}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DomainsFormatter({ domains, createdOn, niceName, provider, color }: Props) {
|
export function DomainsFormatter({ domains, createdOn, niceName, provider, color }: Props) {
|
||||||
|
const { locale } = useLocaleState();
|
||||||
const elms: ReactNode[] = [];
|
const elms: ReactNode[] = [];
|
||||||
if (domains.length === 0 && !niceName) {
|
|
||||||
|
if ((!domains || domains.length === 0) && !niceName) {
|
||||||
elms.push(
|
elms.push(
|
||||||
<span key="nice-name" className="badge bg-danger-lt me-2">
|
<span key="nice-name" className="badge bg-danger-lt me-2">
|
||||||
Unknown
|
Unknown
|
||||||
</span>,
|
</span>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (niceName && provider !== "letsencrypt") {
|
if (!domains || (niceName && provider !== "letsencrypt")) {
|
||||||
elms.push(
|
elms.push(
|
||||||
<span key="nice-name" className="badge bg-info-lt me-2">
|
<span key="nice-name" className="badge bg-info-lt me-2">
|
||||||
{niceName}
|
{niceName}
|
||||||
@@ -46,14 +56,16 @@ export function DomainsFormatter({ domains, createdOn, niceName, provider, color
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
domains.map((domain: string) => elms.push(<DomainLink key={domain} domain={domain} color={color} />));
|
if (domains) {
|
||||||
|
domains.map((domain: string) => elms.push(<DomainLink key={domain} domain={domain} color={color} />));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-fill">
|
<div className="flex-fill">
|
||||||
<div className="font-weight-medium">{...elms}</div>
|
<div className="font-weight-medium">{...elms}</div>
|
||||||
{createdOn ? (
|
{createdOn ? (
|
||||||
<div className="text-secondary mt-1">
|
<div className="text-secondary mt-1">
|
||||||
<T id="created-on" data={{ date: DateTimeFormat(createdOn) }} />
|
<T id="created-on" data={{ date: formatDateTime(createdOn, locale) }} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconShield, IconUser } from "@tabler/icons-react";
|
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconShield, IconUser } from "@tabler/icons-react";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import type { AuditLog } from "src/api/backend";
|
import type { AuditLog } from "src/api/backend";
|
||||||
import { DateTimeFormat, T } from "src/locale";
|
import { useLocaleState } from "src/context";
|
||||||
|
import { formatDateTime, T } from "src/locale";
|
||||||
|
|
||||||
const getEventValue = (event: AuditLog) => {
|
const getEventValue = (event: AuditLog) => {
|
||||||
switch (event.objectType) {
|
switch (event.objectType) {
|
||||||
@@ -66,6 +67,7 @@ interface Props {
|
|||||||
row: AuditLog;
|
row: AuditLog;
|
||||||
}
|
}
|
||||||
export function EventFormatter({ row }: Props) {
|
export function EventFormatter({ row }: Props) {
|
||||||
|
const { locale } = useLocaleState();
|
||||||
return (
|
return (
|
||||||
<div className="flex-fill">
|
<div className="flex-fill">
|
||||||
<div className="font-weight-medium">
|
<div className="font-weight-medium">
|
||||||
@@ -73,7 +75,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 }} />
|
||||||
— <span className="badge">{getEventValue(row)}</span>
|
— <span className="badge">{getEventValue(row)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-secondary mt-1">{DateTimeFormat(row.createdOn)}</div>
|
<div className="text-secondary mt-1">{formatDateTime(row.createdOn, locale)}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { DateTimeFormat, T } from "src/locale";
|
import { useLocaleState } from "src/context";
|
||||||
|
import { formatDateTime, T } from "src/locale";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -6,6 +7,7 @@ interface Props {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) {
|
export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) {
|
||||||
|
const { locale } = useLocaleState();
|
||||||
return (
|
return (
|
||||||
<div className="flex-fill">
|
<div className="flex-fill">
|
||||||
<div className="font-weight-medium">
|
<div className="font-weight-medium">
|
||||||
@@ -13,7 +15,7 @@ export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
{createdOn ? (
|
{createdOn ? (
|
||||||
<div className={`text-secondary mt-1 ${disabled ? "text-red" : ""}`}>
|
<div className={`text-secondary mt-1 ${disabled ? "text-red" : ""}`}>
|
||||||
<T id={disabled ? "disabled" : "created-on"} data={{ date: DateTimeFormat(createdOn) }} />
|
<T id={disabled ? "disabled" : "created-on"} data={{ date: formatDateTime(createdOn, locale) }} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ 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 (
|
||||||
<table className="table table-vcenter table-selectable mb-0">
|
<div className="table-responsive">
|
||||||
{hasRows ? <TableHeader tableInstance={props.tableInstance} /> : null}
|
<table className="table table-vcenter table-selectable mb-0">
|
||||||
<TableBody {...props} />
|
{hasRows ? <TableHeader tableInstance={props.tableInstance} /> : null}
|
||||||
</table>
|
<TableBody {...props} />
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,28 @@
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { createContext, type ReactNode, useContext, useState } from "react";
|
import { createContext, type ReactNode, useContext, useState } from "react";
|
||||||
import { useIntervalWhen } from "rooks";
|
import { useIntervalWhen } from "rooks";
|
||||||
import { getToken, loginAsUser, refreshToken, type TokenResponse } from "src/api/backend";
|
import {
|
||||||
|
getToken,
|
||||||
|
isTwoFactorChallenge,
|
||||||
|
loginAsUser,
|
||||||
|
refreshToken,
|
||||||
|
verify2FA,
|
||||||
|
type TokenResponse,
|
||||||
|
} from "src/api/backend";
|
||||||
import AuthStore from "src/modules/AuthStore";
|
import AuthStore from "src/modules/AuthStore";
|
||||||
|
|
||||||
|
// 2FA challenge state
|
||||||
|
export interface TwoFactorChallenge {
|
||||||
|
challengeToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
export interface AuthContextType {
|
export interface AuthContextType {
|
||||||
authenticated: boolean;
|
authenticated: boolean;
|
||||||
|
twoFactorChallenge: TwoFactorChallenge | null;
|
||||||
login: (username: string, password: string) => Promise<void>;
|
login: (username: string, password: string) => Promise<void>;
|
||||||
|
verifyTwoFactor: (code: string) => Promise<void>;
|
||||||
|
cancelTwoFactor: () => void;
|
||||||
loginAs: (id: number) => Promise<void>;
|
loginAs: (id: number) => Promise<void>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
token?: string;
|
token?: string;
|
||||||
@@ -24,17 +39,35 @@ interface Props {
|
|||||||
function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props) {
|
function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [authenticated, setAuthenticated] = useState(AuthStore.hasActiveToken());
|
const [authenticated, setAuthenticated] = useState(AuthStore.hasActiveToken());
|
||||||
|
const [twoFactorChallenge, setTwoFactorChallenge] = useState<TwoFactorChallenge | null>(null);
|
||||||
|
|
||||||
const handleTokenUpdate = (response: TokenResponse) => {
|
const handleTokenUpdate = (response: TokenResponse) => {
|
||||||
AuthStore.set(response);
|
AuthStore.set(response);
|
||||||
setAuthenticated(true);
|
setAuthenticated(true);
|
||||||
|
setTwoFactorChallenge(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const login = async (identity: string, secret: string) => {
|
const login = async (identity: string, secret: string) => {
|
||||||
const response = await getToken(identity, secret);
|
const response = await getToken(identity, secret);
|
||||||
|
if (isTwoFactorChallenge(response)) {
|
||||||
|
setTwoFactorChallenge({ challengeToken: response.challengeToken });
|
||||||
|
return;
|
||||||
|
}
|
||||||
handleTokenUpdate(response);
|
handleTokenUpdate(response);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const verifyTwoFactor = async (code: string) => {
|
||||||
|
if (!twoFactorChallenge) {
|
||||||
|
throw new Error("No 2FA challenge pending");
|
||||||
|
}
|
||||||
|
const response = await verify2FA(twoFactorChallenge.challengeToken, code);
|
||||||
|
handleTokenUpdate(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelTwoFactor = () => {
|
||||||
|
setTwoFactorChallenge(null);
|
||||||
|
};
|
||||||
|
|
||||||
const loginAs = async (id: number) => {
|
const loginAs = async (id: number) => {
|
||||||
const response = await loginAsUser(id);
|
const response = await loginAsUser(id);
|
||||||
AuthStore.add(response);
|
AuthStore.add(response);
|
||||||
@@ -69,7 +102,15 @@ function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props)
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
const value = { authenticated, login, logout, loginAs };
|
const value = {
|
||||||
|
authenticated,
|
||||||
|
twoFactorChallenge,
|
||||||
|
login,
|
||||||
|
verifyTwoFactor,
|
||||||
|
cancelTwoFactor,
|
||||||
|
loginAs,
|
||||||
|
logout,
|
||||||
|
};
|
||||||
|
|
||||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export * from "./useAuditLog";
|
|||||||
export * from "./useAuditLogs";
|
export * from "./useAuditLogs";
|
||||||
export * from "./useCertificate";
|
export * from "./useCertificate";
|
||||||
export * from "./useCertificates";
|
export * from "./useCertificates";
|
||||||
|
export * from "./useCheckVersion";
|
||||||
export * from "./useDeadHost";
|
export * from "./useDeadHost";
|
||||||
export * from "./useDeadHosts";
|
export * from "./useDeadHosts";
|
||||||
export * from "./useDnsProviders";
|
export * from "./useDnsProviders";
|
||||||
|
|||||||
18
frontend/src/hooks/useCheckVersion.ts
Normal file
18
frontend/src/hooks/useCheckVersion.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { checkVersion, type VersionCheckResponse } from "src/api/backend";
|
||||||
|
|
||||||
|
const fetchVersion = () => checkVersion();
|
||||||
|
|
||||||
|
const useCheckVersion = (options = {}) => {
|
||||||
|
return useQuery<VersionCheckResponse, Error>({
|
||||||
|
queryKey: ["version-check"],
|
||||||
|
queryFn: fetchVersion,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: 5,
|
||||||
|
refetchInterval: 30 * 1000, // 30 seconds
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 mins
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { fetchVersion, useCheckVersion };
|
||||||
@@ -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 };
|
|
||||||
65
frontend/src/locale/IntlProvider.tsx
Normal file → Executable file
65
frontend/src/locale/IntlProvider.tsx
Normal file → Executable file
@@ -1,25 +1,68 @@
|
|||||||
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 langGa from "./lang/ga.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";
|
||||||
|
import langId from "./lang/id.json";
|
||||||
|
|
||||||
// first item of each array should be the language code,
|
// first item of each array should be the language code,
|
||||||
// not the country code
|
// not the country code
|
||||||
// Remember when adding to this list, also update check-locales.js script
|
// Remember when adding to this list, also update check-locales.js script
|
||||||
const localeOptions = [["en", "en-US"]];
|
const localeOptions = [
|
||||||
|
["en", "en-US", langEn],
|
||||||
|
["de", "de-DE", langDe],
|
||||||
|
["es", "es-ES", langEs],
|
||||||
|
["ga", "ga-IE", langGa],
|
||||||
|
["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],
|
||||||
|
["id", "id-ID", langId],
|
||||||
|
];
|
||||||
|
|
||||||
const loadMessages = (locale?: string): typeof langList & typeof langEn => {
|
const loadMessages = (locale?: string): typeof langList & typeof langEn => {
|
||||||
const thisLocale = locale || "en";
|
const thisLocale = (locale || "en").slice(0, 2);
|
||||||
switch (thisLocale.slice(0, 2)) {
|
|
||||||
default:
|
// ensure this lang exists in localeOptions above, otherwise fallback to en
|
||||||
return Object.assign({}, langList, langEn);
|
if (thisLocale === "en" || !localeOptions.some(([code]) => code === thisLocale)) {
|
||||||
|
return Object.assign({}, langList, langEn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Object.assign({}, langList, langEn, localeOptions.find(([code]) => code === thisLocale)?.[2]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFlagCodeForLocale = (locale?: string) => {
|
const getFlagCodeForLocale = (locale?: string) => {
|
||||||
switch (locale) {
|
const thisLocale = (locale || "en").slice(0, 2);
|
||||||
default:
|
|
||||||
return "EN";
|
// only add to this if your flag is different from the locale code
|
||||||
|
const specialCases: Record<string, string> = {
|
||||||
|
ja: "jp", // Japan
|
||||||
|
zh: "cn", // China
|
||||||
|
vi: "vn", // Vietnam
|
||||||
|
ko: "kr", // Korea
|
||||||
|
};
|
||||||
|
|
||||||
|
if (specialCases[thisLocale]) {
|
||||||
|
return specialCases[thisLocale].toUpperCase();
|
||||||
}
|
}
|
||||||
|
return thisLocale.toUpperCase();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLocale = (short = false) => {
|
const getLocale = (short = false) => {
|
||||||
@@ -30,6 +73,10 @@ const getLocale = (short = false) => {
|
|||||||
if (short) {
|
if (short) {
|
||||||
return loc.slice(0, 2);
|
return loc.slice(0, 2);
|
||||||
}
|
}
|
||||||
|
// finally, fallback
|
||||||
|
if (!loc) {
|
||||||
|
loc = "en";
|
||||||
|
}
|
||||||
return loc;
|
return loc;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,4 +123,6 @@ const T = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log("L:", localeOptions);
|
||||||
|
|
||||||
export { localeOptions, getFlagCodeForLocale, getLocale, createIntl, changeLocale, intl, T };
|
export { localeOptions, getFlagCodeForLocale, getLocale, createIntl, changeLocale, intl, T };
|
||||||
|
|||||||
@@ -39,8 +39,10 @@ not be complete by the time you're reading this:
|
|||||||
|
|
||||||
- frontend/src/locale/src/[yourlang].json
|
- frontend/src/locale/src/[yourlang].json
|
||||||
- frontend/src/locale/src/lang-list.json
|
- frontend/src/locale/src/lang-list.json
|
||||||
- frontend/src/locale/src/HelpDoc/*
|
- frontend/src/locale/src/HelpDoc/[yourlang]/*
|
||||||
|
- frontend/src/locale/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
|
||||||
|
|||||||
74
frontend/src/locale/Utils.test.tsx
Normal file
74
frontend/src/locale/Utils.test.tsx
Normal 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("1 Jan 2024, 12:00:00 am");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("format date from unix timestamp number", () => {
|
||||||
|
const value = 1762476112;
|
||||||
|
const text = formatDateTime(value);
|
||||||
|
expect(text).toBe("7 Nov 2025, 12:41:52 am");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("format date from unix timestamp string", () => {
|
||||||
|
const value = "1762476112";
|
||||||
|
const text = formatDateTime(value);
|
||||||
|
expect(text).toBe("7 Nov 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
46
frontend/src/locale/Utils.ts
Normal file
46
frontend/src/locale/Utils.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import {
|
||||||
|
fromUnixTime,
|
||||||
|
type IntlFormatFormatOptions,
|
||||||
|
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, locale = "en-US"): string => {
|
||||||
|
const d = parseDate(value);
|
||||||
|
if (!d) return `${value}`;
|
||||||
|
try {
|
||||||
|
return intlFormat(
|
||||||
|
d,
|
||||||
|
{
|
||||||
|
dateStyle: "medium",
|
||||||
|
timeStyle: "medium",
|
||||||
|
hourCycle: "h12",
|
||||||
|
} as IntlFormatFormatOptions,
|
||||||
|
{ locale },
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return `${value}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { formatDateTime, parseDate, isUnixTimestamp };
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from "./DateTimeFormat";
|
|
||||||
export * from "./IntlProvider";
|
export * from "./IntlProvider";
|
||||||
|
export * from "./Utils";
|
||||||
|
|||||||
@@ -1,215 +0,0 @@
|
|||||||
{
|
|
||||||
"access-list": "Access List",
|
|
||||||
"access-list.access-count": "{count} {count, plural, one {Rule} other {Rules}}",
|
|
||||||
"access-list.auth-count": "{count} {count, plural, one {User} other {Users}}",
|
|
||||||
"access-list.help-rules-last": "When at least 1 rule exists, this deny all rule will be added last",
|
|
||||||
"access-list.help.rules-order": "Note that the allow and deny directives will be applied in the order they are defined.",
|
|
||||||
"access-list.pass-auth": "Pass Auth to Upstream",
|
|
||||||
"access-list.public": "Publicly Accessible",
|
|
||||||
"access-list.public.subtitle": "No basic auth required",
|
|
||||||
"access-list.satisfy-any": "Satisfy Any",
|
|
||||||
"access-list.subtitle": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Rule} other {Rules}} - Created: {date}",
|
|
||||||
"access-lists": "Access Lists",
|
|
||||||
"action.add": "Add",
|
|
||||||
"action.add-location": "Add Location",
|
|
||||||
"action.close": "Close",
|
|
||||||
"action.delete": "Delete",
|
|
||||||
"action.disable": "Disable",
|
|
||||||
"action.download": "Download",
|
|
||||||
"action.edit": "Edit",
|
|
||||||
"action.enable": "Enable",
|
|
||||||
"action.permissions": "Permissions",
|
|
||||||
"action.renew": "Renew",
|
|
||||||
"action.view-details": "View Details",
|
|
||||||
"auditlogs": "Audit Logs",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"certificate": "Certificate",
|
|
||||||
"certificate.custom-certificate": "Certificate",
|
|
||||||
"certificate.custom-certificate-key": "Certificate Key",
|
|
||||||
"certificate.custom-intermediate": "Intermediate Certificate",
|
|
||||||
"certificate.in-use": "In Use",
|
|
||||||
"certificate.none.subtitle": "No certificate assigned",
|
|
||||||
"certificate.none.subtitle.for-http": "This host will not use HTTPS",
|
|
||||||
"certificate.none.title": "None",
|
|
||||||
"certificate.not-in-use": "Not Used",
|
|
||||||
"certificate.renew": "Renew Certificate",
|
|
||||||
"certificates": "Certificates",
|
|
||||||
"certificates.custom": "Custom Certificate",
|
|
||||||
"certificates.custom.warning": "Key files protected with a passphrase are not supported.",
|
|
||||||
"certificates.dns.credentials": "Credentials File Content",
|
|
||||||
"certificates.dns.credentials-note": "This plugin requires a configuration file containing an API token or other credentials for your provider",
|
|
||||||
"certificates.dns.credentials-warning": "This data will be stored as plaintext in the database and in a file!",
|
|
||||||
"certificates.dns.propagation-seconds": "Propagation Seconds",
|
|
||||||
"certificates.dns.propagation-seconds-note": "Leave empty to use the plugins default value. Number of seconds to wait for DNS propagation.",
|
|
||||||
"certificates.dns.provider": "DNS Provider",
|
|
||||||
"certificates.dns.warning": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation.",
|
|
||||||
"certificates.http.reachability-404": "There is a server found at this domain but it does not seem to be Nginx Proxy Manager. Please make sure your domain points to the IP where your NPM instance is running.",
|
|
||||||
"certificates.http.reachability-failed-to-check": "Failed to check the reachability due to a communication error with site24x7.com.",
|
|
||||||
"certificates.http.reachability-not-resolved": "There is no server available at this domain. Please make sure your domain exists and points to the IP where your NPM instance is running and if necessary port 80 is forwarded in your router.",
|
|
||||||
"certificates.http.reachability-ok": "Your server is reachable and creating certificates should be possible.",
|
|
||||||
"certificates.http.reachability-other": "There is a server found at this domain but it returned an unexpected status code {code}. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.",
|
|
||||||
"certificates.http.reachability-wrong-data": "There is a server found at this domain but it returned an unexpected data. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.",
|
|
||||||
"certificates.http.test-results": "Test Results",
|
|
||||||
"certificates.http.warning": "These domains must be already configured to point to this installation.",
|
|
||||||
"certificates.request.subtitle": "with Let's Encrypt",
|
|
||||||
"certificates.request.title": "Request a new Certificate",
|
|
||||||
"column.access": "Access",
|
|
||||||
"column.authorization": "Authorization",
|
|
||||||
"column.authorizations": "Authorizations",
|
|
||||||
"column.custom-locations": "Custom Locations",
|
|
||||||
"column.destination": "Destination",
|
|
||||||
"column.details": "Details",
|
|
||||||
"column.email": "Email",
|
|
||||||
"column.event": "Event",
|
|
||||||
"column.expires": "Expires",
|
|
||||||
"column.http-code": "Access",
|
|
||||||
"column.incoming-port": "Incoming Port",
|
|
||||||
"column.name": "Name",
|
|
||||||
"column.protocol": "Protocol",
|
|
||||||
"column.provider": "Provider",
|
|
||||||
"column.roles": "Roles",
|
|
||||||
"column.rules": "Rules",
|
|
||||||
"column.satisfy": "Satisfy",
|
|
||||||
"column.satisfy-all": "All",
|
|
||||||
"column.satisfy-any": "Any",
|
|
||||||
"column.scheme": "Scheme",
|
|
||||||
"column.source": "Source",
|
|
||||||
"column.ssl": "SSL",
|
|
||||||
"column.status": "Status",
|
|
||||||
"created-on": "Created: {date}",
|
|
||||||
"dashboard": "Dashboard",
|
|
||||||
"dead-host": "404 Host",
|
|
||||||
"dead-hosts": "404 Hosts",
|
|
||||||
"dead-hosts.count": "{count} {count, plural, one {404 Host} other {404 Hosts}}",
|
|
||||||
"disabled": "Disabled",
|
|
||||||
"domain-names": "Domain Names",
|
|
||||||
"domain-names.max": "{count} domain names maximum",
|
|
||||||
"domain-names.placeholder": "Start typing to add domain...",
|
|
||||||
"domain-names.wildcards-not-permitted": "Wildcards not permitted for this type",
|
|
||||||
"domain-names.wildcards-not-supported": "Wildcards not supported for this CA",
|
|
||||||
"domains.force-ssl": "Force SSL",
|
|
||||||
"domains.hsts-enabled": "HSTS Enabled",
|
|
||||||
"domains.hsts-subdomains": "HSTS Sub-domains",
|
|
||||||
"domains.http2-support": "HTTP/2 Support",
|
|
||||||
"domains.use-dns": "Use DNS Challenge",
|
|
||||||
"email-address": "Email address",
|
|
||||||
"empty-search": "No results found",
|
|
||||||
"empty-subtitle": "Why don't you create one?",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"error.access.at-least-one": "Either one Authorization or one Access Rule is required",
|
|
||||||
"error.access.duplicate-usernames": "Authorization Usernames must be unique",
|
|
||||||
"error.invalid-auth": "Invalid email or password",
|
|
||||||
"error.invalid-domain": "Invalid domain: {domain}",
|
|
||||||
"error.invalid-email": "Invalid email address",
|
|
||||||
"error.max-character-length": "Maximum length is {max} character{max, plural, one {} other {s}}",
|
|
||||||
"error.max-domains": "Too many domains, max is {max}",
|
|
||||||
"error.maximum": "Maximum is {max}",
|
|
||||||
"error.min-character-length": "Minimum length is {min} character{min, plural, one {} other {s}}",
|
|
||||||
"error.minimum": "Minimum is {min}",
|
|
||||||
"error.passwords-must-match": "Passwords must match",
|
|
||||||
"error.required": "This is required",
|
|
||||||
"expires.on": "Expires: {date}",
|
|
||||||
"footer.github-fork": "Fork me on Github",
|
|
||||||
"host.flags.block-exploits": "Block Common Exploits",
|
|
||||||
"host.flags.cache-assets": "Cache Assets",
|
|
||||||
"host.flags.preserve-path": "Preserve Path",
|
|
||||||
"host.flags.protocols": "Protocols",
|
|
||||||
"host.flags.websockets-upgrade": "Websockets Support",
|
|
||||||
"host.forward-port": "Forward Port",
|
|
||||||
"host.forward-scheme": "Scheme",
|
|
||||||
"hosts": "Hosts",
|
|
||||||
"http-only": "HTTP Only",
|
|
||||||
"lets-encrypt": "Let's Encrypt",
|
|
||||||
"lets-encrypt-via-dns": "Let's Encrypt via DNS",
|
|
||||||
"lets-encrypt-via-http": "Let's Encrypt via HTTP",
|
|
||||||
"loading": "Loading…",
|
|
||||||
"login.title": "Login to your account",
|
|
||||||
"nginx-config.label": "Custom Nginx Configuration",
|
|
||||||
"nginx-config.placeholder": "# Enter your custom Nginx configuration here at your own risk!",
|
|
||||||
"no-permission-error": "You do not have access to view this.",
|
|
||||||
"notfound.action": "Take me home",
|
|
||||||
"notfound.content": "We are sorry but the page you are looking for was not found",
|
|
||||||
"notfound.title": "Oops… You just found an error page",
|
|
||||||
"notification.error": "Error",
|
|
||||||
"notification.object-deleted": "{object} has been deleted",
|
|
||||||
"notification.object-disabled": "{object} has been disabled",
|
|
||||||
"notification.object-enabled": "{object} has been enabled",
|
|
||||||
"notification.object-renewed": "{object} has been renewed",
|
|
||||||
"notification.object-saved": "{object} has been saved",
|
|
||||||
"notification.success": "Success",
|
|
||||||
"object.actions-title": "{object} #{id}",
|
|
||||||
"object.add": "Add {object}",
|
|
||||||
"object.delete": "Delete {object}",
|
|
||||||
"object.delete.content": "Are you sure you want to delete this {object}?",
|
|
||||||
"object.edit": "Edit {object}",
|
|
||||||
"object.empty": "There are no {objects}",
|
|
||||||
"object.event.created": "Created {object}",
|
|
||||||
"object.event.deleted": "Deleted {object}",
|
|
||||||
"object.event.disabled": "Disabled {object}",
|
|
||||||
"object.event.enabled": "Enabled {object}",
|
|
||||||
"object.event.renewed": "Renewed {object}",
|
|
||||||
"object.event.updated": "Updated {object}",
|
|
||||||
"offline": "Offline",
|
|
||||||
"online": "Online",
|
|
||||||
"options": "Options",
|
|
||||||
"password": "Password",
|
|
||||||
"password.generate": "Generate random password",
|
|
||||||
"password.hide": "Hide Password",
|
|
||||||
"password.show": "Show Password",
|
|
||||||
"permissions.hidden": "Hidden",
|
|
||||||
"permissions.manage": "Manage",
|
|
||||||
"permissions.view": "View Only",
|
|
||||||
"permissions.visibility.all": "All Items",
|
|
||||||
"permissions.visibility.title": "Item Visibility",
|
|
||||||
"permissions.visibility.user": "Created Items Only",
|
|
||||||
"proxy-host": "Proxy Host",
|
|
||||||
"proxy-host.forward-host": "Forward Hostname / IP",
|
|
||||||
"proxy-hosts": "Proxy Hosts",
|
|
||||||
"proxy-hosts.count": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}",
|
|
||||||
"public": "Public",
|
|
||||||
"redirection-host": "Redirection Host",
|
|
||||||
"redirection-host.forward-domain": "Forward Domain",
|
|
||||||
"redirection-hosts": "Redirection Hosts",
|
|
||||||
"redirection-hosts.count": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}",
|
|
||||||
"role.admin": "Administrator",
|
|
||||||
"role.standard-user": "Standard User",
|
|
||||||
"save": "Save",
|
|
||||||
"setting": "Setting",
|
|
||||||
"settings": "Settings",
|
|
||||||
"settings.default-site": "Default Site",
|
|
||||||
"settings.default-site.404": "404 Page",
|
|
||||||
"settings.default-site.444": "No Response (444)",
|
|
||||||
"settings.default-site.congratulations": "Congratulations Page",
|
|
||||||
"settings.default-site.description": "What to show when Nginx is hit with an unknown Host",
|
|
||||||
"settings.default-site.html": "Custom HTML",
|
|
||||||
"settings.default-site.html.placeholder": "<!-- Enter your custom HTML content here -->",
|
|
||||||
"settings.default-site.redirect": "Redirect",
|
|
||||||
"setup.preamble": "Get started by creating your admin account.",
|
|
||||||
"setup.title": "Welcome!",
|
|
||||||
"sign-in": "Sign in",
|
|
||||||
"ssl-certificate": "SSL Certificate",
|
|
||||||
"stream": "Stream",
|
|
||||||
"stream.forward-host": "Forward Host",
|
|
||||||
"stream.incoming-port": "Incoming Port",
|
|
||||||
"streams": "Streams",
|
|
||||||
"streams.count": "{count} {count, plural, one {Stream} other {Streams}}",
|
|
||||||
"streams.tcp": "TCP",
|
|
||||||
"streams.udp": "UDP",
|
|
||||||
"test": "Test",
|
|
||||||
"user": "User",
|
|
||||||
"user.change-password": "Change Password",
|
|
||||||
"user.confirm-password": "Confirm Password",
|
|
||||||
"user.current-password": "Current Password",
|
|
||||||
"user.edit-profile": "Edit Profile",
|
|
||||||
"user.full-name": "Full Name",
|
|
||||||
"user.login-as": "Sign in as {name}",
|
|
||||||
"user.logout": "Logout",
|
|
||||||
"user.new-password": "New Password",
|
|
||||||
"user.nickname": "Nickname",
|
|
||||||
"user.set-password": "Set Password",
|
|
||||||
"user.set-permissions": "Set Permissions for {name}",
|
|
||||||
"user.switch-dark": "Switch to Dark mode",
|
|
||||||
"user.switch-light": "Switch to Light mode",
|
|
||||||
"username": "Username",
|
|
||||||
"users": "Users"
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"locale-en-US": "English"
|
|
||||||
}
|
|
||||||
@@ -31,6 +31,6 @@ for file in *.json; do
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Sorting $file"
|
echo "Sorting $file"
|
||||||
jq --tab --sort-keys . "$file" | sponge "$file"
|
tmp=$(mktemp) && jq --tab --sort-keys . "$file" > "$tmp" && mv "$tmp" "$file"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
7
frontend/src/locale/src/HelpDoc/bg/AccessLists.md
Normal file
7
frontend/src/locale/src/HelpDoc/bg/AccessLists.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
## Какво представлява Списъкът за достъп?
|
||||||
|
|
||||||
|
Списъците за достъп предоставят черен или бял списък от конкретни клиентски IP адреси, както и удостоверяване за Прокси хостове чрез базова HTTP автентикация.
|
||||||
|
|
||||||
|
Можете да конфигурирате множество клиентски правила, потребителски имена и пароли в един Списък за достъп и след това да го приложите към един или повече _Прокси хостове_.
|
||||||
|
|
||||||
|
Това е най-полезно при препращани уеб услуги, които нямат вградени механизми за удостоверяване, или когато искате да защитите достъпа от неизвестни клиенти.
|
||||||
21
frontend/src/locale/src/HelpDoc/bg/Certificates.md
Normal file
21
frontend/src/locale/src/HelpDoc/bg/Certificates.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
## Помощ за сертификати
|
||||||
|
|
||||||
|
### HTTP сертификат
|
||||||
|
|
||||||
|
HTTP валидираният сертификат означава, че сървърите на Let’s Encrypt ще се опитат да достигнат вашите домейни по HTTP (не по HTTPS!) и ако успеят, ще издадат сертификата.
|
||||||
|
|
||||||
|
За този метод трябва да имате създаден _Прокси хост_ за вашия/вашите домейни, който да е достъпен по HTTP и да сочи към тази Nginx инсталация. След като бъде издаден сертификат, можете да промените _Прокси хоста_ така, че да използва сертификата и за HTTPS връзки. Въпреки това, _Прокси хостът_ трябва да остане конфигуриран за достъп по HTTP, за да може сертификатът да се подновява.
|
||||||
|
|
||||||
|
Този процес _не_ поддържа wildcard домейни.
|
||||||
|
|
||||||
|
### DNS сертификат
|
||||||
|
|
||||||
|
DNS валидираният сертификат изисква използването на DNS Provider плъгин. Този DNS Provider ще бъде използван за временно създаване на записи във вашия домейн, след което Let’s Encrypt ще ги провери, за да се увери, че сте собственикът, и при успех ще издаде сертификата.
|
||||||
|
|
||||||
|
Не е необходимо да имате _Прокси хост_, създаден предварително, за да заявите този тип сертификат. Нито е нужно вашият _Прокси хост_ да бъде конфигуриран за достъп по HTTP.
|
||||||
|
|
||||||
|
Този процес _поддържа_ wildcard домейни.
|
||||||
|
|
||||||
|
### Персонализиран сертификат
|
||||||
|
|
||||||
|
Използвайте тази опция, за да качите собствен SSL сертификат, предоставен от ваша сертификатна агенция.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user