Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
a31b1d878f Bump mdast-util-to-hast from 13.2.0 to 13.2.1 in /frontend
Bumps [mdast-util-to-hast](https://github.com/syntax-tree/mdast-util-to-hast) from 13.2.0 to 13.2.1.
- [Release notes](https://github.com/syntax-tree/mdast-util-to-hast/releases)
- [Commits](https://github.com/syntax-tree/mdast-util-to-hast/compare/13.2.0...13.2.1)

---
updated-dependencies:
- dependency-name: mdast-util-to-hast
  dependency-version: 13.2.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-13 13:39:59 +00:00
150 changed files with 3587 additions and 10716 deletions

104
.github/dependabot.yml vendored
View File

@@ -1,104 +0,0 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/backend"
schedule:
interval: "weekly"
groups:
dev-patch-updates:
dependency-type: "development"
update-types:
- "patch"
dev-minor-updates:
dependency-type: "development"
update-types:
- "minor"
prod-patch-updates:
dependency-type: "production"
update-types:
- "patch"
prod-minor-updates:
dependency-type: "production"
update-types:
- "minor"
- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "weekly"
groups:
dev-patch-updates:
dependency-type: "development"
update-types:
- "patch"
dev-minor-updates:
dependency-type: "development"
update-types:
- "minor"
prod-patch-updates:
dependency-type: "production"
update-types:
- "patch"
prod-minor-updates:
dependency-type: "production"
update-types:
- "minor"
- package-ecosystem: "npm"
directory: "/docs"
schedule:
interval: "weekly"
groups:
dev-patch-updates:
dependency-type: "development"
update-types:
- "patch"
dev-minor-updates:
dependency-type: "development"
update-types:
- "minor"
prod-patch-updates:
dependency-type: "production"
update-types:
- "patch"
prod-minor-updates:
dependency-type: "production"
update-types:
- "minor"
- package-ecosystem: "npm"
directory: "/test"
schedule:
interval: "weekly"
groups:
dev-patch-updates:
dependency-type: "development"
update-types:
- "patch"
dev-minor-updates:
dependency-type: "development"
update-types:
- "minor"
prod-patch-updates:
dependency-type: "production"
update-types:
- "patch"
prod-minor-updates:
dependency-type: "production"
update-types:
- "minor"
- package-ecosystem: "docker"
directory: "/docker"
schedule:
interval: "weekly"
groups:
updates:
update-types:
- "patch"
- "minor"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -8,7 +8,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
- uses: actions/stale@v9
with:
stale-issue-label: 'stale'
stale-pr-label: 'stale'

View File

@@ -1 +1 @@
2.14.0
2.13.5

View File

@@ -1,7 +1,7 @@
<p align="center">
<img src="https://nginxproxymanager.com/github.png">
<br><br>
<img src="https://img.shields.io/badge/version-2.14.0-green.svg?style=for-the-badge">
<img src="https://img.shields.io/badge/version-2.13.5-green.svg?style=for-the-badge">
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager">
<img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge">
</a>
@@ -36,10 +36,6 @@ so that the barrier for entry here is low.
- Advanced Nginx configuration available for super users
- User management, permissions and audit log
::: warning
`armv7` is no longer supported in version 2.14+. This is due to Nodejs dropping support for armhf. Please
use the `2.13.7` image tag if this applies to you.
:::
## Hosting your home network
@@ -47,15 +43,16 @@ I won't go in to too much detail here but here are the basics for someone new to
1. Your home router will have a Port Forwarding section somewhere. Log in and find it
2. Add port forwarding for port 80 and 443 to the server hosting this project
3. Configure your domain name details to point to your home, either with a static ip or a service like
- DuckDNS
- [Amazon Route53](https://github.com/jc21/route53-ddns)
- [Cloudflare](https://github.com/jc21/cloudflare-ddns)
3. Configure your domain name details to point to your home, either with a static ip or a service like DuckDNS or [Amazon Route53](https://github.com/jc21/route53-ddns)
4. Use the Nginx Proxy Manager as your gateway to forward to your other web based services
## Quick Setup
1. [Install Docker](https://docs.docker.com/install/)
1. Install Docker and Docker-Compose
- [Docker Install documentation](https://docs.docker.com/install/)
- [Docker-Compose Install documentation](https://docs.docker.com/compose/install/)
2. Create a docker-compose.yml file similar to this:
```yml

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.3/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.2/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",

View File

@@ -23,14 +23,6 @@
"credentials": "dns_aliyun_access_key = 12345678\ndns_aliyun_access_key_secret = 1234567890abcdef1234567890abcdef",
"full_plugin_name": "dns-aliyun"
},
"arvan": {
"name": "ArvanCloud",
"package_name": "certbot-dns-arvan",
"version": ">=0.1.0",
"dependencies": "",
"credentials": "dns_arvan_key = Apikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"full_plugin_name": "dns-arvan"
},
"azure": {
"name": "Azure",
"package_name": "certbot-dns-azure",
@@ -82,7 +74,7 @@
"cloudns": {
"name": "ClouDNS",
"package_name": "certbot-dns-cloudns",
"version": "~=0.7.0",
"version": "~=0.6.0",
"dependencies": "",
"credentials": "# Target user ID (see https://www.cloudns.net/api-settings/)\n\tdns_cloudns_auth_id=1234\n\t# Alternatively, one of the following two options can be set:\n\t# dns_cloudns_sub_auth_id=1234\n\t# dns_cloudns_sub_auth_user=foobar\n\n\t# API password\n\tdns_cloudns_auth_password=password1",
"full_plugin_name": "dns-cloudns"

View File

@@ -2,7 +2,7 @@
"database": {
"engine": "knex-native",
"knex": {
"client": "better-sqlite3",
"client": "sqlite3",
"connection": {
"filename": "/app/config/mydb.sqlite"
},

View File

@@ -1,9 +1,9 @@
import crypto from "node:crypto";
import bcrypt from "bcrypt";
import { createGuardrails, generateSecret, generateURI, verify } from "otplib";
import errs from "../lib/error.js";
import crypto from "node:crypto";
import { authenticator } from "otplib";
import authModel from "../models/auth.js";
import internalUser from "./user.js";
import userModel from "../models/user.js";
import errs from "../lib/error.js";
const APP_NAME = "Nginx Proxy Manager";
const BACKUP_CODE_COUNT = 8;
@@ -26,98 +26,134 @@ const generateBackupCodes = async () => {
return { plain, hashed };
};
const internal2fa = {
export default {
/**
* Generate a new TOTP secret
* @returns {string}
*/
generateSecret: () => {
return authenticator.generateSecret();
},
/**
* Generate otpauth URL for QR code
* @param {string} email
* @param {string} secret
* @returns {string}
*/
generateOTPAuthURL: (email, secret) => {
return authenticator.keyuri(email, APP_NAME, secret);
},
/**
* Verify a TOTP code
* @param {string} secret
* @param {string} code
* @returns {boolean}
*/
verifyCode: (secret, code) => {
try {
return authenticator.verify({ token: code, secret });
} catch {
return false;
}
},
/**
* 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;
const auth = await authModel
.query()
.where("user_id", userId)
.where("type", "password")
.first();
if (!auth || !auth.meta) {
return false;
}
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}>}
* @param {number} userId
* @returns {Promise<{enabled: boolean, backupCodesRemaining: 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;
getStatus: async (userId) => {
const auth = await authModel
.query()
.where("user_id", userId)
.where("type", "password")
.first();
if (enabled) {
const backupCodes = auth.meta.backup_codes || [];
backup_codes_remaining = backupCodes.length;
if (!auth || !auth.meta || !auth.meta.totp_enabled) {
return { enabled: false, backupCodesRemaining: 0 };
}
const backupCodes = auth.meta.backup_codes || [];
return {
enabled,
backup_codes_remaining,
enabled: true,
backupCodesRemaining: backupCodes.length,
};
},
/**
* Start 2FA setup - store pending secret
*
* @param {Access} access
* @param {number} userId
* @returns {Promise<{secret: string, otpauth_url: string}>}
* @param {number} userId
* @returns {Promise<{secret: string, otpauthUrl: string}>}
*/
startSetup: async (access, userId) => {
await access.can("users:password", userId);
const user = await internalUser.get(access, { id: userId });
const secret = generateSecret();
const otpauth_url = generateURI({
issuer: APP_NAME,
label: user.email,
secret: secret,
});
const auth = await internal2fa.getUserPasswordAuth(userId);
startSetup: async (userId) => {
const user = await userModel.query().where("id", userId).first();
if (!user) {
throw new errs.ItemNotFoundError("User not found");
}
// 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 secret = authenticator.generateSecret();
const otpauthUrl = authenticator.keyuri(user.email, APP_NAME, secret);
const auth = await authModel
.query()
.where("user_id", userId)
.where("type", "password")
.first();
if (!auth) {
throw new errs.ItemNotFoundError("Auth record not found");
}
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 });
await authModel.query().where("id", auth.id).patch({ meta });
return { secret, otpauth_url };
return { secret, otpauthUrl };
},
/**
* Enable 2FA after verifying code
*
* @param {Access} access
* @param {number} userId
* @param {string} code
* @returns {Promise<{backup_codes: string[]}>}
* @param {number} userId
* @param {string} code
* @returns {Promise<{backupCodes: 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;
enable: async (userId, code) => {
const auth = await authModel
.query()
.where("user_id", userId)
.where("type", "password")
.first();
if (!secret) {
if (!auth || !auth.meta || !auth.meta.totp_pending_secret) {
throw new errs.ValidationError("No pending 2FA setup found");
}
const result = await verify({ token: code, secret });
if (!result.valid) {
const secret = auth.meta.totp_pending_secret;
const valid = authenticator.verify({ token: code, secret });
if (!valid) {
throw new errs.ValidationError("Invalid verification code");
}
@@ -132,44 +168,35 @@ const internal2fa = {
};
delete meta.totp_pending_secret;
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
await authModel.query().where("id", auth.id).patch({ meta });
return { backup_codes: plain };
return { backupCodes: plain };
},
/**
* Disable 2FA
*
* @param {Access} access
* @param {number} userId
* @param {string} code
* @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);
disable: async (userId, code) => {
const auth = await authModel
.query()
.where("user_id", userId)
.where("type", "password")
.first();
const enabled = auth?.meta?.totp_enabled === true;
if (!enabled) {
if (!auth || !auth.meta || !auth.meta.totp_enabled) {
throw new errs.ValidationError("2FA is not enabled");
}
const result = await verify({
token: code,
secret: auth.meta.totp_secret,
guardrails: createGuardrails({
MIN_SECRET_BYTES: 10,
}),
});
const valid = authenticator.verify({
token: code,
secret: auth.meta.totp_secret,
});
if (!result.valid) {
throw new errs.AuthError("Invalid verification code");
if (!valid) {
throw new errs.ValidationError("Invalid verification code");
}
const meta = { ...auth.meta };
@@ -178,64 +205,46 @@ const internal2fa = {
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 });
await authModel.query().where("id", auth.id).patch({ meta });
},
/**
* Verify 2FA code for login
*
* @param {number} userId
* @param {string} token
* @param {number} userId
* @param {string} code
* @returns {Promise<boolean>}
*/
verifyForLogin: async (userId, token) => {
const auth = await internal2fa.getUserPasswordAuth(userId);
const secret = auth?.meta?.totp_secret || false;
verifyForLogin: async (userId, code) => {
const auth = await authModel
.query()
.where("user_id", userId)
.where("type", "password")
.first();
if (!secret) {
if (!auth || !auth.meta || !auth.meta.totp_secret) {
return false;
}
// Try TOTP code first, if it's 6 chars. it will throw errors if it's not 6 chars
// and the backup codes are 8 chars.
if (token.length === 6) {
const result = await verify({
token,
secret,
// These guardrails lower the minimum length requirement for secrets.
// In v12 of otplib the default minimum length is 10 and in v13 it is 16.
// Since there are 2fa secrets in the wild generated with v12 we need to allow shorter secrets
// so people won't be locked out when upgrading.
guardrails: createGuardrails({
MIN_SECRET_BYTES: 10,
}),
});
// Try TOTP code first
const valid = authenticator.verify({
token: code,
secret: auth.meta.totp_secret,
});
if (result.valid) {
return true;
}
if (valid) {
return true;
}
// Try backup codes
const backupCodes = auth?.meta?.backup_codes || [];
const backupCodes = auth.meta.backup_codes || [];
for (let i = 0; i < backupCodes.length; i++) {
const match = await bcrypt.compare(token.toUpperCase(), backupCodes[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 });
await authModel.query().where("id", auth.id).patch({ meta });
return true;
}
}
@@ -245,61 +254,35 @@ const internal2fa = {
/**
* Regenerate backup codes
*
* @param {Access} access
* @param {number} userId
* @param {string} token
* @returns {Promise<{backup_codes: string[]}>}
* @param {number} userId
* @param {string} code
* @returns {Promise<{backupCodes: 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;
regenerateBackupCodes: async (userId, code) => {
const auth = await authModel
.query()
.where("user_id", userId)
.where("type", "password")
.first();
if (!enabled) {
if (!auth || !auth.meta || !auth.meta.totp_enabled) {
throw new errs.ValidationError("2FA is not enabled");
}
if (!secret) {
throw new errs.ValidationError("No 2FA secret found");
}
const result = await verify({
token,
secret,
const valid = authenticator.verify({
token: code,
secret: auth.meta.totp_secret,
});
if (!result.valid) {
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 });
await authModel.query().where("id", auth.id).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;
return { backupCodes: plain };
},
};
export default internal2fa;

View File

@@ -630,7 +630,7 @@ const internalCertificate = {
* @param {String} privateKey This is the entire key contents as a string
*/
checkPrivateKey: async (privateKey) => {
const filepath = await tempWrite(privateKey);
const filepath = await tempWrite(privateKey, "/tmp");
const failTimeout = setTimeout(() => {
throw new error.ValidationError(
"Result Validation Error: Validation timed out. This could be due to the key being passphrase-protected.",
@@ -660,8 +660,8 @@ const internalCertificate = {
* @param {Boolean} [throwExpired] Throw when the certificate is out of date
*/
getCertificateInfo: async (certificate, throwExpired) => {
const filepath = await tempWrite(certificate);
try {
const filepath = await tempWrite(certificate, "/tmp");
const certData = await internalCertificate.getCertificateInfoFromFile(filepath, throwExpired);
fs.unlinkSync(filepath);
return certData;

View File

@@ -2,13 +2,10 @@ import fs from "node:fs";
import NodeRSA from "node-rsa";
import { global as logger } from "../logger.js";
const keysFile = '/data/keys.json';
const mysqlEngine = 'mysql2';
const postgresEngine = 'pg';
const sqliteClientName = 'better-sqlite3';
// Not used for new setups anymore but may exist in legacy setups
const legacySqliteClientName = 'sqlite3';
const keysFile = '/data/keys.json';
const mysqlEngine = 'mysql2';
const postgresEngine = 'pg';
const sqliteClientName = 'sqlite3';
let instance = null;
@@ -87,7 +84,6 @@ const configure = () => {
}
const envSqliteFile = process.env.DB_SQLITE_FILE || "/data/database.sqlite";
logger.info(`Using Sqlite: ${envSqliteFile}`);
instance = {
database: {
@@ -187,7 +183,7 @@ const configGet = (key) => {
*/
const isSqlite = () => {
instance === null && configure();
return instance.database.knex && [sqliteClientName, legacySqliteClientName].includes(instance.database.knex.client);
return instance.database.knex && instance.database.knex.client === sqliteClientName;
};
/**

View File

@@ -1,43 +0,0 @@
import { migrate as logger } from "../logger.js";
const migrateName = "trust_forwarded_proto";
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = function (knex) {
logger.info(`[${migrateName}] Migrating Up...`);
return knex.schema
.alterTable('proxy_host', (table) => {
table.tinyint('trust_forwarded_proto').notNullable().defaultTo(0);
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = function (knex) {
logger.info(`[${migrateName}] Migrating Down...`);
return knex.schema
.alterTable('proxy_host', (table) => {
table.dropColumn('trust_forwarded_proto');
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
});
};
export { up, down };

View File

@@ -21,7 +21,6 @@ const boolFields = [
"enabled",
"hsts_enabled",
"hsts_subdomains",
"trust_forwarded_proto",
];
class ProxyHost extends Model {

View File

@@ -9,42 +9,40 @@
"scripts": {
"lint": "biome lint",
"prettier": "biome format --write .",
"validate-schema": "node validate-schema.js",
"regenerate-config": "node scripts/regenerate-config"
"validate-schema": "node validate-schema.js"
},
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^15.2.2",
"ajv": "^8.18.0",
"archiver": "^7.0.1",
"@apidevtools/json-schema-ref-parser": "^11.7.0",
"ajv": "^8.17.1",
"archiver": "^5.3.0",
"batchflow": "^0.4.0",
"bcrypt": "^6.0.0",
"better-sqlite3": "^12.6.2",
"body-parser": "^2.2.2",
"compression": "^1.8.1",
"express": "^5.2.1",
"bcrypt": "^5.0.0",
"body-parser": "^1.20.3",
"compression": "^1.7.4",
"express": "^4.22.0",
"express-fileupload": "^1.5.2",
"gravatar": "^1.8.2",
"jsonwebtoken": "^9.0.3",
"knex": "3.1.0",
"liquidjs": "10.24.0",
"lodash": "^4.17.23",
"jsonwebtoken": "^9.0.2",
"knex": "2.4.2",
"liquidjs": "10.6.1",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"mysql2": "^3.17.5",
"mysql2": "^3.15.3",
"node-rsa": "^1.1.1",
"objection": "3.1.5",
"otplib": "^13.3.0",
"objection": "3.0.1",
"otplib": "^12.0.1",
"path": "^0.12.7",
"pg": "^8.18.0",
"pg": "^8.16.3",
"proxy-agent": "^6.5.0",
"signale": "1.4.0",
"sqlite3": "^5.1.7",
"temp-write": "^6.0.1"
"temp-write": "^4.0.0"
},
"devDependencies": {
"@apidevtools/swagger-parser": "^12.1.0",
"@biomejs/biome": "^2.4.3",
"chalk": "5.6.2",
"nodemon": "^3.1.14"
"@apidevtools/swagger-parser": "^10.1.0",
"@biomejs/biome": "^2.3.2",
"chalk": "4.1.2",
"nodemon": "^2.0.2"
},
"signale": {
"displayDate": true,

View File

@@ -66,7 +66,16 @@ router
*/
.post(async (req, res, next) => {
try {
const { challenge_token, code } = await apiValidator(getValidationSchema("/tokens/2fa", "post"), req.body);
const { challenge_token, code } = req.body;
if (!challenge_token || !code) {
return res.status(400).json({
error: {
message: "Missing challenge_token or code",
},
});
}
const result = await internalToken.verify2FA(challenge_token, code);
res.status(200).send(result);
} catch (err) {

View File

@@ -339,21 +339,6 @@ router
.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
*
@@ -361,7 +346,15 @@ router
*/
.get(async (req, res, next) => {
try {
const status = await internal2FA.getStatus(res.locals.access, req.params.user_id);
const userId = Number.parseInt(req.params.user_id, 10);
const access = res.locals.access;
// Users can only view their own 2FA status
if (access.token.getUserId() !== userId && !access.token.hasScope("admin")) {
throw new errs.PermissionError("Cannot view 2FA status for other users");
}
const status = await internal2FA.getStatus(userId);
res.status(200).send(status);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
@@ -370,18 +363,63 @@ router
})
/**
* DELETE /api/users/123/2fa?code=XXXXXX
* DELETE /api/users/123/2fa
*
* 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");
const userId = Number.parseInt(req.params.user_id, 10);
const access = res.locals.access;
// Users can only disable their own 2FA
if (access.token.getUserId() !== userId && !access.token.hasScope("admin")) {
throw new errs.PermissionError("Cannot disable 2FA for other users");
}
await internal2FA.disable(res.locals.access, req.params.user_id, code);
res.status(200).send(true);
const { code } = req.body;
if (!code) {
throw new errs.ValidationError("Verification code is required");
}
await internal2FA.disable(userId, code);
res.status(200).send({ success: true });
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* User 2FA setup
*
* /api/users/123/2fa/setup
*/
router
.route("/:user_id/2fa/setup")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* POST /api/users/123/2fa/setup
*
* Start 2FA setup, returns QR code URL
*/
.post(async (req, res, next) => {
try {
const userId = Number.parseInt(req.params.user_id, 10);
const access = res.locals.access;
// Users can only setup their own 2FA
if (access.token.getUserId() !== userId) {
throw new errs.PermissionError("Cannot setup 2FA for other users");
}
const result = await internal2FA.startSetup(userId);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
@@ -402,17 +440,26 @@ router
.all(userIdFromMe)
/**
* POST /api/users/123/2fa/enable
* PUT /api/users/123/2fa/enable
*
* Verify code and enable 2FA
*/
.post(async (req, res, next) => {
.put(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);
const userId = Number.parseInt(req.params.user_id, 10);
const access = res.locals.access;
// Users can only enable their own 2FA
if (access.token.getUserId() !== userId) {
throw new errs.PermissionError("Cannot enable 2FA for other users");
}
const { code } = req.body;
if (!code) {
throw new errs.ValidationError("Verification code is required");
}
const result = await internal2FA.enable(userId, code);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
@@ -440,11 +487,20 @@ router
*/
.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);
const userId = Number.parseInt(req.params.user_id, 10);
const access = res.locals.access;
// Users can only regenerate their own backup codes
if (access.token.getUserId() !== userId) {
throw new errs.PermissionError("Cannot regenerate backup codes for other users");
}
const { code } = req.body;
if (!code) {
throw new errs.ValidationError("Verification code is required");
}
const result = await internal2FA.regenerateBackupCodes(userId, code);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);

View File

@@ -22,8 +22,7 @@
"enabled",
"locations",
"hsts_enabled",
"hsts_subdomains",
"trust_forwarded_proto"
"hsts_subdomains"
],
"properties": {
"id": {
@@ -142,11 +141,6 @@
"hsts_subdomains": {
"$ref": "../common.json#/properties/hsts_subdomains"
},
"trust_forwarded_proto":{
"type": "boolean",
"description": "Trust the forwarded headers",
"example": false
},
"certificate": {
"oneOf": [
{

View File

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

View File

@@ -58,8 +58,7 @@
"enabled": true,
"locations": [],
"hsts_enabled": false,
"hsts_subdomains": false,
"trust_forwarded_proto": false
"hsts_subdomains": false
}
]
}

View File

@@ -56,7 +56,6 @@
"locations": [],
"hsts_enabled": false,
"hsts_subdomains": false,
"trust_forwarded_proto": false,
"owner": {
"id": 1,
"created_on": "2025-10-28T00:50:24.000Z",

View File

@@ -56,9 +56,6 @@
"hsts_subdomains": {
"$ref": "../../../../components/proxy-host-object.json#/properties/hsts_subdomains"
},
"trust_forwarded_proto": {
"$ref": "../../../../components/proxy-host-object.json#/properties/trust_forwarded_proto"
},
"http2_support": {
"$ref": "../../../../components/proxy-host-object.json#/properties/http2_support"
},
@@ -125,7 +122,6 @@
"locations": [],
"hsts_enabled": false,
"hsts_subdomains": false,
"trust_forwarded_proto": false,
"owner": {
"id": 1,
"created_on": "2025-10-28T00:50:24.000Z",

View File

@@ -48,9 +48,6 @@
"hsts_subdomains": {
"$ref": "../../../components/proxy-host-object.json#/properties/hsts_subdomains"
},
"trust_forwarded_proto": {
"$ref": "../../../components/proxy-host-object.json#/properties/trust_forwarded_proto"
},
"http2_support": {
"$ref": "../../../components/proxy-host-object.json#/properties/http2_support"
},
@@ -122,7 +119,6 @@
"locations": [],
"hsts_enabled": false,
"hsts_subdomains": false,
"trust_forwarded_proto": false,
"certificate": null,
"owner": {
"id": 1,

View File

@@ -1,55 +0,0 @@
{
"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": 8,
"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"
}
}
}

View File

@@ -50,14 +50,7 @@
}
},
"schema": {
"oneOf": [
{
"$ref": "../../components/token-object.json"
},
{
"$ref": "../../components/token-challenge.json"
}
]
"$ref": "../../components/token-object.json"
}
}
},

View File

@@ -1,92 +0,0 @@
{
"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": "Verification Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"code": {
"minLength": 6,
"maxLength": 8,
"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"
}
}
}

View File

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

View File

@@ -1,92 +0,0 @@
{
"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": "Verification Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"code": {
"minLength": 6,
"maxLength": 8,
"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"
}
}
}

View File

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

View File

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

View File

@@ -293,11 +293,6 @@
"$ref": "./paths/tokens/post.json"
}
},
"/tokens/2fa": {
"post": {
"$ref": "./paths/tokens/2fa/post.json"
}
},
"/version/check": {
"get": {
"$ref": "./paths/version/check/get.json"
@@ -322,27 +317,6 @@
"$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": {
"put": {
"$ref": "./paths/users/userID/auth/put.json"

View File

@@ -1,104 +0,0 @@
#!/usr/bin/env node
import * as process from "node:process"; // Use the node: protocol for built-ins
import internalNginx from "../internal/nginx.js";
import { castJsonIfNeed } from "../lib/helpers.js";
import { global as logger } from "../logger.js";
import deadHostModel from "../models/dead_host.js";
import proxyHostModel from "../models/proxy_host.js";
import redirectionHostModel from "../models/redirection_host.js";
import streamModel from "../models/stream.js";
const args = process.argv.slice(2);
const UNATTENDED = args.includes("-y") || args.includes("--yes");
if (args.includes("--help")) {
console.log("Usage: ./regenerate-config [--help] [-y|--yes]");
}
// ask for the user to confirm the action if not in unattended mode
if (!UNATTENDED) {
const readline = await import("node:readline");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const question = (query) =>
new Promise((resolve) => rl.question(query, resolve));
const answer = await question(
"This will iterate over all Hosts and regnerate their Nginx configs.\n\nAre you sure you want to proceed? (y/N) ",
);
rl.close();
if (answer.toLowerCase() !== "y") {
console.log("Aborting.");
process.exit(0);
}
}
// Let's do it.
// Proxy hosts
const proxyRows = await proxyHostModel
.query()
.where("is_deleted", 0)
.andWhere("enabled", 1)
.groupBy("id")
.allowGraph("[owner,access_list,certificate]")
.orderBy(castJsonIfNeed("domain_names"), "ASC");
for (const row of proxyRows) {
logger.info(
`Regenerating config for Proxy Host #${row.id}: ${row.domain_names.join(", ")}`,
);
await internalNginx.configure(proxyHostModel, "proxy_host", row);
}
// Redirection hosts
const redirectionRows = await redirectionHostModel
.query()
.where("is_deleted", 0)
.andWhere("enabled", 1)
.groupBy("id")
.allowGraph("[owner,access_list,certificate]")
.orderBy(castJsonIfNeed("domain_names"), "ASC");
for (const row of redirectionRows) {
logger.info(
`Regenerating config for Redirection Host #${row.id}: ${row.domain_names.join(", ")}`,
);
await internalNginx.configure(redirectionHostModel, "redirection_host", row);
}
// 404 hosts
const deadRows = await deadHostModel
.query()
.where("is_deleted", 0)
.andWhere("enabled", 1)
.groupBy("id")
.allowGraph("[owner,access_list,certificate]")
.orderBy(castJsonIfNeed("domain_names"), "ASC");
for (const row of deadRows) {
logger.info(
`Regenerating config for 404 Host #${row.id}: ${row.domain_names.join(", ")}`,
);
await internalNginx.configure(deadHostModel, "dead_host", row);
}
// Streams
const streamRows = await streamModel
.query()
.where("is_deleted", 0)
.andWhere("enabled", 1)
.groupBy("id")
.allowGraph("[owner,access_list,certificate]");
for (const row of streamRows) {
logger.info(`Regenerating config for Stream #${row.id}: ${row.incoming_port} -> ${row.forwarding_host}:${row.forwarding_port}`);
await internalNginx.configure(deadHostModel, "stream", row);
}
logger.success("Completed");
process.exit(0);

View File

@@ -1,11 +1,6 @@
{% if certificate and certificate_id > 0 -%}
{% if ssl_forced == 1 or ssl_forced == true %}
# Force SSL
{% if trust_forwarded_proto == true %}
set $trust_forwarded_proto "T";
{% else %}
set $trust_forwarded_proto "F";
{% endif %}
include conf.d/include/force-ssl.conf;
{% endif %}
{% endif %}

File diff suppressed because it is too large Load Diff

View File

@@ -109,7 +109,7 @@ services:
- "cypress_logs:/test/results"
- "./dev/resolv.conf:/etc/resolv.conf:ro"
- "/etc/localtime:/etc/localtime:ro"
command: cypress run --browser chrome --config-file=cypress/config/ci.mjs
command: cypress run --browser chrome --config-file=cypress/config/ci.js
networks:
- fulltest

View File

@@ -192,7 +192,7 @@ services:
- "../test/results:/results"
- "./dev/resolv.conf:/etc/resolv.conf:ro"
- "/etc/localtime:/etc/localtime:ro"
command: cypress run --browser chrome --config-file=cypress/config/ci.mjs
command: cypress run --browser chrome --config-file=cypress/config/ci.js
networks:
- nginx_proxy_manager

View File

@@ -5,28 +5,6 @@ if ($scheme = "http") {
if ($request_uri = /.well-known/acme-challenge/test-challenge) {
set $test "${test}T";
}
# Check if the ssl staff has been handled
set $test_ssl_handled "";
if ($trust_forwarded_proto = "") {
set $trust_forwarded_proto "F";
}
if ($trust_forwarded_proto = "T") {
set $test_ssl_handled "${test_ssl_handled}T";
}
if ($http_x_forwarded_proto = "https") {
set $test_ssl_handled "${test_ssl_handled}S";
}
if ($http_x_forwarded_scheme = "https") {
set $test_ssl_handled "${test_ssl_handled}S";
}
if ($test_ssl_handled = "TSS") {
set $test_ssl_handled "TS";
}
if ($test_ssl_handled = "TS") {
set $test "${test}S";
}
if ($test = H) {
return 301 https://$host$request_uri;
}

View File

@@ -1,7 +1,7 @@
add_header X-Served-By $host;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $x_forwarded_scheme;
proxy_set_header X-Forwarded-Proto $x_forwarded_proto;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass $forward_scheme://$server:$port$request_uri;

View File

@@ -47,28 +47,16 @@ http {
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
include /etc/nginx/conf.d/include/log-proxy[.]conf;
include /etc/nginx/conf.d/include/log-proxy.conf;
# Dynamically generated resolvers file
include /etc/nginx/conf.d/include/resolvers[.]conf;
include /etc/nginx/conf.d/include/resolvers.conf;
# Default upstream scheme
map $host $forward_scheme {
default http;
}
# Handle upstream X-Forwarded-Proto and X-Forwarded-Scheme header
map $http_x_forwarded_proto $x_forwarded_proto {
"http" "http";
"https" "https";
default $scheme;
}
map $http_x_forwarded_scheme $x_forwarded_scheme {
"http" "http";
"https" "https";
default $scheme;
}
# Real IP Determination
# Local subnets:
@@ -76,7 +64,7 @@ http {
set_real_ip_from 172.16.0.0/12; # Includes Docker subnet
set_real_ip_from 192.168.0.0/16;
# NPM generated CDN ip ranges:
include conf.d/include/ip_ranges[.]conf;
include conf.d/include/ip_ranges.conf;
# always put the following 2 lines after ip subnets:
real_ip_header X-Real-IP;
real_ip_recursive on;
@@ -98,7 +86,7 @@ http {
stream {
# Log format and fallback log file
include /etc/nginx/conf.d/include/log-stream[.]conf;
include /etc/nginx/conf.d/include/log-stream.conf;
# Files generated by NPM
include /data/nginx/stream/*.conf;

View File

@@ -7,10 +7,8 @@ log_info 'Dynamic resolvers ...'
# Dynamically generate resolvers file, if resolver is IPv6, enclose in `[]`
# thanks @tfmm
if [ "$(is_true "${DISABLE_RESOLVER:-}")" = '0' ]; then
if [ "$(is_true "${DISABLE_IPV6:-}")" = '1' ]; then
echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) ipv6=off valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf
else
echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf
fi
if [ "$(is_true "$DISABLE_IPV6")" = '1' ]; then
echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) ipv6=off valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf
else
echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf
fi

View File

@@ -12,7 +12,7 @@ process_folder () {
FILES=$(find "$1" -type f -name "*.conf")
SED_REGEX=
if [ "$(is_true "${DISABLE_IPV6:-}")" = '1' ]; then
if [ "$(is_true "$DISABLE_IPV6")" = '1' ]; then
# IPV6 is disabled
echo "Disabling IPV6 in hosts in: $1"
SED_REGEX='s/^([^#]*)listen \[::\]/\1#listen [::]/g'
@@ -25,13 +25,7 @@ process_folder () {
for FILE in $FILES
do
echo "- ${FILE}"
TMPFILE="${FILE}.tmp"
if sed -E "$SED_REGEX" "$FILE" > "$TMPFILE" && [ -s "$TMPFILE" ]; then
mv "$TMPFILE" "$FILE"
else
echo "WARNING: skipping ${FILE} — sed produced empty output" >&2
rm -f "$TMPFILE"
fi
echo "$(sed -E "$SED_REGEX" "$FILE")" > $FILE
done
# ensure the files are still owned by the npm user

View File

@@ -17,6 +17,10 @@ case $TARGETPLATFORM in
S6_ARCH=aarch64
;;
linux/arm/v7)
S6_ARCH=armhf
;;
*)
S6_ARCH=x86_64
;;

View File

@@ -2,8 +2,7 @@
"scripts": {
"dev": "vitepress dev --host",
"build": "vitepress build",
"preview": "vitepress preview",
"set-version": "./scripts/set-version.sh"
"preview": "vitepress preview"
},
"devDependencies": {
"vitepress": "^1.6.4"

View File

@@ -1,17 +0,0 @@
#!/bin/bash
set -euf
# this script accepts a version number as an argument
# and replaces {{VERSION}} in src/*.md with the provided version number.
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <version>"
exit 1
fi
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$DIR/.." || exit 1
VERSION="$1"
# find all .md files in src/ and replace {{VERSION}} with the provided version number
find src/ -type f -name "*.md" -exec sed -i "s/{{VERSION}}/$VERSION/g" {} \;

View File

@@ -0,0 +1,176 @@
# Two-Factor Authentication Implementation
> **Note:** This document should be deleted after PR approval. It serves as a reference for reviewers to understand the scope of the contribution.
---
**Acknowledgments**
Thanks to all contributors and authors from the Inte.Team for the great work on Nginx Proxy Manager. It saves us time and effort, and we're happy to contribute back to the project.
---
## Overview
Add TOTP-based two-factor authentication to the login flow. Users can enable 2FA from their profile settings, scan a QR code with any authenticator app (Google Authenticator, Authy, etc.), and will be required to enter a 6-digit code on login.
## Current Authentication Flow
```
POST /tokens {identity, secret}
-> Validate user exists and is not disabled
-> Verify password against auth.secret
-> Return JWT token
```
## Proposed 2FA Flow
```
POST /tokens {identity, secret}
-> Validate user exists and is not disabled
-> Verify password against auth.secret
-> If 2FA enabled:
Return {requires_2fa: true, challenge_token: <short-lived JWT>}
-> Else:
Return {token: <JWT>, expires: <timestamp>}
POST /tokens/2fa {challenge_token, code}
-> Validate challenge_token
-> Verify TOTP code against user's secret
-> Return {token: <JWT>, expires: <timestamp>}
```
## Database Changes
Extend the existing `auth.meta` JSON column to store 2FA data:
```json
{
"totp_secret": "<encrypted-secret>",
"totp_enabled": true,
"totp_enabled_at": "<timestamp>",
"backup_codes": ["<hashed-code-1>", "<hashed-code-2>", ...]
}
```
No new tables required. The `auth.meta` column is already designed for this purpose.
## Backend Changes
### New Files
1. `backend/internal/2fa.js` - Core 2FA logic
- `generateSecret()` - Generate TOTP secret
- `generateQRCodeURL(user, secret)` - Generate otpauth URL
- `verifyCode(secret, code)` - Verify TOTP code
- `generateBackupCodes()` - Generate 8 backup codes
- `verifyBackupCode(user, code)` - Verify and consume backup code
2. `backend/routes/2fa.js` - 2FA management endpoints
- `GET /users/:id/2fa` - Get 2FA status
- `POST /users/:id/2fa/setup` - Start 2FA setup, return QR code
- `PUT /users/:id/2fa/enable` - Verify code and enable 2FA
- `DELETE /users/:id/2fa` - Disable 2FA (requires code)
- `GET /users/:id/2fa/backup-codes` - View remaining backup codes count
- `POST /users/:id/2fa/backup-codes` - Regenerate backup codes
### Modified Files
1. `backend/internal/token.js`
- Update `getTokenFromEmail()` to check for 2FA
- Add `verifyTwoFactorChallenge()` function
- Add `createChallengeToken()` for short-lived 2FA tokens
2. `backend/routes/tokens.js`
- Add `POST /tokens/2fa` endpoint
3. `backend/index.js`
- Register new 2FA routes
### Dependencies
Add to `package.json`:
```json
"otplib": "^12.0.1"
```
## Frontend Changes
### New Files
1. `frontend/src/pages/Login2FA/index.tsx` - 2FA code entry page
2. `frontend/src/modals/TwoFactorSetupModal.tsx` - Setup wizard modal
3. `frontend/src/api/backend/twoFactor.ts` - 2FA API functions
4. `frontend/src/api/backend/verify2FA.ts` - Token verification
### Modified Files
1. `frontend/src/api/backend/responseTypes.ts`
- Add `TwoFactorChallengeResponse` type
- Add `TwoFactorStatusResponse` type
2. `frontend/src/context/AuthContext.tsx`
- Add `twoFactorRequired` state
- Add `challengeToken` state
- Update `login()` to handle 2FA response
- Add `verify2FA()` function
3. `frontend/src/pages/Login/index.tsx`
- Handle 2FA challenge response
- Redirect to 2FA entry when required
4. `frontend/src/pages/Settings/` (or user profile)
- Add 2FA enable/disable section
### Dependencies
Add to `package.json`:
```json
"qrcode.react": "^3.1.0"
```
## API Endpoints Summary
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| POST | /tokens | No | Login (returns challenge if 2FA) |
| POST | /tokens/2fa | Challenge | Complete 2FA login |
| GET | /users/:id/2fa | JWT | Get 2FA status |
| POST | /users/:id/2fa/setup | JWT | Start setup, get QR code |
| PUT | /users/:id/2fa/enable | JWT | Verify and enable |
| DELETE | /users/:id/2fa | JWT | Disable (requires code) |
| GET | /users/:id/2fa/backup-codes | JWT | Get backup codes count |
| POST | /users/:id/2fa/backup-codes | JWT | Regenerate codes |
## Security Considerations
1. Challenge tokens expire in 5 minutes
2. TOTP secrets encrypted at rest
3. Backup codes hashed with bcrypt
4. Rate limit on 2FA attempts (5 attempts, 15 min lockout)
5. Backup codes single-use only
6. 2FA disable requires valid TOTP code
## Implementation Order
1. Backend: Add `otplib` dependency
2. Backend: Create `internal/2fa.js` module
3. Backend: Update `internal/token.js` for challenge flow
4. Backend: Add `POST /tokens/2fa` route
5. Backend: Create `routes/2fa.js` for management
6. Frontend: Add `qrcode.react` dependency
7. Frontend: Update API types and functions
8. Frontend: Update AuthContext for 2FA state
9. Frontend: Create Login2FA page
10. Frontend: Update Login to handle 2FA
11. Frontend: Add 2FA settings UI
## Testing
1. Enable 2FA for user
2. Login with password only - should get challenge
3. Submit correct TOTP - should get token
4. Submit wrong TOTP - should fail
5. Use backup code - should work once
6. Disable 2FA - should require valid code
7. Login after disable - should work without 2FA

View File

@@ -14,7 +14,7 @@ on the `data` and `letsencrypt` folders at startup.
```yml
services:
app:
image: 'jc21/nginx-proxy-manager:{{VERSION}}'
image: 'jc21/nginx-proxy-manager:latest'
environment:
PUID: 1000
PGID: 1000
@@ -101,7 +101,7 @@ secrets:
services:
app:
image: 'jc21/nginx-proxy-manager:{{VERSION}}'
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
# Public HTTP Port:
@@ -130,16 +130,18 @@ services:
- db
db:
image: 'linuxserver/mariadb'
image: jc21/mariadb-aria
restart: unless-stopped
environment:
# MYSQL_ROOT_PASSWORD: "npm" # use secret instead
MYSQL_ROOT_PASSWORD__FILE: /run/secrets/DB_ROOT_PWD
MYSQL_DATABASE: 'npm'
MYSQL_USER: 'npm'
MYSQL_DATABASE: "npm"
MYSQL_USER: "npm"
# MYSQL_PASSWORD: "npm" # use secret instead
MYSQL_PASSWORD__FILE: /run/secrets/MYSQL_PWD
TZ: 'Australia/Brisbane'
MARIADB_AUTO_UPGRADE: '1'
volumes:
- ./mariadb:/config
- ./mysql:/var/lib/mysql
secrets:
- DB_ROOT_PWD
- MYSQL_PWD
@@ -231,20 +233,8 @@ load_module /usr/lib/nginx/modules/ngx_stream_geoip2_module.so;
Setting these environment variables will create the default user on startup, skipping the UI first user setup screen:
```yml
```
environment:
INITIAL_ADMIN_EMAIL: my@example.com
INITIAL_ADMIN_PASSWORD: mypassword1
```
## Disable Nginx Resolver
On startup, we generate a resolvers directive for Nginx unless this is defined:
```yml
environment:
DISABLE_RESOLVER: true
```
In this configuration, all DNS queries performed by Nginx will fall to the `/etc/hosts` file
and then the `/etc/resolv.conf`.

View File

@@ -64,7 +64,7 @@ I won't go in to too much detail here but here are the basics for someone new to
```yml
services:
app:
image: 'jc21/nginx-proxy-manager:{{VERSION}}'
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
environment:
TZ: "Australia/Brisbane"

View File

@@ -11,7 +11,7 @@ Create a `docker-compose.yml` file:
```yml
services:
app:
image: 'jc21/nginx-proxy-manager:{{VERSION}}'
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
@@ -45,7 +45,10 @@ docker compose up -d
## Using MySQL / MariaDB Database
If you opt for the MySQL configuration you will have to provide the database server yourself.
If you opt for the MySQL configuration you will have to provide the database server yourself. You can also use MariaDB. Here are the minimum supported versions:
- MySQL v5.7.8+
- MariaDB v10.2.7+
It's easy to use another docker container for your database also and link it as part of the docker stack, so that's what the following examples
are going to use.
@@ -55,7 +58,7 @@ Here is an example of what your `docker-compose.yml` will look like when using a
```yml
services:
app:
image: 'jc21/nginx-proxy-manager:{{VERSION}}'
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
# These ports are in format <host-port>:<container-port>
@@ -85,29 +88,31 @@ services:
- db
db:
image: 'linuxserver/mariadb'
image: 'jc21/mariadb-aria:latest'
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: 'npm'
MYSQL_DATABASE: 'npm'
MYSQL_USER: 'npm'
MYSQL_PASSWORD: 'npm'
TZ: 'Australia/Brisbane'
MARIADB_AUTO_UPGRADE: '1'
volumes:
- ./mariadb:/config
- ./mysql:/var/lib/mysql
```
::: warning
Please note, that `DB_MYSQL_*` environment variables will take precedent over `DB_SQLITE_*` variables. So if you keep the MySQL variables, you will not be able to use SQLite.
:::
### Optional: MySQL / MariaDB SSL
You can enable TLS for the MySQL/MariaDB connection with these environment variables:
- `DB_MYSQL_SSL`: Enable SSL when set to true. If unset or false, SSL disabled (previous default behaviour).
- `DB_MYSQL_SSL_REJECT_UNAUTHORIZED`: (default: true) Validate the server certificate chain. Set to false to allow selfsigned/unknown CA.
- `DB_MYSQL_SSL_VERIFY_IDENTITY`: (default: true) Performs host name / identity verification.
- DB_MYSQL_SSL: Enable SSL when set to true. If unset or false, SSL disabled (previous default behaviour).
- DB_MYSQL_SSL_REJECT_UNAUTHORIZED: (default: true) Validate the server certificate chain. Set to false to allow selfsigned/unknown CA.
- DB_MYSQL_SSL_VERIFY_IDENTITY: (default: true) Performs host name / identity verification.
Enabling SSL using a self-signed cert (not recommended for production).
@@ -118,7 +123,7 @@ Similar to the MySQL server setup:
```yml
services:
app:
image: 'jc21/nginx-proxy-manager:{{VERSION}}'
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
# These ports are in format <host-port>:<container-port>
@@ -164,11 +169,7 @@ Custom Postgres schema is not supported, as such `public` will be used.
The docker images support the following architectures:
- amd64
- arm64
::: warning
`armv7` is no longer supported in version 2.14+. This is due to Nodejs dropping support for armhf. Please
use the `2.13.7` image tag if this applies to you.
:::
- armv7
The docker images are a manifest of all the architecture docker builds supported, so this means
you don't have to worry about doing anything special and you can follow the common instructions above.
@@ -180,6 +181,8 @@ for a list of supported architectures and if you want one that doesn't exist,
Also, if you don't know how to already, follow [this guide to install docker and docker-compose](https://manre-universe.net/how-to-run-docker-and-docker-compose-on-raspbian/)
on Raspbian.
Please note that the `jc21/mariadb-aria:latest` image might have some problems on some ARM devices, if you want a separate database container, use the `yobasystems/alpine-mariadb:latest` image.
## Initial Run
After the app is running for the first time, the following will happen:

View File

@@ -335,130 +335,85 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
"@rollup/rollup-android-arm-eabi@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz#a6742c74c7d9d6d604ef8a48f99326b4ecda3d82"
integrity sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==
"@rollup/rollup-android-arm-eabi@4.24.0":
version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz#1661ff5ea9beb362795304cb916049aba7ac9c54"
integrity sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==
"@rollup/rollup-android-arm64@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz#97247be098de4df0c11971089fd2edf80a5da8cf"
integrity sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==
"@rollup/rollup-android-arm64@4.24.0":
version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz#2ffaa91f1b55a0082b8a722525741aadcbd3971e"
integrity sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==
"@rollup/rollup-darwin-arm64@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz#674852cf14cf11b8056e0b1a2f4e872b523576cf"
integrity sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==
"@rollup/rollup-darwin-arm64@4.24.0":
version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz#627007221b24b8cc3063703eee0b9177edf49c1f"
integrity sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==
"@rollup/rollup-darwin-x64@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz#36dfd7ed0aaf4d9d89d9ef983af72632455b0246"
integrity sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==
"@rollup/rollup-darwin-x64@4.24.0":
version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz#0605506142b9e796c370d59c5984ae95b9758724"
integrity sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==
"@rollup/rollup-freebsd-arm64@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz#2f87c2074b4220260fdb52a9996246edfc633c22"
integrity sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==
"@rollup/rollup-linux-arm-gnueabihf@4.24.0":
version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz#62dfd196d4b10c0c2db833897164d2d319ee0cbb"
integrity sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==
"@rollup/rollup-freebsd-x64@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz#9b5a26522a38a95dc06616d1939d4d9a76937803"
integrity sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==
"@rollup/rollup-linux-arm-musleabihf@4.24.0":
version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz#53ce72aeb982f1f34b58b380baafaf6a240fddb3"
integrity sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==
"@rollup/rollup-linux-arm-gnueabihf@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz#86aa4859385a8734235b5e40a48e52d770758c3a"
integrity sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==
"@rollup/rollup-linux-arm64-gnu@4.24.0":
version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz#1632990f62a75c74f43e4b14ab3597d7ed416496"
integrity sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==
"@rollup/rollup-linux-arm-musleabihf@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz#cbe70e56e6ece8dac83eb773b624fc9e5a460976"
integrity sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==
"@rollup/rollup-linux-arm64-musl@4.24.0":
version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz#8c03a996efb41e257b414b2e0560b7a21f2d9065"
integrity sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==
"@rollup/rollup-linux-arm64-gnu@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz#d14992a2e653bc3263d284bc6579b7a2890e1c45"
integrity sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==
"@rollup/rollup-linux-powerpc64le-gnu@4.24.0":
version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz#5b98729628d5bcc8f7f37b58b04d6845f85c7b5d"
integrity sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==
"@rollup/rollup-linux-arm64-musl@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz#2fdd1ddc434ea90aeaa0851d2044789b4d07f6da"
integrity sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==
"@rollup/rollup-linux-riscv64-gnu@4.24.0":
version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz#48e42e41f4cabf3573cfefcb448599c512e22983"
integrity sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==
"@rollup/rollup-linux-loong64-gnu@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz#8a181e6f89f969f21666a743cd411416c80099e7"
integrity sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==
"@rollup/rollup-linux-s390x-gnu@4.24.0":
version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz#e0b4f9a966872cb7d3e21b9e412a4b7efd7f0b58"
integrity sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==
"@rollup/rollup-linux-loong64-musl@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz#904125af2babc395f8061daa27b5af1f4e3f2f78"
integrity sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==
"@rollup/rollup-linux-x64-gnu@4.24.0":
version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz#78144741993100f47bd3da72fce215e077ae036b"
integrity sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==
"@rollup/rollup-linux-ppc64-gnu@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz#a57970ac6864c9a3447411a658224bdcf948be22"
integrity sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==
"@rollup/rollup-linux-x64-musl@4.24.0":
version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz#d9fe32971883cd1bd858336bd33a1c3ca6146127"
integrity sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==
"@rollup/rollup-linux-ppc64-musl@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz#bb84de5b26870567a4267666e08891e80bb56a63"
integrity sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==
"@rollup/rollup-win32-arm64-msvc@4.24.0":
version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz#71fa3ea369316db703a909c790743972e98afae5"
integrity sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==
"@rollup/rollup-linux-riscv64-gnu@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz#72d00d2c7fb375ce3564e759db33f17a35bffab9"
integrity sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==
"@rollup/rollup-win32-ia32-msvc@4.24.0":
version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz#653f5989a60658e17d7576a3996deb3902e342e2"
integrity sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==
"@rollup/rollup-linux-riscv64-musl@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz#4c166ef58e718f9245bd31873384ba15a5c1a883"
integrity sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==
"@rollup/rollup-linux-s390x-gnu@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz#bb5025cde9a61db478c2ca7215808ad3bce73a09"
integrity sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==
"@rollup/rollup-linux-x64-gnu@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz#9b66b1f9cd95c6624c788f021c756269ffed1552"
integrity sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==
"@rollup/rollup-linux-x64-musl@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz#b007ca255dc7166017d57d7d2451963f0bd23fd9"
integrity sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==
"@rollup/rollup-openbsd-x64@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz#e8b357b2d1aa2c8d76a98f5f0d889eabe93f4ef9"
integrity sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==
"@rollup/rollup-openharmony-arm64@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz#96c2e3f4aacd3d921981329831ff8dde492204dc"
integrity sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==
"@rollup/rollup-win32-arm64-msvc@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz#2d865149d706d938df8b4b8f117e69a77646d581"
integrity sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==
"@rollup/rollup-win32-ia32-msvc@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz#abe1593be0fa92325e9971c8da429c5e05b92c36"
integrity sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==
"@rollup/rollup-win32-x64-gnu@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz#c4af3e9518c9a5cd4b1c163dc81d0ad4d82e7eab"
integrity sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==
"@rollup/rollup-win32-x64-msvc@4.59.0":
version "4.59.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz#4584a8a87b29188a4c1fe987a9fcf701e256d86c"
integrity sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==
"@rollup/rollup-win32-x64-msvc@4.24.0":
version "4.24.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz#0574d7e87b44ee8511d08cc7f914bcb802b70818"
integrity sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==
"@shikijs/core@2.5.0", "@shikijs/core@^2.1.0":
version "2.5.0"
@@ -524,10 +479,10 @@
resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz#a90ab31d0cc1dfb54c66a69e515bf624fa7b2224"
integrity sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==
"@types/estree@1.0.8":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@types/estree@1.0.6":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
"@types/hast@^3.0.0", "@types/hast@^3.0.4":
version "3.0.4"
@@ -1039,37 +994,28 @@ rfdc@^1.4.1:
integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
rollup@^4.20.0:
version "4.59.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.59.0.tgz#cf74edac17c1486f562d728a4d923a694abdf06f"
integrity sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==
version "4.24.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.24.0.tgz#c14a3576f20622ea6a5c9cad7caca5e6e9555d05"
integrity sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==
dependencies:
"@types/estree" "1.0.8"
"@types/estree" "1.0.6"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.59.0"
"@rollup/rollup-android-arm64" "4.59.0"
"@rollup/rollup-darwin-arm64" "4.59.0"
"@rollup/rollup-darwin-x64" "4.59.0"
"@rollup/rollup-freebsd-arm64" "4.59.0"
"@rollup/rollup-freebsd-x64" "4.59.0"
"@rollup/rollup-linux-arm-gnueabihf" "4.59.0"
"@rollup/rollup-linux-arm-musleabihf" "4.59.0"
"@rollup/rollup-linux-arm64-gnu" "4.59.0"
"@rollup/rollup-linux-arm64-musl" "4.59.0"
"@rollup/rollup-linux-loong64-gnu" "4.59.0"
"@rollup/rollup-linux-loong64-musl" "4.59.0"
"@rollup/rollup-linux-ppc64-gnu" "4.59.0"
"@rollup/rollup-linux-ppc64-musl" "4.59.0"
"@rollup/rollup-linux-riscv64-gnu" "4.59.0"
"@rollup/rollup-linux-riscv64-musl" "4.59.0"
"@rollup/rollup-linux-s390x-gnu" "4.59.0"
"@rollup/rollup-linux-x64-gnu" "4.59.0"
"@rollup/rollup-linux-x64-musl" "4.59.0"
"@rollup/rollup-openbsd-x64" "4.59.0"
"@rollup/rollup-openharmony-arm64" "4.59.0"
"@rollup/rollup-win32-arm64-msvc" "4.59.0"
"@rollup/rollup-win32-ia32-msvc" "4.59.0"
"@rollup/rollup-win32-x64-gnu" "4.59.0"
"@rollup/rollup-win32-x64-msvc" "4.59.0"
"@rollup/rollup-android-arm-eabi" "4.24.0"
"@rollup/rollup-android-arm64" "4.24.0"
"@rollup/rollup-darwin-arm64" "4.24.0"
"@rollup/rollup-darwin-x64" "4.24.0"
"@rollup/rollup-linux-arm-gnueabihf" "4.24.0"
"@rollup/rollup-linux-arm-musleabihf" "4.24.0"
"@rollup/rollup-linux-arm64-gnu" "4.24.0"
"@rollup/rollup-linux-arm64-musl" "4.24.0"
"@rollup/rollup-linux-powerpc64le-gnu" "4.24.0"
"@rollup/rollup-linux-riscv64-gnu" "4.24.0"
"@rollup/rollup-linux-s390x-gnu" "4.24.0"
"@rollup/rollup-linux-x64-gnu" "4.24.0"
"@rollup/rollup-linux-x64-musl" "4.24.0"
"@rollup/rollup-win32-arm64-msvc" "4.24.0"
"@rollup/rollup-win32-ia32-msvc" "4.24.0"
"@rollup/rollup-win32-x64-msvc" "4.24.0"
fsevents "~2.3.2"
shiki@^2.1.0:

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.3/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.2/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",

View File

@@ -7,30 +7,25 @@
// - Also checks the error messages returned by the backend
const allLocales = [
["en", "en-US"],
["de", "de-DE"],
["pt", "pt-PT"],
["es", "es-ES"],
["et", "et-EE"],
["fr", "fr-FR"],
["it", "it-IT"],
["ja", "ja-JP"],
["nl", "nl-NL"],
["pl", "pl-PL"],
["ru", "ru-RU"],
["sk", "sk-SK"],
["cs", "cs-CZ"],
["vi", "vi-VN"],
["zh", "zh-CN"],
["ko", "ko-KR"],
["bg", "bg-BG"],
["id", "id-ID"],
["tr", "tr-TR"],
["hu", "hu-HU"],
["no", "no-NO"],
["en", "en-US"],
["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 = [
/^.*$/,
];
const { spawnSync } = require("child_process");
const fs = require("fs");
@@ -71,95 +66,105 @@ const allWarnings = [];
const allKeys = [];
const checkLangList = (fullCode) => {
const key = "locale-" + fullCode;
if (typeof langList[key] === "undefined") {
allErrors.push("ERROR: `" + key + "` language does not exist in lang-list.json");
}
const key = "locale-" + fullCode;
if (typeof langList[key] === "undefined") {
allErrors.push(
"ERROR: `" + key + "` language does not exist in lang-list.json",
);
}
};
const compareLocale = (locale) => {
const projectLocaleKeys = Object.keys(allLocalesInProject);
// Check that locale contains the items used in the codebase
projectLocaleKeys.map((key) => {
if (typeof locale.data[key] === "undefined") {
allErrors.push("ERROR: `" + locale[0] + "` does not contain item: `" + key + "`");
}
return null;
});
// Check that locale contains all error.* items
BACKEND_ERRORS.forEach((key) => {
if (typeof locale.data[key] === "undefined") {
allErrors.push("ERROR: `" + locale[0] + "` does not contain item: `" + key + "`");
}
return null;
});
const projectLocaleKeys = Object.keys(allLocalesInProject);
// Check that locale contains the items used in the codebase
projectLocaleKeys.map((key) => {
if (typeof locale.data[key] === "undefined") {
allErrors.push(
"ERROR: `" + locale[0] + "` does not contain item: `" + key + "`",
);
}
return null;
});
// Check that locale contains all error.* items
BACKEND_ERRORS.forEach((key) => {
if (typeof locale.data[key] === "undefined") {
allErrors.push(
"ERROR: `" + locale[0] + "` does not contain item: `" + key + "`",
);
}
return null;
});
// Check that locale does not contain items not used in the codebase
const localeKeys = Object.keys(locale.data);
localeKeys.map((key) => {
let ignored = false;
ignoreUnused.map((regex) => {
if (key.match(regex)) {
ignored = true;
}
return null;
});
// Check that locale does not contain items not used in the codebase
const localeKeys = Object.keys(locale.data);
localeKeys.map((key) => {
let ignored = false;
ignoreUnused.map((regex) => {
if (key.match(regex)) {
ignored = true;
}
return null;
});
if (!ignored && typeof allLocalesInProject[key] === "undefined") {
// ensure this key doesn't exist in the backend errors either
if (!BACKEND_ERRORS.includes(key)) {
allErrors.push("ERROR: `" + locale[0] + "` contains unused item: `" + key + "`");
}
}
if (!ignored && typeof allLocalesInProject[key] === "undefined") {
// ensure this key doesn't exist in the backend errors either
if (!BACKEND_ERRORS.includes(key)) {
allErrors.push(
"ERROR: `" + locale[0] + "` contains unused item: `" + key + "`",
);
}
}
// Add this key to allKeys
if (allKeys.indexOf(key) === -1) {
allKeys.push(key);
}
return null;
});
// Add this key to allKeys
if (allKeys.indexOf(key) === -1) {
allKeys.push(key);
}
return null;
});
};
// Checks for any keys missing from this locale, that
// have been defined in any other locales
const checkForMissing = (locale) => {
allKeys.forEach((key) => {
if (typeof locale.data[key] === "undefined") {
allWarnings.push("WARN: `" + locale[0] + "` does not contain item: `" + key + "`");
}
return null;
});
allKeys.forEach((key) => {
if (typeof locale.data[key] === "undefined") {
allWarnings.push(
"WARN: `" + locale[0] + "` does not contain item: `" + key + "`",
);
}
return null;
});
};
// Local all locale data
allLocales.map((locale, idx) => {
checkLangList(locale[1]);
allLocales[idx].data = require("./src/locale/src/" + locale[0] + ".json");
return null;
checkLangList(locale[1]);
allLocales[idx].data = require("./src/locale/src/" + locale[0] + ".json");
return null;
});
// Verify all locale data
allLocales.map((locale) => {
compareLocale(locale);
checkForMissing(locale);
return null;
compareLocale(locale);
checkForMissing(locale);
return null;
});
if (allErrors.length) {
allErrors.map((err) => {
console.log("\x1b[31m%s\x1b[0m", err);
return null;
});
allErrors.map((err) => {
console.log("\x1b[31m%s\x1b[0m", err);
return null;
});
}
if (allWarnings.length) {
allWarnings.map((err) => {
console.log("\x1b[33m%s\x1b[0m", err);
return null;
});
allWarnings.map((err) => {
console.log("\x1b[33m%s\x1b[0m", err);
return null;
});
}
if (allErrors.length) {
process.exit(1);
process.exit(1);
}
console.log("\x1b[32m%s\x1b[0m", "Locale check passed");

View File

@@ -17,50 +17,50 @@
},
"dependencies": {
"@tabler/core": "^1.4.0",
"@tabler/icons-react": "^3.37.1",
"@tanstack/react-query": "^5.90.21",
"@tabler/icons-react": "^3.35.0",
"@tanstack/react-query": "^5.90.6",
"@tanstack/react-table": "^8.21.3",
"@uiw/react-textarea-code-editor": "^3.1.1",
"classnames": "^2.5.1",
"country-flag-icons": "^1.6.14",
"country-flag-icons": "^1.5.21",
"date-fns": "^4.1.0",
"ez-modal-react": "^1.0.5",
"formik": "^2.4.9",
"formik": "^2.4.6",
"generate-password-browser": "^1.1.0",
"humps": "^2.0.1",
"query-string": "^9.3.1",
"react": "^19.2.4",
"react": "^19.2.3",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.2.4",
"react-intl": "^8.1.3",
"react-dom": "^19.2.3",
"react-intl": "^7.1.14",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.13.0",
"react-router-dom": "^7.9.5",
"react-select": "^5.10.2",
"react-toastify": "^11.0.5",
"rooks": "^9.5.0"
"rooks": "^9.3.0"
},
"devDependencies": {
"@biomejs/biome": "^2.4.3",
"@formatjs/cli": "^6.13.0",
"@tanstack/react-query-devtools": "^5.91.3",
"@biomejs/biome": "^2.3.2",
"@formatjs/cli": "^6.7.4",
"@tanstack/react-query-devtools": "^5.90.2",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/react": "^16.3.0",
"@types/country-flag-icons": "^1.2.2",
"@types/humps": "^2.0.6",
"@types/react": "^19.2.14",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/react-table": "^7.7.20",
"@vitejs/plugin-react": "^5.1.4",
"happy-dom": "^20.7.0",
"@vitejs/plugin-react": "^5.1.2",
"happy-dom": "^20.0.10",
"postcss": "^8.5.6",
"postcss-simple-vars": "^7.0.1",
"sass": "^1.97.3",
"sass": "^1.93.3",
"tmp": "^0.2.5",
"typescript": "5.9.3",
"vite": "^7.3.1",
"vite-plugin-checker": "^0.12.0",
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.0.18"
"vite": "^7.1.12",
"vite-plugin-checker": "^0.11.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^4.0.6"
}
}

View File

@@ -156,6 +156,7 @@ export async function del({ url, params }: DeleteArgs, abortController?: AbortCo
const method = "DELETE";
const headers = {
...buildAuthHeader(),
[contentTypeHeader]: "application/json",
};
const signal = abortController?.signal;
const response = await fetch(apiUrl, { method, headers, signal });

View File

@@ -127,7 +127,6 @@ export interface ProxyHost {
locations?: ProxyLocation[];
hstsEnabled: boolean;
hstsSubdomains: boolean;
trustForwardedProto: boolean;
// Expansions:
owner?: User;
accessList?: AccessList;

View File

@@ -1,5 +1,11 @@
import { camelizeKeys, decamelizeKeys } from "humps";
import AuthStore from "src/modules/AuthStore";
import type {
TwoFactorEnableResponse,
TwoFactorSetupResponse,
TwoFactorStatusResponse,
} from "./responseTypes";
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({
@@ -9,27 +15,42 @@ export async function get2FAStatus(userId: number | "me"): Promise<TwoFactorStat
export async function start2FASetup(userId: number | "me"): Promise<TwoFactorSetupResponse> {
return await api.post({
url: `/users/${userId}/2fa`,
url: `/users/${userId}/2fa/setup`,
});
}
export async function enable2FA(userId: number | "me", code: string): Promise<TwoFactorEnableResponse> {
return await api.post({
return await api.put({
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 disable2FA(userId: number | "me", code: string): Promise<{ success: boolean }> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (AuthStore.token) {
headers.Authorization = `Bearer ${AuthStore.token.token}`;
}
const response = await fetch(`/api/users/${userId}/2fa`, {
method: "DELETE",
headers,
body: JSON.stringify(decamelizeKeys({ code })),
});
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.error?.messageI18n || payload.error?.message || "Failed to disable 2FA");
}
return camelizeKeys(payload) as { success: boolean };
}
export async function regenerateBackupCodes(userId: number | "me", code: string): Promise<TwoFactorEnableResponse> {
export async function regenerateBackupCodes(
userId: number | "me",
code: string,
): Promise<TwoFactorEnableResponse> {
return await api.post({
url: `/users/${userId}/2fa/backup-codes`,
data: { code },

View File

@@ -5,18 +5,17 @@ import { T } from "src/locale";
interface Props {
forHttp?: boolean; // the sslForced, http2Support, hstsEnabled, hstsSubdomains fields
forProxyHost?: boolean; // the advanced fields
forceDNSForNew?: boolean;
requireDomainNames?: boolean; // used for streams
color?: string;
}
export function SSLOptionsFields({ forHttp = true, forProxyHost = false, forceDNSForNew, requireDomainNames, color = "bg-cyan" }: Props) {
export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomainNames, color = "bg-cyan" }: Props) {
const { values, setFieldValue } = useFormikContext();
const v: any = values || {};
const newCertificate = v?.certificateId === "new";
const hasCertificate = newCertificate || (v?.certificateId && v?.certificateId > 0);
const { sslForced, http2Support, hstsEnabled, hstsSubdomains, trustForwardedProto, meta } = v;
const { sslForced, http2Support, hstsEnabled, hstsSubdomains, meta } = v;
const { dnsChallenge } = meta || {};
if (forceDNSForNew && newCertificate && !dnsChallenge) {
@@ -116,34 +115,6 @@ export function SSLOptionsFields({ forHttp = true, forProxyHost = false, forceDN
</div>
);
const getHttpAdvancedOptions = () =>(
<div>
<details>
<summary className="mb-1"><T id="domains.advanced" /></summary>
<div className="row">
<div className="col-12">
<Field name="trustForwardedProto">
{({ field }: any) => (
<label className="form-check form-switch mt-1">
<input
className={trustForwardedProto ? toggleEnabled : toggleClasses}
type="checkbox"
checked={!!trustForwardedProto}
onChange={(e) => handleToggleChange(e, field.name)}
disabled={!hasCertificate || !sslForced}
/>
<span className="form-check-label">
<T id="domains.trust-forwarded-proto" />
</span>
</label>
)}
</Field>
</div>
</div>
</details>
</div>
);
return (
<div>
{forHttp ? getHttpOptions() : null}
@@ -169,7 +140,6 @@ export function SSLOptionsFields({ forHttp = true, forProxyHost = false, forceDN
{dnsChallenge ? <DNSProviderFields showBoundaryBox /> : null}
</>
) : null}
{forProxyHost && forHttp ? getHttpAdvancedOptions() : null}
</div>
);
}

View File

@@ -24,7 +24,6 @@ const fetchProxyHost = (id: number | "new") => {
enabled: true,
hstsEnabled: false,
hstsSubdomains: false,
trustForwardedProto: false,
} as ProxyHost);
}
return getProxyHost(id, ["owner"]);

View File

@@ -1,98 +1,81 @@
import { createIntl, createIntlCache } from "react-intl";
import langBg from "./lang/bg.json";
import langDe from "./lang/de.json";
import langPt from "./lang/pt.json";
import langEn from "./lang/en.json";
import langEs from "./lang/es.json";
import langEt from "./lang/et.json";
import langFr from "./lang/fr.json";
import langGa from "./lang/ga.json";
import langId from "./lang/id.json";
import langIt from "./lang/it.json";
import langJa from "./lang/ja.json";
import langKo from "./lang/ko.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 langCs from "./lang/cs.json";
import langVi from "./lang/vi.json";
import langZh from "./lang/zh.json";
import langTr from "./lang/tr.json";
import langHu from "./lang/hu.json";
import langNo from "./lang/no.json";
import langList from "./lang/lang-list.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,
// not the country code
// Remember when adding to this list, also update check-locales.js script
const localeOptions = [
["en", "en-US", langEn],
["de", "de-DE", langDe],
["es", "es-ES", langEs],
["et", "et-EE", langEt],
["pt", "pt-PT", langPt],
["fr", "fr-FR", langFr],
["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],
["cs", "cs-CZ", langCs],
["vi", "vi-VN", langVi],
["zh", "zh-CN", langZh],
["ko", "ko-KR", langKo],
["bg", "bg-BG", langBg],
["id", "id-ID", langId],
["tr", "tr-TR", langTr],
["hu", "hu-HU", langHu],
["no", "no-NO", langNo],
["en", "en-US", langEn],
["de", "de-DE", langDe],
["es", "es-ES", langEs],
["ja", "ja-JP", langJa],
["it", "it-IT", langIt],
["nl", "nl-NL", langNl],
["pl", "pl-PL", langPl],
["ru", "ru-RU", langRu],
["sk", "sk-SK", langSk],
["vi", "vi-VN", langVi],
["zh", "zh-CN", langZh],
["ko", "ko-KR", langKo],
["bg", "bg-BG", langBg],
["id", "id-ID", langId],
];
const loadMessages = (locale?: string): typeof langList & typeof langEn => {
const thisLocale = (locale || "en").slice(0, 2);
const thisLocale = (locale || "en").slice(0, 2);
// ensure this lang exists in localeOptions above, otherwise fallback to en
if (thisLocale === "en" || !localeOptions.some(([code]) => code === thisLocale)) {
return Object.assign({}, langList, langEn);
}
// ensure this lang exists in localeOptions above, otherwise fallback to en
if (thisLocale === "en" || !localeOptions.some(([code]) => code === thisLocale)) {
return Object.assign({}, langList, langEn);
}
return Object.assign({}, langList, langEn, localeOptions.find(([code]) => code === thisLocale)?.[2]);
return Object.assign({}, langList, langEn, localeOptions.find(([code]) => code === thisLocale)?.[2]);
};
const getFlagCodeForLocale = (locale?: string) => {
const thisLocale = (locale || "en").slice(0, 2);
const thisLocale = (locale || "en").slice(0, 2);
// only add to this if your flag is different from the locale code
const specialCases: Record<string, string> = {
ja: "jp", // Japan
zh: "cn", // China
vi: "vn", // Vietnam
ko: "kr", // Korea
cs: "cz", // Czechia
};
// 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();
if (specialCases[thisLocale]) {
return specialCases[thisLocale].toUpperCase();
}
return thisLocale.toUpperCase();
};
const getLocale = (short = false) => {
let loc = window.localStorage.getItem("locale");
if (!loc) {
loc = document.documentElement.lang;
}
if (short) {
return loc.slice(0, 2);
}
// finally, fallback
if (!loc) {
loc = "en";
}
return loc;
let loc = window.localStorage.getItem("locale");
if (!loc) {
loc = document.documentElement.lang;
}
if (short) {
return loc.slice(0, 2);
}
// finally, fallback
if (!loc) {
loc = "en";
}
return loc;
};
const cache = createIntlCache();
@@ -101,43 +84,43 @@ const initialMessages = loadMessages(getLocale());
let intl = createIntl({ locale: getLocale(), messages: initialMessages }, cache);
const changeLocale = (locale: string): void => {
const messages = loadMessages(locale);
intl = createIntl({ locale, messages }, cache);
window.localStorage.setItem("locale", locale);
document.documentElement.lang = locale;
const messages = loadMessages(locale);
intl = createIntl({ locale, messages }, cache);
window.localStorage.setItem("locale", locale);
document.documentElement.lang = locale;
};
// This is a translation component that wraps the translation in a span with a data
// attribute so devs can inspect the element to see the translation ID
const T = ({
id,
data,
tData,
id,
data,
tData,
}: {
id: string;
data?: Record<string, string | number | undefined>;
tData?: Record<string, string>;
id: string;
data?: Record<string, string | number | undefined>;
tData?: Record<string, string>;
}) => {
const translatedData: Record<string, string> = {};
if (tData) {
// iterate over tData and translate each value
Object.entries(tData).forEach(([key, value]) => {
translatedData[key] = intl.formatMessage({ id: value });
});
}
return (
<span data-translation-id={id}>
{intl.formatMessage(
{ id },
{
...data,
...translatedData,
},
)}
</span>
);
const translatedData: Record<string, string> = {};
if (tData) {
// iterate over tData and translate each value
Object.entries(tData).forEach(([key, value]) => {
translatedData[key] = intl.formatMessage({ id: value });
});
}
return (
<span data-translation-id={id}>
{intl.formatMessage(
{ id },
{
...data,
...translatedData,
},
)}
</span>
);
};
//console.log("L:", localeOptions);
console.log("L:", localeOptions);
export { localeOptions, getFlagCodeForLocale, getLocale, createIntl, changeLocale, intl, T };

View File

@@ -1,69 +0,0 @@
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const DIR = path.resolve(__dirname, "../src");
// Function to sort object keys recursively
function sortKeys(obj) {
if (obj === null || typeof obj !== "object" || obj instanceof Array) {
return obj;
}
const sorted = {};
const keys = Object.keys(obj).sort();
for (const key of keys) {
const value = obj[key];
if (typeof value === "object" && value !== null && !(value instanceof Array)) {
sorted[key] = sortKeys(value);
} else {
sorted[key] = value;
}
}
return sorted;
}
// Get all JSON files in the directory
const files = fs.readdirSync(DIR).filter((file) => {
return file.endsWith(".json") && file !== "lang-list.json";
});
files.forEach((file) => {
const filePath = path.join(DIR, file);
const stats = fs.statSync(filePath);
if (!stats.isFile()) {
return;
}
if (stats.size === 0) {
console.log(`Skipping empty file ${file}`);
return;
}
try {
// Read original content
const originalContent = fs.readFileSync(filePath, "utf8");
const originalJson = JSON.parse(originalContent);
// Sort keys
const sortedJson = sortKeys(originalJson);
// Convert back to string with tabs
const sortedContent = JSON.stringify(sortedJson, null, "\t") + "\n";
// Compare (normalize whitespace)
if (originalContent.trim() === sortedContent.trim()) {
console.log(`${file} is already sorted`);
return;
}
// Write sorted content
fs.writeFileSync(filePath, sortedContent, "utf8");
console.log(`Sorted ${file}`);
} catch (error) {
console.error(`Error processing ${file}:`, error.message);
}
});

View File

@@ -1,7 +0,0 @@
## Co je seznam přístupů?
Seznamy přístupů poskytují blacklist nebo whitelist konkrétních IP adres klientů spolu s ověřením pro proxy hostitele prostřednictvím základního ověřování HTTP.
Můžete nakonfigurovat více pravidel pro klienty, uživatelská jména a hesla pro jeden seznam přístupu a poté ho použít na jednoho nebo více proxy hostitelů.
Toto je nejužitečnější pro přesměrované webové služby, které nemají vestavěné ověřovací mechanismy, nebo pokud se chcete chránit před neznámými klienty.

View File

@@ -1,32 +0,0 @@
## Pomoc s certifikáty
### Certifikát HTTP
Certifikát ověřený prostřednictvím protokolu HTTP znamená, že servery Let's Encrypt se
pokusí připojit k vašim doménám přes protokol HTTP (nikoli HTTPS!) a v případě úspěchu
vydají váš certifikát.
Pro tuto metodu budete muset mít pro své domény vytvořeného _Proxy Host_, který
je přístupný přes HTTP a směruje na tuto instalaci Nginx. Po vydání certifikátu
můžete změnit _Proxy Host_ tak, aby tento certifikát používal i pro HTTPS
připojení. _Proxy Host_ však bude stále potřeba nakonfigurovat pro přístup přes HTTP,
aby se certifikát mohl obnovit.
Tento proces _nepodporuje_ domény se zástupnými znaky.
### Certifikát DNS
Certifikát ověřený DNS vyžaduje použití pluginu DNS Provider. Tento DNS
Provider se použije na vytvoření dočasných záznamů ve vaší doméně a poté Let's
Encrypt ověří tyto záznamy, aby se ujistil, že jste vlastníkem, a pokud bude úspěšný,
vydá váš certifikát.
Před požádáním o tento typ certifikátu není potřeba vytvořit _Proxy Host_.
Není také potřeba mít _Proxy Host_ nakonfigurovaný pro přístup HTTP.
Tento proces _podporuje_ domény se zástupnými znaky.
### Vlastní certifikát
Tuto možnost použijte na nahrání vlastního SSL certifikátu, který vám poskytla vaše
certifikační autorita.

View File

@@ -1,10 +0,0 @@
## Co je to 404 Host?
404 Host je jednoduše nastavení hostitele, které zobrazuje stránku 404.
To může být užitečné, pokud je vaše doména uvedena ve vyhledávačích a chcete
poskytnout hezčí chybovou stránku nebo konkrétně oznámit vyhledávačům, že
stránky domény již neexistují.
Další výhodou tohoto hostitele je sledování protokolů o návštěvách a
zobrazení odkazů.

View File

@@ -1,7 +0,0 @@
## Co je proxy hostitel?
Proxy hostitel je příchozí koncový bod pro webovou službu, kterou chcete přesměrovat.
Poskytuje volitelné ukončení SSL pro vaši službu, která nemusí mít zabudovanou podporu SSL.
Proxy hostitelé jsou nejběžnějším použitím pro Nginx Proxy Manager.

View File

@@ -1,7 +0,0 @@
## Co je přesměrovací hostitel?
Přesměrovací hostitel přesměruje požadavky z příchozí domény a přesměruje
návštěvníka na jinou doménu.
Nejčastějším důvodem pro použití tohoto typu hostitele je situace, kdy vaše webová stránka změní
doménu, ale stále máte odkazy ve vyhledávačích nebo referenční odkazy směřující na starou doménu.

View File

@@ -1,6 +0,0 @@
## Co je stream?
Stream je relativně nová funkce pro Nginx, která slouží na přesměrování TCP/UDP
datového toku přímo do jiného počítače v síti.
Pokud provozujete herní servery, FTP nebo SSH servery, tato funkce se vám může hodit.

View File

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

View File

@@ -1,7 +0,0 @@
## Mis on juurdepääsuloend?
Ligipääsuloendid pakuvad konkreetsete klientide IP-aadresside musta või valget nimekirja koos puhverserverite autentimisega põhilise HTTP-autentimise kaudu.
Saate ühe juurdepääsuloendi jaoks konfigureerida mitu kliendireeglit, kasutajanime ja parooli ning seejärel rakendada neid ühele või mitmele _puhverserverile_.
See on kõige kasulikum edastatud veebiteenuste puhul, millel pole sisseehitatud autentimismehhanisme või kui soovite kaitsta tundmatute klientide eest.

View File

@@ -1,26 +0,0 @@
## Sertifikaatide abi
### HTTP-sertifikaat
HTTP-valideeritud sertifikaat tähendab, et Let's Encrypti serverid
proovivad teie domeenidega ühendust luua HTTP (mitte HTTPS!) kaudu ja kui see õnnestub,
väljastavad nad teile sertifikaadi.
Selle meetodi jaoks peate oma domeeni(de) jaoks looma _Proxy Host_, millele pääseb ligi HTTP kaudu ja mis osutab sellele Nginxi installile. Pärast sertifikaadi väljastamist saate muuta _Proxy Host_'i, et seda sertifikaati ka HTTPS
ühenduste jaoks kasutada. Sertifikaadi uuendamiseks tuleb aga _Proxy Host_ ikkagi HTTP-juurdepääsu jaoks konfigureerida.
See protsess _ei_ toeta metamärke kasutavaid domeene.
### DNS-sertifikaat
DNS-i poolt valideeritud sertifikaadi saamiseks peate kasutama DNS-pakkuja pistikprogrammi. Seda DNS-teenuse pakkujat kasutatakse teie domeenis ajutiste kirjete loomiseks ja seejärel pärib Let's
Encrypt nende kirjete kohta päringu, et veenduda, et olete omanik, ja kui see õnnestub, väljastavad nad teile sertifikaadi.
Selle tüüpi sertifikaadi taotlemiseks ei ole vaja luua _Proxy Host_'i. Samuti ei pea teie _Proxy Host_ olema HTTP-juurdepääsu jaoks konfigureeritud.
See protsess _toetab_ metamärke kasutavaid domeene.
### Kohandatud sertifikaat
Kasutage seda valikut oma SSL-sertifikaadi üleslaadimiseks, mille on esitanud teie enda sertifitseerimisasutus.

View File

@@ -1,9 +0,0 @@
## Mis on 404 host?
404 host on lihtsalt hosti seadistus, mis kuvab 404 lehte.
See võib olla kasulik, kui teie domeen on otsingumootorites loetletud ja soovite
esitada kenama vealehe või konkreetselt otsingu indekseerijatele öelda, et
domeenilehed enam ei eksisteeri.
Selle hosti teine eelis on selle külastatavuste logide jälgimine ja suunajate vaatamine.

View File

@@ -1,7 +0,0 @@
## Mis on puhverserver?
Puhverserver on veebiteenuse sissetuleva andmevoo lõpp-punkt, mida soovite edastada.
See pakub valikulist SSL-i lõpetamist teie teenusele, millel ei pruugi olla sisseehitatud SSL-tuge.
Puhverserverid on Nginxi puhverserveri halduri kõige levinum kasutusala.

View File

@@ -1,5 +0,0 @@
## Mis on ümbersuunamishost?
Ümbersuunamishost suunab sissetuleva domeeni päringud ümber ja suunab vaataja teisele domeenile.
Kõige levinum põhjus seda tüüpi hosti kasutamiseks on see, kui teie veebisaidi domeenid muutuvad, kuid otsingumootori või suunaja lingid osutavad endiselt vanale domeenile.

View File

@@ -1,5 +0,0 @@
## Mis on voog?
Nginxi suhteliselt uus funktsioon, voog, edastab TCP/UDP liiklust otse võrgus olevale teisele arvutile.
Kui sul on mänguserverid, FTP- või SSH-serverid, võib see kasuks tulla.

View File

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

View File

@@ -1,7 +0,0 @@
## Qu'est-ce qu'une liste d'accès ?
Les listes d'accès permettent de définir une liste noire ou une liste blanche d'adresses IP clientes spécifiques, ainsi que l'authentification des Hôtes Proxy via l'authentification HTTP de base.
Vous pouvez configurer plusieurs règles client, noms d'utilisateur et mots de passe pour une même liste d'accès, puis l'appliquer à un ou plusieurs Hôtes Proxy.
Ceci est particulièrement utile pour les services web redirigés qui ne disposent pas de mécanismes d'authentification intégrés ou lorsque vous souhaitez vous protéger contre les clients inconnus.

View File

@@ -1,23 +0,0 @@
## Aide concernant les certificats
### Certificat HTTP
Un certificat HTTP validé signifie que les serveurs de Let's Encrypt testeront d'accéder à vos domaines via HTTP (et non HTTPS !). En cas de succès, ils émettront votre certificat.
Pour cette méthode, vous devrez créer un Hôte Proxy pour votre ou vos domaines. Cet Hôte Proxy devra être accessible via HTTP et pointer vers cette installation Nginx. Une fois le certificat émis, vous pourrez modifier l'Hôte Proxy pour qu'il utilise également ce certificat pour les connexions HTTPS. Cependant, l'Hôte Proxy devra toujours être configuré pour l'accès HTTP afin que le certificat puisse être renouvelé.
Ce processus ne prend pas en charge les domaines génériques.
### Certificat DNS
Un certificat DNS validé nécessite l'utilisation du plugin Fournisseur DNS. Fournisseur DNS créera des enregistrements temporaires sur votre domaine. Let's Encrypt interrogera ensuite ces enregistrements pour vérifier que vous en êtes bien le propriétaire. En cas de succès, votre certificat sera émis.
Il n'est pas nécessaire de créer un Hôte Proxy avant de demander ce type de certificat.
Il n'est pas non plus nécessaire de configurer votre Hôte Proxy pour l'accès HTTP.
Ce processus prend en charge les domaines génériques.
## Certificat personnalisé
Utilisez cette option pour importer votre propre certificat SSL, fourni par votre autorité de certification.

View File

@@ -1,7 +0,0 @@
## Qu'est-ce qu'un serveur 404 ?
Un Hôte 404 est simplement un hôte configuré pour afficher une page 404.
Cela peut s'avérer utile lorsque votre domaine est indexé par les moteurs de recherche et que vous souhaitez fournir une page d'erreur plus conviviale ou, plus précisément, indiquer aux moteurs de recherche que les pages du domaine n'existent plus.
Un autre avantage de cet hôte est la possibilité de suivre les journaux et de consulter les sites référenceurs.

View File

@@ -1,7 +0,0 @@
## Qu'est-ce qu'un hôte proxy ?
Un Hôte Proxy est le point de terminaison entrant d'un service web que vous souhaitez rediriger.
Il assure la terminaison SSL optionnelle pour votre service qui ne prend pas en charge SSL nativement.
Les Hôtes Proxy constituent l'utilisation la plus courante du Nginx Proxy Manager.

View File

@@ -1,5 +0,0 @@
## Qu'est-ce qu'un serveur de redirection ?
Un Hôte de Redirection redirige les requêtes provenant du domaine entrant vers un autre domaine.
On utilise généralement ce type d'hôte lorsque votre site web change de domaine, mais que des liens provenant des moteurs de recherche ou des sites référenceurs pointent toujours vers l'ancien domaine.

View File

@@ -1,5 +0,0 @@
## Qu'est-ce qu'un Stream ?
Fonctionnalité relativement récente de Nginx, un Stream permet de rediriger le trafic TCP/UDP directement vers un autre ordinateur du réseau.
Si vous gérez des serveurs de jeux, FTP ou SSH, cela peut s'avérer très utile.

View File

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

View File

@@ -1,7 +0,0 @@
## Cad is Liosta Rochtana ann?
Soláthraíonn Liostaí Rochtana liosta dubh nó liosta bán de sheoltaí IP cliant ar leith mar aon le fíordheimhniú do na hÓstaigh Seachfhreastalaí trí Fhíordheimhniú Bunúsach HTTP.
Is féidir leat rialacha cliant, ainmneacha úsáideora agus pasfhocail iolracha a chumrú le haghaidh Liosta Rochtana aonair agus ansin iad sin a chur i bhfeidhm ar _Óstach Seachfhreastalaí_ amháin nó níos mó.
Tá sé seo an-úsáideach i gcás seirbhísí gréasáin atreoraithe nach bhfuil meicníochtaí fíordheimhnithe ionsuite iontu nó nuair is mian leat cosaint a dhéanamh ar chliaint anaithnide.

View File

@@ -1,21 +0,0 @@
## Cabhair le Deimhnithe
### Teastas HTTP
Ciallaíonn deimhniú bailíochtaithe HTTP go ndéanfaidh freastalaithe Let's Encrypt iarracht teacht ar do fhearainn thar HTTP (ní HTTPS!) agus má éiríonn leo, eiseoidh siad do theastas.
Chun an modh seo a dhéanamh, beidh ort _Óstach Proxy_ a chruthú do do fhearainn(eanna) atá inrochtana le HTTP agus ag pointeáil chuig an suiteáil Nginx seo. Tar éis deimhniú a thabhairt, is féidir leat an _Óstach Proxy_ a mhodhnú chun an deimhniú seo a úsáid le haghaidh naisc HTTPS freisin. Mar sin féin, beidh ort an _Óstach Proxy_ a chumrú fós le haghaidh rochtain HTTP chun go ndéanfar an deimhniú a athnuachan.
_Ní thacaíonn_ an próiseas seo le fearainn fiáine.
### Teastas DNS
Éilíonn deimhniú bailíochtaithe DNS ort breiseán Soláthraí DNS a úsáid. Úsáidfear an Soláthraí DNS seo chun taifid shealadacha a chruthú ar do fhearann agus ansin déanfaidh Let's Encrypt fiosrúchán ar na taifid sin lena chinntiú gurb tusa an t-úinéir agus má éiríonn leo, eiseoidh siad do theastas.
Ní gá duit _Óstach Proxy_ a chruthú sula n-iarrann tú an cineál seo teastais. Ní gá duit do _Óstach Proxy_ a chumrú le haghaidh rochtana HTTP ach an oiread.
_Tacaíonn_ an próiseas seo le fearainn fiáine.
### Teastas Saincheaptha
Úsáid an rogha seo chun do Theastas SSL féin a uaslódáil, mar a sholáthraíonn d'Údarás Deimhnithe féin é.

View File

@@ -1,7 +0,0 @@
## Cad is Óstach 404 ann?
Is socrú óstach a thaispeánann leathanach 404 é Óstach 404.
Is féidir leis seo a bheith úsáideach nuair a bhíonn do fhearann liostaithe in innill chuardaigh agus más mian leat leathanach earráide níos deise a sholáthar nó a chur in iúl do na hinnéacsóirí cuardaigh go sonrach nach bhfuil na leathanaigh fearainn ann a thuilleadh.
Buntáiste eile a bhaineann leis an óstach seo a bheith agat ná go bhfeictear na logaí le haghaidh amas agus go bhfeictear na tagairtí.

View File

@@ -1,7 +0,0 @@
## Cad is Óstach Seachfhreastalaí ann?
Is é Óstach Seachfhreastalaí an críochphointe isteach do sheirbhís ghréasáin ar mhaith leat a atreorú.
Soláthraíonn sé foirceannadh SSL roghnach do do sheirbhís nach bhfuil tacaíocht SSL ionsuite inti b'fhéidir.
Is iad Óstaigh Seachfhreastalaí an úsáid is coitianta a bhaintear as Bainisteoir Seachfhreastalaí Nginx.

View File

@@ -1,5 +0,0 @@
## Cad is Óstach Athsheolta ann?
Déanfaidh Óstach Athsheolta iarratais a atreorú ón bhfearann ag teacht isteach agus an breathnóir a bhrú chuig fearann eile.
Is é an chúis is coitianta le húsáid a bhaint as an gcineál seo óstála ná nuair a athraíonn do shuíomh Gréasáin fearainn ach go bhfuil naisc innill chuardaigh nó atreoraithe agat fós ag tagairt don seanfhearann.

View File

@@ -1,5 +0,0 @@
## Cad is Sruth ann?
Gné réasúnta nua do Nginx is ea Sruth a sheolfaidh trácht TCP/UDP go díreach chuig ríomhaire eile ar an líonra.
Más freastalaithe cluichí, freastalaithe FTP nó SSH atá á rith agat, dfhéadfadh sé seo a bheith úsáideach.

View File

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

View File

@@ -1,7 +0,0 @@
## Mi az a hozzáférési lista?
A hozzáférési listák feketelistát vagy fehérlistát biztosítanak meghatározott kliens IP-címekhez, valamint alap HTTP-hitelesítést (Basic HTTP Authentication) a proxy kiszolgálókhoz.
Egyetlen hozzáférési listához több kliensszabályt, felhasználónevet és jelszót is beállíthatsz, majd ezt alkalmazhatod egy vagy több _Proxy Kiszolgáló_-ra.
Ez különösen hasznos olyan továbbított webszolgáltatásoknál, amelyekben nincs beépített hitelesítési mechanizmus, vagy amikor ismeretlen kliensektől szeretnél védeni.

View File

@@ -1,21 +0,0 @@
## Tanúsítványok súgó
### HTTP tanúsítvány
A HTTP érvényes tanúsítvány azt jelenti, hogy a Let's Encrypt szerverek megpróbálják elérni a domaineket HTTP-n keresztül (nem HTTPS-en!), és ha sikerül, kiállítják a tanúsítványt.
Ehhez a módszerhez létre kell hoznod egy _Proxy Kiszolgáló_-t a domain(ek)hez, amely HTTP-n keresztül elérhető és erre az Nginx telepítésre mutat. Miután a tanúsítvány megérkezett, módosíthatod a _Proxy Kiszolgáló_-t, hogy ezt a tanúsítványt használja a HTTPS kapcsolatokhoz is. Azonban a _Proxy Kiszolgáló_-nak továbbra is konfigurálva kell lennie HTTP hozzáféréshez, hogy a tanúsítvány megújulhasson.
Ez a folyamat _nem_ támogatja a helyettesítő karakteres domaineket.
### DNS tanúsítvány
A DNS érvényes tanúsítvány megköveteli, hogy DNS szolgáltató plugint használj. Ez a DNS szolgáltató ideiglenes rekordokat hoz létre a domainen, majd a Let's Encrypt lekérdezi ezeket a rekordokat, hogy megbizonyosodjon a tulajdonjogról, és ha sikeres, kiállítják a tanúsítványt.
Nem szükséges előzetesen _Proxy Kiszolgáló_-t létrehozni az ilyen típusú tanúsítvány igényléséhez. Nem is kell a _Proxy Kiszolgáló_-t HTTP hozzáférésre konfigurálni.
Ez a folyamat _támogatja_ a helyettesítő karakteres domaineket.
### Egyéni tanúsítvány
Ezt az opciót használd a saját SSL tanúsítvány feltöltéséhez, amelyet a saját tanúsítványkibocsátód biztosított.

View File

@@ -1,7 +0,0 @@
## Mi az a 404-es Kiszolgáló?
A 404-es Kiszolgáló egyszerűen egy olyan kiszolgáló beállítás, amely egy 404-es oldalt jelenít meg.
Ez akkor lehet hasznos, ha a domained szerepel a keresőmotorokban, és egy szebb hibaoldalt szeretnél nyújtani, vagy kifejezetten jelezni akarod a keresőrobotoknak, hogy a domain oldalai már nem léteznek.
Ennek a kiszolgálónak egy további előnye, hogy nyomon követheted a rá érkező találatokat a naplókban, és megtekintheted a hivatkozó oldalakat.

View File

@@ -1,7 +0,0 @@
## Mi az a Proxy Kiszolgáló?
A Proxy Kiszolgáló egy bejövő végpont egy olyan webszolgáltatáshoz, amelyet továbbítani szeretnél.
Opcionális SSL lezárást biztosít a szolgáltatásodhoz, amelyben esetleg nincs beépített SSL támogatás.
A Proxy Kiszolgálók az Nginx Proxy Manager leggyakoribb felhasználási módjai.

View File

@@ -1,5 +0,0 @@
## Mi az az Átirányító Kiszolgáló?
Az Átirányító Kiszolgáló a bejövő domainre érkező kéréseket átirányítja, és a látogatót egy másik domainre küldi.
Ennek a kiszolgálótípusnak a leggyakoribb használati oka az, amikor a weboldalad domaint vált, de a keresőkben vagy a hivatkozó oldalakon még mindig a régi domainre mutató linkek vannak.

View File

@@ -1,5 +0,0 @@
## Mi az a Stream?
Az Nginx egy viszonylag új funkciója, a Stream arra szolgál, hogy a TCP/UDP forgalmat közvetlenül továbbítsa a hálózat egy másik számítógépére.
Ha játékszervereket, FTP vagy SSH szervereket futtatsz, ez hasznos lehet.

View File

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

View File

@@ -1,39 +1,36 @@
import * as bg from "./bg/index";
import * as de from "./de/index";
import * as pt from "./pt/index";
import * as en from "./en/index";
import * as es from "./es/index";
import * as et from "./et/index";
import * as fr from "./fr/index";
import * as ga from "./ga/index";
import * as id from "./id/index";
import * as it from "./it/index";
import * as ja from "./ja/index";
import * as ko from "./ko/index";
import * as nl from "./nl/index";
import * as pl from "./pl/index";
import * as ru from "./ru/index";
import * as sk from "./sk/index";
import * as cs from "./cs/index";
import * as vi from "./vi/index";
import * as zh from "./zh/index";
import * as tr from "./tr/index";
import * as hu from "./hu/index";
const items: any = { en, de, pt, es, et, ja, sk, cs, zh, pl, ru, it, vi, nl, bg, ko, ga, id, fr, tr, hu };
import * as ko from "./ko/index";
import * as bg from "./bg/index";
import * as id from "./id/index";
const items: any = { en, de, ja, sk, zh, pl, ru, it, vi, nl, bg, ko, id };
const fallbackLang = "en";
export const getHelpFile = (lang: string, section: string): string => {
if (typeof items[lang] !== "undefined" && typeof items[lang][section] !== "undefined") {
return items[lang][section].default;
}
// Fallback to English
if (typeof items[fallbackLang] !== "undefined" && typeof items[fallbackLang][section] !== "undefined") {
return items[fallbackLang][section].default;
}
throw new Error(`Cannot load help doc for ${lang}-${section}`);
if (
typeof items[lang] !== "undefined" &&
typeof items[lang][section] !== "undefined"
) {
return items[lang][section].default;
}
// Fallback to English
if (
typeof items[fallbackLang] !== "undefined" &&
typeof items[fallbackLang][section] !== "undefined"
) {
return items[fallbackLang][section].default;
}
throw new Error(`Cannot load help doc for ${lang}-${section}`);
};
export default items;

View File

@@ -1,7 +0,0 @@
## Hva er en tilgangsliste?
Tilgangslister gir en svarteliste eller hviteliste over spesifikke klientIPadresser, sammen med autentisering for `Proxyhosts` via Basic HTTPautentisering.
Du kan konfigurere flere klientregler, brukernavn og passord for én tilgangsliste og deretter bruke denne på én eller flere `Proxyhosts`.
Dette er spesielt nyttig for videresendte webtjenester som ikke har innebygd autentisering, eller når du ønsker å beskytte mot ukjente klienter.

View File

@@ -1,29 +0,0 @@
## Hjelp om sertifikater
### HTTPsertifikat
Et HTTPvalidert sertifikat betyr at Let's Encryptserverne vil forsøke å nå
domenene dine over HTTP (ikke HTTPS!) og hvis det lykkes, vil de utstede sertifikatet.
For denne metoden må du ha en `Proxyhost` opprettet for domenet/domenene dine som
er tilgjengelig over HTTP og peker til denne Nginxinstallasjonen. Etter at et sertifikat
er utstedt, kan du endre `Proxyhost` til også å bruke dette sertifikatet for HTTPStilkoblinger.
Proxyhosten må imidlertid fortsatt være konfigurert for HTTPtilgang for at sertifikatet skal kunne fornyes.
Denne prosessen _støtter ikke_ wildcarddomener.
### DNSsertifikat
Et DNSvalidert sertifikat krever at du bruker en DNSleverandørplugin. Denne leverandøren
vil opprette midlertidige DNSposter på domenet ditt, og Let's Encrypt vil deretter spørre
disse postene for å bekrefte at du eier domenet. Hvis valideringen lykkes, utstedes sertifikatet.
Du trenger ikke å ha en `Proxyhost` opprettet før du ber om denne typen sertifikat. Du trenger heller
ikke at `Proxyhost` er konfigurert for HTTPtilgang.
Denne prosessen _støtter_ wildcarddomener.
### Egendefinert sertifikat
Bruk dette alternativet for å laste opp ditt eget SSLsertifikat, levert av din
egen sertifikatmyndighet (CA).

View File

@@ -1,10 +0,0 @@
## Hva er en 404host?
En 404host er enkelt og greit en hostoppsett som viser en 404side.
Dette kan være nyttig når domenet ditt er oppført i søkemotorer og du ønsker å
vise en penere feilmelding, eller for å fortelle søkeindekser at sidene på domenet
ikke lenger eksisterer.
En annen fordel med å ha denne hosten er å kunne spore treff i loggene og
se hvilke henvisere som kommer til den.

View File

@@ -1,7 +0,0 @@
## Hva er en Proxyhost?
En Proxyhost er inngangspunktet (innkommende endepunkt) for en webtjeneste du ønsker å videresende.
Den tilbyr valgfri SSLterminering for tjenesten din hvis tjenesten ikke har innebygd støtte for SSL.
Proxyhosts er den vanligste bruken av Nginx Proxy Manager.

View File

@@ -1,7 +0,0 @@
## Hva er en omdirigeringshost?
En omdirigeringshost omdirigerer forespørsler fra det innkommende domenet og videresender
brukeren til et annet domene.
Den vanligste årsaken til å bruke denne typen host er når nettstedet ditt har byttet
domene, men søkemotorer eller henvisningslenker fortsatt peker til det gamle domenet.

View File

@@ -1,6 +0,0 @@
## Hva er en Stream?
En relativt ny funksjon i Nginx. En Stream brukes til å videresende TCP/UDPtrafikk
direkte til en annen maskin i nettverket.
Dette er nyttig hvis du kjører spillservere, FTP eller SSHservere.

View File

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

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