Compare commits

...

61 Commits

Author SHA1 Message Date
jc21
f3efaae320 Merge pull request #5141 from NginxProxyManager/develop
v2.13.6
2026-01-14 14:30:49 +10:00
jc21
7b3c1fd061 Merge branch 'master' into develop 2026-01-14 13:47:51 +10:00
Jamie Curnow
ee42202348 Bump version 2026-01-14 13:34:17 +10:00
Jamie Curnow
c1ad7788f1 Changed 2fa delete from body to query for code
as per best practices
2026-01-14 13:24:38 +10:00
Jamie Curnow
d33bb02c74 Add missing params to swagger 2026-01-14 12:46:30 +10:00
Jamie Curnow
462c134751 2fa work slight refactor
- use existing access mechanisms for validation
- adds swagger/schema and validation of incoming payload
2026-01-14 11:45:12 +10:00
jc21
b7dfaddbb1 Merge pull request #4970 from zdzichu6969/develop
All checks were successful
Close stale issues and PRs / stale (push) Successful in 33s
Polish Translation Fixes
2026-01-14 07:33:49 +10:00
jc21
11ee4f0820 Merge pull request #4965 from archettitechnology/develop
Update Italian locale message for empty objects
2026-01-14 07:32:07 +10:00
jc21
19970a4220 Merge pull request #5095 from aindriu80/develop
feat: (i18n) Added Irish translation
2026-01-14 07:26:10 +10:00
jc21
59bac3b468 Merge pull request #5005 from NginxProxyManager/dependabot/npm_and_yarn/backend/express-4.22.0
Bump express from 4.21.2 to 4.22.0 in /backend
2026-01-13 23:35:27 +10:00
jc21
48753fb101 Merge pull request #5136 from NginxProxyManager/dependabot/npm_and_yarn/docs/mdast-util-to-hast-13.2.1
Bump mdast-util-to-hast from 13.2.0 to 13.2.1 in /docs
2026-01-13 23:35:13 +10:00
dependabot[bot]
2a3978ae3f Bump mdast-util-to-hast from 13.2.0 to 13.2.1 in /docs
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:28:52 +00:00
dependabot[bot]
4ce5da5930 Bump express from 4.21.2 to 4.22.0 in /backend
Bumps [express](https://github.com/expressjs/express) from 4.21.2 to 4.22.0.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.22.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.2...4.22.0)

---
updated-dependencies:
- dependency-name: express
  dependency-version: 4.22.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-13 13:26:06 +00:00
jc21
89d3756ee6 Merge pull request #5118 from mobilandi/develop
Add DNS plugin for All-Inkl provider
2026-01-13 23:19:00 +10:00
Jamie Curnow
58c63096e4 Skip color output for vitest in ci 2026-01-13 22:55:19 +10:00
Jamie Curnow
b01a22c393 Fix frontend locale tests after date-fns changed intl formatting
and also attempt to format dates in locale
2026-01-13 22:42:42 +10:00
Jamie Curnow
9c25410331 Fix locale sort not to use sponge 2026-01-13 22:15:54 +10:00
jc21
b3a901bbc5 Merge pull request #5015 from NginxProxyManager/dependabot/npm_and_yarn/backend/jws-3.2.3
Bump jws from 3.2.2 to 3.2.3 in /backend
2026-01-13 15:18:41 +10:00
jc21
3e3396ba9a Update lang-list.json 2026-01-13 15:05:13 +10:00
jc21
3eb493bb8b Merge pull request #5022 from dupsatou/add-dns-plugin-support-he-ddns
Add Hurricane Electric DDNS plugin configuration
2026-01-13 14:53:51 +10:00
jc21
8c8221a352 Merge pull request #5037 from vtj-mizuno/fix-japanese-translate
Fix Japanese translate
2026-01-13 14:53:07 +10:00
jc21
582681e3ff Merge pull request #5080 from bzuro/develop
Change visibility to permission_visibility in report.js
2026-01-13 14:52:45 +10:00
jc21
52fae6d35f Merge pull request #5084 from lacamera/security/CVE-2025-55182
security: bump react to 19.2.3 to fix CVE-2025-55182 (#5020)
2026-01-13 14:50:39 +10:00
jc21
6c0ea835ce Merge branch 'develop' into develop 2026-01-13 14:46:35 +10:00
jc21
fb52655374 Merge pull request #5103 from CamelT0E/develop
Update German locale message from 'German' to 'Deutsch'
2026-01-13 14:43:42 +10:00
Jamie Curnow
336726db8d Backend yarn lock updates 2026-01-13 14:40:10 +10:00
jc21
4a7853163e Merge pull request #5107 from teguh02/develop
feat(i18n): add Bahasa Indonesia translations and help documentation
2026-01-13 14:32:18 +10:00
jc21
b30f8e47e2 Merge pull request #5109 from piotrfx/develop
Add TOTP-based two-factor authentication
2026-01-13 14:30:48 +10:00
jc21
6fa30840be Merge pull request #5114 from Shotz5/develop
Added logging for streams based on port
2026-01-13 14:18:13 +10:00
jc21
05726aaab9 Merge pull request #5119 from manisto/develop
Added support for DNS challenges with Simply.com
2026-01-13 14:14:38 +10:00
jc21
f85bb79f13 Merge pull request #5121 from KalebCheng/feature/certificate-key-type-selection
Add option to select RSA or ECDSA key type when creating certificates
2026-01-13 14:13:22 +10:00
kk.cheng
471b62c7fe Add option to select RSA or ECDSA key type when creating certificates 2026-01-07 19:13:12 +08:00
Gert Rue Brigsted
55a1e0a4e7 Added support for DNS challenges with Simply.com 2026-01-04 21:50:47 +01:00
mobilandi
f25afa3590 Change version constraint for certbot-dns-kas 2026-01-03 23:08:34 +01:00
mobilandi
9211ba6d1a Add DNS plugin for All-Inkl provider 2026-01-03 23:06:25 +01:00
Alex Kitsul
aeb44244a7 Added logging for streams based on port 2025-12-30 21:44:29 -08:00
piotrfx
d2d204ab8e Trigger CI 2025-12-28 12:04:35 +01:00
piotrfx
427afa55b4 Add TOTP-based two-factor authentication
- Add 2FA setup, enable, disable, and backup code management
- Integrate 2FA challenge flow into login process
- Add frontend modal for 2FA configuration
- Support backup codes for account recovery
2025-12-28 11:58:30 +01:00
Teguh Rijanandi
bbe98a639a Add Indonesian locale and help docs 2025-12-27 22:35:17 +07:00
Aindriú Mac Giolla Eoin
f0c0b465d9 Removiving 0x200b - Zero width space 2025-12-20 17:53:05 +00:00
Aindriú Mac Giolla Eoin
6c2f6a9d39 Fixing plural/iolra issue 2025-12-19 11:43:18 +00:00
Aindriú Mac Giolla Eoin
2f6e3ad804 Added Irish translation 2025-12-18 18:21:14 +00:00
Francesco La Camera
5e6ead1eee security: bump react to 19.2.3 to fix CVE-2025-55182 (#5020) 2025-12-15 09:54:18 +01:00
bzuro
da519e72ba Change visibility to permission_visibility in report.js
fix for issue #2014
when even administrator with all_items visibility got 0 proxy hosts in dashboard.
2025-12-14 00:35:22 +01:00
Hajime MIZUNO
b13ebb2247 Fix Japanese translate 2025-12-10 23:28:53 +09:00
dupsatou
6b322582b9 Add Hurricane Electric DDNS plugin configuration
Add support for dns verification using Hurricane Electric DDNS credentials as a more secure way over account root credentials.  More information available here: https://github.com/mafredri/certbot-dns-he-ddns
2025-12-08 09:45:11 -06:00
angioletto
7fe5070337 Merge branch 'NginxProxyManager:develop' into develop 2025-12-06 14:56:52 +01:00
CamelT0E
1b8f1fbb79 Update German locale message from 'German' to 'Deutsch' 2025-12-06 01:30:56 +01:00
dependabot[bot]
4abea1247d Bump jws from 3.2.2 to 3.2.3 in /backend
Bumps [jws](https://github.com/brianloveswords/node-jws) from 3.2.2 to 3.2.3.
- [Release notes](https://github.com/brianloveswords/node-jws/releases)
- [Changelog](https://github.com/auth0/node-jws/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianloveswords/node-jws/compare/v3.2.2...v3.2.3)

---
updated-dependencies:
- dependency-name: jws
  dependency-version: 3.2.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-04 16:58:07 +00:00
Mateusz Gruszczyński
073ee95e56 change 2025-12-02 12:57:09 +01:00
Mateusz Gruszczyński
168078eb40 changes 2025-11-26 10:54:30 +01:00
Mateusz Gruszczyński
2c9f8f4d64 changes 2025-11-26 10:50:41 +01:00
Mateusz Gruszczyński
8403a0c761 changes 2025-11-26 10:42:48 +01:00
angioletto
927e57257b Merge branch 'NginxProxyManager:develop' into develop 2025-11-21 17:03:47 +01:00
Mateusz Gruszczyński
56875bba52 pretty :) 2025-11-19 21:23:23 +01:00
Mateusz Gruszczyński
b55f51bd63 fixes1 in pl 2025-11-19 15:10:56 +01:00
Mateusz Gruszczyński
86b7394620 fixes1 2025-11-19 11:01:25 +01:00
Mateusz Gruszczyński
91a1f39c02 fixes1 2025-11-19 10:53:55 +01:00
angioletto
5c114e9db7 Update Italian locale message for empty objects
Wrong translation of line 431
2025-11-19 09:56:05 +01:00
Mateusz Gruszczyński
fec9bffe29 fixes1 2025-11-19 09:13:55 +01:00
jc21
847c58b170 Merge pull request #4956 from NginxProxyManager/develop
v2.13.5
2025-11-18 21:13:24 +10:00
84 changed files with 4691 additions and 1243 deletions

View File

@@ -1 +1 @@
2.13.5
2.13.6

View File

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

View File

@@ -295,6 +295,14 @@
"credentials": "dns_he_user = Me\ndns_he_pass = my HE password",
"full_plugin_name": "dns-he"
},
"he-ddns": {
"name": "Hurricane Electric - DDNS",
"package_name": "certbot-dns-he-ddns",
"version": "~=0.1.0",
"dependencies": "",
"credentials": "dns_he_ddns_password = verysecurepassword",
"full_plugin_name": "dns-he-ddns"
},
"hetzner": {
"name": "Hetzner",
"package_name": "certbot-dns-hetzner",
@@ -375,6 +383,14 @@
"credentials": "dns_joker_username = <Dynamic DNS Authentication Username>\ndns_joker_password = <Dynamic DNS Authentication Password>\ndns_joker_domain = <Dynamic DNS Domain>",
"full_plugin_name": "dns-joker"
},
"kas": {
"name": "All-Inkl",
"package_name": "certbot-dns-kas",
"version": "~=0.1.1",
"dependencies": "kasserver",
"credentials": "dns_kas_user = your_kas_user\ndns_kas_password = your_kas_password",
"full_plugin_name": "dns-kas"
},
"leaseweb": {
"name": "LeaseWeb",
"package_name": "certbot-dns-leaseweb",
@@ -535,6 +551,14 @@
"credentials": "[default]\naws_access_key_id=AKIAIOSFODNN7EXAMPLE\naws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"full_plugin_name": "dns-route53"
},
"simply": {
"name": "Simply",
"package_name": "certbot-dns-simply",
"version": "~=0.1.2",
"dependencies": "",
"credentials": "dns_simply_account_name = UExxxxxx\ndns_simply_api_key = DsHJdsjh2812872sahj",
"full_plugin_name": "dns-simply"
},
"spaceship": {
"name": "Spaceship",
"package_name": "certbot-dns-spaceship",

288
backend/internal/2fa.js Normal file
View File

@@ -0,0 +1,288 @@
import crypto from "node:crypto";
import bcrypt from "bcrypt";
import { authenticator } from "otplib";
import errs from "../lib/error.js";
import authModel from "../models/auth.js";
import internalUser from "./user.js";
const APP_NAME = "Nginx Proxy Manager";
const BACKUP_CODE_COUNT = 8;
/**
* Generate backup codes
* @returns {Promise<{plain: string[], hashed: string[]}>}
*/
const generateBackupCodes = async () => {
const plain = [];
const hashed = [];
for (let i = 0; i < BACKUP_CODE_COUNT; i++) {
const code = crypto.randomBytes(4).toString("hex").toUpperCase();
plain.push(code);
const hash = await bcrypt.hash(code, 10);
hashed.push(hash);
}
return { plain, hashed };
};
const internal2fa = {
/**
* Check if user has 2FA enabled
* @param {number} userId
* @returns {Promise<boolean>}
*/
isEnabled: async (userId) => {
const auth = await internal2fa.getUserPasswordAuth(userId);
return auth?.meta?.totp_enabled === true;
},
/**
* Get 2FA status for user
* @param {Access} access
* @param {number} userId
* @returns {Promise<{enabled: boolean, backup_codes_remaining: number}>}
*/
getStatus: async (access, userId) => {
await access.can("users:password", userId);
await internalUser.get(access, { id: userId });
const auth = await internal2fa.getUserPasswordAuth(userId);
const enabled = auth?.meta?.totp_enabled === true;
let backup_codes_remaining = 0;
if (enabled) {
const backupCodes = auth.meta.backup_codes || [];
backup_codes_remaining = backupCodes.length;
}
return {
enabled,
backup_codes_remaining,
};
},
/**
* Start 2FA setup - store pending secret
*
* @param {Access} access
* @param {number} userId
* @returns {Promise<{secret: string, otpauth_url: string}>}
*/
startSetup: async (access, userId) => {
await access.can("users:password", userId);
const user = await internalUser.get(access, { id: userId });
const secret = authenticator.generateSecret();
const otpauth_url = authenticator.keyuri(user.email, APP_NAME, secret);
const auth = await internal2fa.getUserPasswordAuth(userId);
// ensure user isn't already setup for 2fa
const enabled = auth?.meta?.totp_enabled === true;
if (enabled) {
throw new errs.ValidationError("2FA is already enabled");
}
const meta = auth.meta || {};
meta.totp_pending_secret = secret;
await authModel.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
return { secret, otpauth_url };
},
/**
* Enable 2FA after verifying code
*
* @param {Access} access
* @param {number} userId
* @param {string} code
* @returns {Promise<{backup_codes: string[]}>}
*/
enable: async (access, userId, code) => {
await access.can("users:password", userId);
await internalUser.get(access, { id: userId });
const auth = await internal2fa.getUserPasswordAuth(userId);
const secret = auth?.meta?.totp_pending_secret || false;
if (!secret) {
throw new errs.ValidationError("No pending 2FA setup found");
}
const valid = authenticator.verify({ token: code, secret });
if (!valid) {
throw new errs.ValidationError("Invalid verification code");
}
const { plain, hashed } = await generateBackupCodes();
const meta = {
...auth.meta,
totp_secret: secret,
totp_enabled: true,
totp_enabled_at: new Date().toISOString(),
backup_codes: hashed,
};
delete meta.totp_pending_secret;
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
return { backup_codes: plain };
},
/**
* Disable 2FA
*
* @param {Access} access
* @param {number} userId
* @param {string} code
* @returns {Promise<void>}
*/
disable: async (access, userId, code) => {
await access.can("users:password", userId);
await internalUser.get(access, { id: userId });
const auth = await internal2fa.getUserPasswordAuth(userId);
const enabled = auth?.meta?.totp_enabled === true;
if (!enabled) {
throw new errs.ValidationError("2FA is not enabled");
}
const valid = authenticator.verify({
token: code,
secret: auth.meta.totp_secret,
});
if (!valid) {
throw new errs.AuthError("Invalid verification code");
}
const meta = { ...auth.meta };
delete meta.totp_secret;
delete meta.totp_enabled;
delete meta.totp_enabled_at;
delete meta.backup_codes;
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
},
/**
* Verify 2FA code for login
*
* @param {number} userId
* @param {string} token
* @returns {Promise<boolean>}
*/
verifyForLogin: async (userId, token) => {
const auth = await internal2fa.getUserPasswordAuth(userId);
const secret = auth?.meta?.totp_secret || false;
if (!secret) {
return false;
}
// Try TOTP code first
const valid = authenticator.verify({
token,
secret,
});
if (valid) {
return true;
}
// Try backup codes
const backupCodes = auth?.meta?.backup_codes || [];
for (let i = 0; i < backupCodes.length; i++) {
const match = await bcrypt.compare(code.toUpperCase(), backupCodes[i]);
if (match) {
// Remove used backup code
const updatedCodes = [...backupCodes];
updatedCodes.splice(i, 1);
const meta = { ...auth.meta, backup_codes: updatedCodes };
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
return true;
}
}
return false;
},
/**
* Regenerate backup codes
*
* @param {Access} access
* @param {number} userId
* @param {string} token
* @returns {Promise<{backup_codes: string[]}>}
*/
regenerateBackupCodes: async (access, userId, token) => {
await access.can("users:password", userId);
await internalUser.get(access, { id: userId });
const auth = await internal2fa.getUserPasswordAuth(userId);
const enabled = auth?.meta?.totp_enabled === true;
const secret = auth?.meta?.totp_secret || false;
if (!enabled) {
throw new errs.ValidationError("2FA is not enabled");
}
if (!secret) {
throw new errs.ValidationError("No 2FA secret found");
}
const valid = authenticator.verify({
token,
secret,
});
if (!valid) {
throw new errs.ValidationError("Invalid verification code");
}
const { plain, hashed } = await generateBackupCodes();
const meta = { ...auth.meta, backup_codes: hashed };
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
return { backup_codes: plain };
},
getUserPasswordAuth: async (userId) => {
const auth = await authModel
.query()
.where("user_id", userId)
.andWhere("type", "password")
.first();
if (!auth) {
throw new errs.ItemNotFoundError("Auth not found");
}
return auth;
},
};
export default internal2fa;

View File

@@ -798,6 +798,11 @@ const internalCertificate = {
certificate.domain_names.join(","),
];
// Add key-type parameter if specified
if (certificate.meta?.key_type) {
args.push("--key-type", certificate.meta.key_type);
}
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id);
args.push(...adds.args);
@@ -858,6 +863,11 @@ const internalCertificate = {
);
}
// Add key-type parameter if specified
if (certificate.meta?.key_type) {
args.push("--key-type", certificate.meta.key_type);
}
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);
args.push(...adds.args);
@@ -938,6 +948,11 @@ const internalCertificate = {
"--disable-hook-validation",
];
// Add key-type parameter if specified
if (certificate.meta?.key_type) {
args.push("--key-type", certificate.meta.key_type);
}
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);
args.push(...adds.args);
@@ -979,6 +994,11 @@ const internalCertificate = {
"--no-random-sleep-on-renew",
];
// Add key-type parameter if specified
if (certificate.meta?.key_type) {
args.push("--key-type", certificate.meta.key_type);
}
const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider);
args.push(...adds.args);

View File

@@ -15,10 +15,10 @@ const internalReport = {
const userId = access.token.getUserId(1);
const promises = [
internalProxyHost.getCount(userId, access_data.visibility),
internalRedirectionHost.getCount(userId, access_data.visibility),
internalStream.getCount(userId, access_data.visibility),
internalDeadHost.getCount(userId, access_data.visibility),
internalProxyHost.getCount(userId, access_data.permission_visibility),
internalRedirectionHost.getCount(userId, access_data.permission_visibility),
internalStream.getCount(userId, access_data.permission_visibility),
internalDeadHost.getCount(userId, access_data.permission_visibility),
];
return Promise.all(promises);

View File

@@ -4,9 +4,12 @@ import { parseDatePeriod } from "../lib/helpers.js";
import authModel from "../models/auth.js";
import TokenModel from "../models/token.js";
import userModel from "../models/user.js";
import twoFactor from "./2fa.js";
const ERROR_MESSAGE_INVALID_AUTH = "Invalid email or password";
const ERROR_MESSAGE_INVALID_AUTH_I18N = "error.invalid-auth";
const ERROR_MESSAGE_INVALID_2FA = "Invalid verification code";
const ERROR_MESSAGE_INVALID_2FA_I18N = "error.invalid-2fa";
export default {
/**
@@ -59,6 +62,25 @@ export default {
throw new errs.AuthError(`Invalid scope: ${data.scope}`);
}
// Check if 2FA is enabled
const has2FA = await twoFactor.isEnabled(user.id);
if (has2FA) {
// Return challenge token instead of full token
const challengeToken = await Token.create({
iss: issuer || "api",
attrs: {
id: user.id,
},
scope: ["2fa-challenge"],
expiresIn: "5m",
});
return {
requires_2fa: true,
challenge_token: challengeToken.token,
};
}
// Create a moment of the expiry expression
const expiry = parseDatePeriod(data.expiry);
if (expiry === null) {
@@ -129,6 +151,65 @@ export default {
throw new error.AssertionFailedError("Existing token contained invalid user data");
},
/**
* Verify 2FA code and return full token
* @param {string} challengeToken
* @param {string} code
* @param {string} [expiry]
* @returns {Promise}
*/
verify2FA: async (challengeToken, code, expiry) => {
const Token = TokenModel();
const tokenExpiry = expiry || "1d";
// Verify challenge token
let tokenData;
try {
tokenData = await Token.load(challengeToken);
} catch {
throw new errs.AuthError("Invalid or expired challenge token");
}
// Check scope
if (!tokenData.scope || tokenData.scope[0] !== "2fa-challenge") {
throw new errs.AuthError("Invalid challenge token");
}
const userId = tokenData.attrs?.id;
if (!userId) {
throw new errs.AuthError("Invalid challenge token");
}
// Verify 2FA code
const valid = await twoFactor.verifyForLogin(userId, code);
if (!valid) {
throw new errs.AuthError(
ERROR_MESSAGE_INVALID_2FA,
ERROR_MESSAGE_INVALID_2FA_I18N,
);
}
// Create full token
const expiryDate = parseDatePeriod(tokenExpiry);
if (expiryDate === null) {
throw new errs.AuthError(`Invalid expiry time: ${tokenExpiry}`);
}
const signed = await Token.create({
iss: "api",
attrs: {
id: userId,
},
scope: ["user"],
expiresIn: tokenExpiry,
});
return {
token: signed.token,
expires: expiryDate.toISOString(),
};
},
/**
* @param {Object} user
* @returns {Promise}

View File

@@ -19,7 +19,7 @@
"bcrypt": "^5.0.0",
"body-parser": "^1.20.3",
"compression": "^1.7.4",
"express": "^4.20.0",
"express": "^4.22.0",
"express-fileupload": "^1.5.2",
"gravatar": "^1.8.2",
"jsonwebtoken": "^9.0.2",
@@ -30,6 +30,7 @@
"mysql2": "^3.15.3",
"node-rsa": "^1.1.1",
"objection": "3.0.1",
"otplib": "^12.0.1",
"path": "^0.12.7",
"pg": "^8.16.3",
"proxy-agent": "^6.5.0",

View File

@@ -53,4 +53,26 @@ router
}
});
router
.route("/2fa")
.options((_, res) => {
res.sendStatus(204);
})
/**
* POST /tokens/2fa
*
* Verify 2FA code and get full token
*/
.post(async (req, res, next) => {
try {
const { challenge_token, code } = await apiValidator(getValidationSchema("/tokens/2fa", "post"), req.body);
const result = await internalToken.verify2FA(challenge_token, code);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;

View File

@@ -1,4 +1,5 @@
import express from "express";
import internal2FA from "../internal/2fa.js";
import internalUser from "../internal/user.js";
import Access from "../lib/access.js";
import { isCI } from "../lib/config.js";
@@ -325,4 +326,130 @@ router
}
});
/**
* User 2FA status
*
* /api/users/123/2fa
*/
router
.route("/:user_id/2fa")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* POST /api/users/123/2fa
*
* Start 2FA setup, returns QR code URL
*/
.post(async (req, res, next) => {
try {
const result = await internal2FA.startSetup(res.locals.access, req.params.user_id);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* GET /api/users/123/2fa
*
* Get 2FA status for a user
*/
.get(async (req, res, next) => {
try {
const status = await internal2FA.getStatus(res.locals.access, req.params.user_id);
res.status(200).send(status);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/users/123/2fa?code=XXXXXX
*
* Disable 2FA for a user
*/
.delete(async (req, res, next) => {
try {
const code = typeof req.query.code === "string" ? req.query.code : null;
if (!code) {
throw new errs.ValidationError("Missing required parameter: code");
}
await internal2FA.disable(res.locals.access, req.params.user_id, code);
res.status(200).send(true);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* User 2FA enable
*
* /api/users/123/2fa/enable
*/
router
.route("/:user_id/2fa/enable")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* POST /api/users/123/2fa/enable
*
* Verify code and enable 2FA
*/
.post(async (req, res, next) => {
try {
const { code } = await apiValidator(
getValidationSchema("/users/{userID}/2fa/enable", "post"),
req.body,
);
const result = await internal2FA.enable(res.locals.access, req.params.user_id, code);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* User 2FA backup codes
*
* /api/users/123/2fa/backup-codes
*/
router
.route("/:user_id/2fa/backup-codes")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* POST /api/users/123/2fa/backup-codes
*
* Regenerate backup codes
*/
.post(async (req, res, next) => {
try {
const { code } = await apiValidator(
getValidationSchema("/users/{userID}/2fa/backup-codes", "post"),
req.body,
);
const result = await internal2FA.regenerateBackupCodes(res.locals.access, req.params.user_id, code);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;

View File

@@ -71,6 +71,11 @@
"propagation_seconds": {
"type": "integer",
"minimum": 0
},
"key_type": {
"type": "string",
"enum": ["rsa", "ecdsa"],
"default": "rsa"
}
},
"example": {

View File

@@ -0,0 +1,18 @@
{
"type": "object",
"description": "Token object",
"required": ["requires_2fa", "challenge_token"],
"additionalProperties": false,
"properties": {
"requires_2fa": {
"description": "Whether this token request requires two-factor authentication",
"example": true,
"type": "boolean"
},
"challenge_token": {
"description": "Challenge Token used in subsequent 2FA verification",
"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
"type": "string"
}
}
}

View File

@@ -0,0 +1,55 @@
{
"operationId": "loginWith2FA",
"summary": "Verify 2FA code and get full token",
"tags": ["tokens"],
"requestBody": {
"description": "2fa Challenge Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"challenge_token": {
"minLength": 1,
"type": "string",
"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
},
"code": {
"minLength": 6,
"maxLength": 6,
"type": "string",
"example": "012345"
}
},
"required": ["challenge_token", "code"],
"type": "object"
},
"example": {
"challenge_token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
"code": "012345"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"expires": "2025-02-04T20:40:46.340Z",
"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
}
}
},
"schema": {
"$ref": "../../../components/token-object.json"
}
}
},
"description": "200 response"
}
}
}

View File

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

View File

@@ -0,0 +1,92 @@
{
"operationId": "regenUser2faCodes",
"summary": "Regenerate 2FA backup codes",
"tags": ["users"],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
}
],
"requestBody": {
"description": "Verififcation Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"code": {
"minLength": 6,
"maxLength": 6,
"type": "string",
"example": "123456"
}
},
"required": ["code"],
"type": "object"
},
"example": {
"code": "123456"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"backup_codes": [
"6CD7CB06",
"495302F3",
"D8037852",
"A6FFC956",
"BC1A1851",
"A05E644F",
"A406D2E8",
"0AE3C522"
]
}
}
},
"schema": {
"type": "object",
"required": ["backup_codes"],
"additionalProperties": false,
"properties": {
"backup_codes": {
"description": "Backup codes",
"example": [
"6CD7CB06",
"495302F3",
"D8037852",
"A6FFC956",
"BC1A1851",
"A05E644F",
"A406D2E8",
"0AE3C522"
],
"type": "array",
"items": {
"type": "string",
"example": "6CD7CB06"
}
}
}
}
}
},
"description": "200 response"
}
}
}

View File

@@ -0,0 +1,48 @@
{
"operationId": "disableUser2fa",
"summary": "Disable 2fa for user",
"tags": ["users"],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
},
{
"in": "query",
"name": "code",
"schema": {
"type": "string",
"minLength": 6,
"maxLength": 6,
"example": "012345"
},
"required": true,
"description": "2fa Code",
"example": "012345"
}
],
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
},
"description": "200 response"
}
}
}

View File

@@ -0,0 +1,92 @@
{
"operationId": "enableUser2fa",
"summary": "Verify code and enable 2FA",
"tags": ["users"],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
}
],
"requestBody": {
"description": "Verififcation Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"code": {
"minLength": 6,
"maxLength": 6,
"type": "string",
"example": "123456"
}
},
"required": ["code"],
"type": "object"
},
"example": {
"code": "123456"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"backup_codes": [
"6CD7CB06",
"495302F3",
"D8037852",
"A6FFC956",
"BC1A1851",
"A05E644F",
"A406D2E8",
"0AE3C522"
]
}
}
},
"schema": {
"type": "object",
"required": ["backup_codes"],
"additionalProperties": false,
"properties": {
"backup_codes": {
"description": "Backup codes",
"example": [
"6CD7CB06",
"495302F3",
"D8037852",
"A6FFC956",
"BC1A1851",
"A05E644F",
"A406D2E8",
"0AE3C522"
],
"type": "array",
"items": {
"type": "string",
"example": "6CD7CB06"
}
}
}
}
}
},
"description": "200 response"
}
}
}

View File

@@ -0,0 +1,57 @@
{
"operationId": "getUser2faStatus",
"summary": "Get user 2fa Status",
"tags": ["users"],
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"enabled": false,
"backup_codes_remaining": 0
}
}
},
"schema": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "backup_codes_remaining"],
"properties": {
"enabled": {
"type": "boolean",
"description": "Is 2FA enabled for this user",
"example": true
},
"backup_codes_remaining": {
"type": "integer",
"description": "Number of remaining backup codes for this user",
"example": 5
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,52 @@
{
"operationId": "setupUser2fa",
"summary": "Start 2FA setup, returns QR code URL",
"tags": ["users"],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
}
],
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"secret": "JZYCEBIEEJYUGPQM",
"otpauth_url": "otpauth://totp/Nginx%20Proxy%20Manager:jc%40jc21.com?secret=JZYCEBIEEJYUGPQM&period=30&digits=6&algorithm=SHA1&issuer=Nginx%20Proxy%20Manager"
}
}
},
"schema": {
"type": "object",
"required": ["secret", "otpauth_url"],
"additionalProperties": false,
"properties": {
"secret": {
"description": "TOTP Secret",
"example": "JZYCEBIEEJYUGPQM",
"type": "string"
},
"otpauth_url": {
"description": "OTP Auth URL for QR Code generation",
"example": "otpauth://totp/Nginx%20Proxy%20Manager:jc%40jc21.com?secret=JZYCEBIEEJYUGPQM&period=30&digits=6&algorithm=SHA1&issuer=Nginx%20Proxy%20Manager",
"type": "string"
}
}
}
}
},
"description": "200 response"
}
}
}

View File

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

@@ -12,6 +12,9 @@ server {
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
access_log /data/logs/stream-{{ id }}_access.log stream;
error_log /data/logs/stream-{{ id }}_error.log warn;
# Custom
include /data/nginx/custom/server_stream[.]conf;
include /data/nginx/custom/server_stream_tcp[.]conf;
@@ -25,9 +28,12 @@ server {
proxy_pass {{ forwarding_host }}:{{ forwarding_port }};
access_log /data/logs/stream-{{ id }}_access.log stream;
error_log /data/logs/stream-{{ id }}_error.log warn;
# Custom
include /data/nginx/custom/server_stream[.]conf;
include /data/nginx/custom/server_stream_udp[.]conf;
}
{% endif %}
{% endif %}
{% endif %}

View File

@@ -138,6 +138,44 @@
mkdirp "^1.0.4"
rimraf "^3.0.2"
"@otplib/core@^12.0.1":
version "12.0.1"
resolved "https://registry.yarnpkg.com/@otplib/core/-/core-12.0.1.tgz#73720a8cedce211fe5b3f683cd5a9c098eaf0f8d"
integrity sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==
"@otplib/plugin-crypto@^12.0.1":
version "12.0.1"
resolved "https://registry.yarnpkg.com/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz#2b42c624227f4f9303c1c041fca399eddcbae25e"
integrity sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==
dependencies:
"@otplib/core" "^12.0.1"
"@otplib/plugin-thirty-two@^12.0.1":
version "12.0.1"
resolved "https://registry.yarnpkg.com/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz#5cc9b56e6e89f2a1fe4a2b38900ca4e11c87aa9e"
integrity sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==
dependencies:
"@otplib/core" "^12.0.1"
thirty-two "^1.0.2"
"@otplib/preset-default@^12.0.1":
version "12.0.1"
resolved "https://registry.yarnpkg.com/@otplib/preset-default/-/preset-default-12.0.1.tgz#cb596553c08251e71b187ada4a2246ad2a3165ba"
integrity sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==
dependencies:
"@otplib/core" "^12.0.1"
"@otplib/plugin-crypto" "^12.0.1"
"@otplib/plugin-thirty-two" "^12.0.1"
"@otplib/preset-v11@^12.0.1":
version "12.0.1"
resolved "https://registry.yarnpkg.com/@otplib/preset-v11/-/preset-v11-12.0.1.tgz#4c7266712e7230500b421ba89252963c838fc96d"
integrity sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==
dependencies:
"@otplib/core" "^12.0.1"
"@otplib/plugin-crypto" "^12.0.1"
"@otplib/plugin-thirty-two" "^12.0.1"
"@tootallnate/once@1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@@ -389,23 +427,23 @@ blueimp-md5@^2.16.0:
resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.19.0.tgz#b53feea5498dcb53dc6ec4b823adb84b729c4af0"
integrity sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==
body-parser@1.20.3, body-parser@^1.20.3:
version "1.20.3"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6"
integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==
body-parser@^1.20.3, body-parser@~1.20.3:
version "1.20.4"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.4.tgz#f8e20f4d06ca8a50a71ed329c15dccad1cdc547f"
integrity sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==
dependencies:
bytes "3.1.2"
bytes "~3.1.2"
content-type "~1.0.5"
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
http-errors "2.0.0"
iconv-lite "0.4.24"
on-finished "2.4.1"
qs "6.13.0"
raw-body "2.5.2"
destroy "~1.2.0"
http-errors "~2.0.1"
iconv-lite "~0.4.24"
on-finished "~2.4.1"
qs "~6.14.0"
raw-body "~2.5.3"
type-is "~1.6.18"
unpipe "1.0.0"
unpipe "~1.0.0"
brace-expansion@^1.1.7:
version "1.1.12"
@@ -454,7 +492,7 @@ busboy@^1.6.0:
dependencies:
streamsearch "^1.1.0"
bytes@3.1.2:
bytes@3.1.2, bytes@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
@@ -649,7 +687,7 @@ console-control-strings@^1.0.0, console-control-strings@^1.1.0:
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
content-disposition@0.5.4:
content-disposition@~0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
@@ -661,15 +699,15 @@ content-type@~1.0.4, content-type@~1.0.5:
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
cookie-signature@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.7.tgz#ab5dd7ab757c54e60f37ef6550f481c426d10454"
integrity sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==
cookie@0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9"
integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==
cookie@~0.7.1:
version "0.7.2"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
core-util-is@~1.0.0:
version "1.0.3"
@@ -706,10 +744,10 @@ debug@2.6.9:
dependencies:
ms "2.0.0"
debug@4, debug@^4.3.3:
version "4.4.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
debug@4, debug@^4.3.3, debug@^4.3.4:
version "4.4.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
dependencies:
ms "^2.1.3"
@@ -727,13 +765,6 @@ debug@^3.2.7:
dependencies:
ms "^2.1.1"
debug@^4.3.4:
version "4.4.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
dependencies:
ms "^2.1.3"
decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -770,12 +801,12 @@ denque@^2.1.0:
resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
depd@2.0.0:
depd@2.0.0, depd@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
destroy@1.2.0:
destroy@1.2.0, destroy@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
@@ -816,11 +847,6 @@ emoji-regex@^8.0.0:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
encodeurl@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
@@ -937,39 +963,39 @@ express-fileupload@^1.5.2:
dependencies:
busboy "^1.6.0"
express@^4.20.0:
version "4.21.2"
resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32"
integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==
express@^4.22.0:
version "4.22.0"
resolved "https://registry.yarnpkg.com/express/-/express-4.22.0.tgz#a9d7abdce6d774ed1b4479019387763d1798bd03"
integrity sha512-c2iPh3xp5vvCLgaHK03+mWLFPhox7j1LwyxcZwFVApEv5i0X+IjPpbT50SJJwwLpdBVfp45AkK/v+AFgv/XlfQ==
dependencies:
accepts "~1.3.8"
array-flatten "1.1.1"
body-parser "1.20.3"
content-disposition "0.5.4"
body-parser "~1.20.3"
content-disposition "~0.5.4"
content-type "~1.0.4"
cookie "0.7.1"
cookie-signature "1.0.6"
cookie "~0.7.1"
cookie-signature "~1.0.6"
debug "2.6.9"
depd "2.0.0"
encodeurl "~2.0.0"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "1.3.1"
fresh "0.5.2"
http-errors "2.0.0"
finalhandler "~1.3.1"
fresh "~0.5.2"
http-errors "~2.0.0"
merge-descriptors "1.0.3"
methods "~1.1.2"
on-finished "2.4.1"
on-finished "~2.4.1"
parseurl "~1.3.3"
path-to-regexp "0.1.12"
path-to-regexp "~0.1.12"
proxy-addr "~2.0.7"
qs "6.13.0"
qs "~6.14.0"
range-parser "~1.2.1"
safe-buffer "5.2.1"
send "0.19.0"
serve-static "1.16.2"
send "~0.19.0"
serve-static "~1.16.2"
setprototypeof "1.2.0"
statuses "2.0.1"
statuses "~2.0.1"
type-is "~1.6.18"
utils-merge "1.0.1"
vary "~1.1.2"
@@ -1003,17 +1029,17 @@ fill-range@^7.1.1:
dependencies:
to-regex-range "^5.0.1"
finalhandler@1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019"
integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==
finalhandler@~1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.2.tgz#1ebc2228fc7673aac4a472c310cc05b77d852b88"
integrity sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==
dependencies:
debug "2.6.9"
encodeurl "~2.0.0"
escape-html "~1.0.3"
on-finished "2.4.1"
on-finished "~2.4.1"
parseurl "~1.3.3"
statuses "2.0.1"
statuses "~2.0.2"
unpipe "~1.0.0"
find-up@^2.0.0:
@@ -1036,7 +1062,7 @@ forwarded@0.2.0:
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
fresh@0.5.2:
fresh@~0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
@@ -1228,16 +1254,16 @@ http-cache-semantics@^4.1.0:
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz#205f4db64f8562b76a4ff9235aa5279839a09dd5"
integrity sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==
http-errors@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
http-errors@~2.0.0, http-errors@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b"
integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==
dependencies:
depd "2.0.0"
inherits "2.0.4"
setprototypeof "1.2.0"
statuses "2.0.1"
toidentifier "1.0.1"
depd "~2.0.0"
inherits "~2.0.4"
setprototypeof "~1.2.0"
statuses "~2.0.2"
toidentifier "~1.0.1"
http-proxy-agent@^4.0.1:
version "4.0.1"
@@ -1279,13 +1305,6 @@ humanize-ms@^1.2.1:
dependencies:
ms "^2.0.0"
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
iconv-lite@^0.6.2:
version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
@@ -1300,6 +1319,13 @@ iconv-lite@^0.7.0:
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
iconv-lite@~0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
ieee754@^1.1.13:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@@ -1333,7 +1359,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -1462,7 +1488,7 @@ jsonwebtoken@^9.0.2:
ms "^2.1.1"
semver "^7.5.4"
jwa@^1.4.1:
jwa@^1.4.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9"
integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==
@@ -1472,11 +1498,11 @@ jwa@^1.4.1:
safe-buffer "^5.0.1"
jws@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
version "3.2.3"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.3.tgz#5ac0690b460900a27265de24520526853c0b8ca1"
integrity sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==
dependencies:
jwa "^1.4.1"
jwa "^1.4.2"
safe-buffer "^5.0.1"
knex@2.4.2:
@@ -1959,7 +1985,7 @@ objection@3.0.1:
ajv "^8.6.2"
db-errors "^0.2.3"
on-finished@2.4.1:
on-finished@~2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
@@ -1978,6 +2004,15 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
dependencies:
wrappy "1"
otplib@^12.0.1:
version "12.0.1"
resolved "https://registry.yarnpkg.com/otplib/-/otplib-12.0.1.tgz#c1d3060ab7aadf041ed2960302f27095777d1f73"
integrity sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==
dependencies:
"@otplib/core" "^12.0.1"
"@otplib/preset-default" "^12.0.1"
"@otplib/preset-v11" "^12.0.1"
p-limit@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
@@ -2078,7 +2113,7 @@ path-parse@^1.0.7:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-to-regexp@0.1.12:
path-to-regexp@~0.1.12:
version "0.1.12"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==
@@ -2273,12 +2308,12 @@ pump@^3.0.0:
end-of-stream "^1.1.0"
once "^1.3.1"
qs@6.13.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
qs@~6.14.0:
version "6.14.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.1.tgz#a41d85b9d3902f31d27861790506294881871159"
integrity sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==
dependencies:
side-channel "^1.0.6"
side-channel "^1.1.0"
querystring@0.2.0:
version "0.2.0"
@@ -2290,15 +2325,15 @@ range-parser@~1.2.1:
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
raw-body@~2.5.3:
version "2.5.3"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.3.tgz#11c6650ee770a7de1b494f197927de0c923822e2"
integrity sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==
dependencies:
bytes "3.1.2"
http-errors "2.0.0"
iconv-lite "0.4.24"
unpipe "1.0.0"
bytes "~3.1.2"
http-errors "~2.0.1"
iconv-lite "~0.4.24"
unpipe "~1.0.0"
rc@^1.2.7:
version "1.2.8"
@@ -2429,46 +2464,46 @@ semver@~7.0.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
send@0.19.0:
version "0.19.0"
resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8"
integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==
send@~0.19.0, send@~0.19.1:
version "0.19.2"
resolved "https://registry.yarnpkg.com/send/-/send-0.19.2.tgz#59bc0da1b4ea7ad42736fd642b1c4294e114ff29"
integrity sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==
dependencies:
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
encodeurl "~1.0.2"
encodeurl "~2.0.0"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "2.0.0"
fresh "~0.5.2"
http-errors "~2.0.1"
mime "1.6.0"
ms "2.1.3"
on-finished "2.4.1"
on-finished "~2.4.1"
range-parser "~1.2.1"
statuses "2.0.1"
statuses "~2.0.2"
seq-queue@^0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e"
integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==
serve-static@1.16.2:
version "1.16.2"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296"
integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==
serve-static@~1.16.2:
version "1.16.3"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.3.tgz#a97b74d955778583f3862a4f0b841eb4d5d78cf9"
integrity sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==
dependencies:
encodeurl "~2.0.0"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "0.19.0"
send "~0.19.1"
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
setprototypeof@1.2.0:
setprototypeof@1.2.0, setprototypeof@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
@@ -2502,7 +2537,7 @@ side-channel-weakmap@^1.0.2:
object-inspect "^1.13.3"
side-channel-map "^1.0.1"
side-channel@^1.0.6:
side-channel@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9"
integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==
@@ -2613,10 +2648,10 @@ ssri@^8.0.0, ssri@^8.0.1:
dependencies:
minipass "^3.1.1"
statuses@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
statuses@~2.0.1, statuses@~2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382"
integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==
streamsearch@^1.1.0:
version "1.1.0"
@@ -2736,6 +2771,11 @@ temp-write@^4.0.0:
temp-dir "^1.0.0"
uuid "^3.3.2"
thirty-two@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a"
integrity sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==
tildify@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a"
@@ -2748,7 +2788,7 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
toidentifier@1.0.1:
toidentifier@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
@@ -2802,7 +2842,7 @@ unique-slug@^2.0.0:
dependencies:
imurmurhash "^0.1.4"
unpipe@1.0.0, unpipe@~1.0.0:
unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==

View File

@@ -8,8 +8,8 @@ server {
set $port "80";
server_name localhost-nginx-proxy-manager;
access_log /data/logs/fallback_access.log standard;
error_log /data/logs/fallback_error.log warn;
access_log /data/logs/fallback_http_access.log standard;
error_log /data/logs/fallback_http_error.log warn;
include conf.d/include/assets.conf;
include conf.d/include/block-exploits.conf;
include conf.d/include/letsencrypt-acme-challenge.conf;
@@ -30,7 +30,7 @@ server {
set $port "443";
server_name localhost;
access_log /data/logs/fallback_access.log standard;
access_log /data/logs/fallback_http_access.log standard;
error_log /dev/null crit;
include conf.d/include/ssl-ciphers.conf;
ssl_reject_handshake on;

View File

@@ -1,4 +1,4 @@
log_format proxy '[$time_local] $upstream_cache_status $upstream_status $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] [Sent-to $server] "$http_user_agent" "$http_referer"';
log_format standard '[$time_local] $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] "$http_user_agent" "$http_referer"';
access_log /data/logs/fallback_access.log proxy;
access_log /data/logs/fallback_http_access.log proxy;

View File

@@ -0,0 +1,3 @@
log_format stream '[$time_local] [Client $remote_addr:$remote_port] $protocol $status $bytes_sent $bytes_received $session_time [Sent-to $upstream_addr] [Sent $upstream_bytes_sent] [Received $upstream_bytes_received] [Time $upstream_connect_time] $ssl_protocol $ssl_cipher';
access_log /data/logs/fallback_stream_access.log stream;

View File

@@ -47,7 +47,7 @@ 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.conf;
include /etc/nginx/conf.d/include/log-proxy.conf;
# Dynamically generated resolvers file
include /etc/nginx/conf.d/include/resolvers.conf;
@@ -85,6 +85,9 @@ http {
}
stream {
# Log format and fallback log file
include /etc/nginx/conf.d/include/log-stream.conf;
# Files generated by NPM
include /data/nginx/stream/*.conf;

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@ const allLocales = [
["zh", "zh-CN"],
["ko", "ko-KR"],
["bg", "bg-BG"],
["id", "id-ID"],
];
const ignoreUnused = [

View File

@@ -29,9 +29,9 @@
"generate-password-browser": "^1.1.0",
"humps": "^2.0.1",
"query-string": "^9.3.1",
"react": "^19.2.0",
"react": "^19.2.3",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.2.0",
"react-dom": "^19.2.3",
"react-intl": "^7.1.14",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.9.5",
@@ -48,10 +48,10 @@
"@testing-library/react": "^16.3.0",
"@types/country-flag-icons": "^1.2.2",
"@types/humps": "^2.0.6",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/react-table": "^7.7.20",
"@vitejs/plugin-react": "^5.1.0",
"@vitejs/plugin-react": "^5.1.2",
"happy-dom": "^20.0.10",
"postcss": "^8.5.6",
"postcss-simple-vars": "^7.0.1",

View File

@@ -156,7 +156,6 @@ 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

@@ -1,9 +1,22 @@
import * as api from "./base";
import type { TokenResponse } from "./responseTypes";
import type { TokenResponse, TwoFactorChallengeResponse } from "./responseTypes";
export async function getToken(identity: string, secret: string): Promise<TokenResponse> {
export type LoginResponse = TokenResponse | TwoFactorChallengeResponse;
export function isTwoFactorChallenge(response: LoginResponse): response is TwoFactorChallengeResponse {
return "requires2fa" in response && response.requires2fa === true;
}
export async function getToken(identity: string, secret: string): Promise<LoginResponse> {
return await api.post({
url: "/tokens",
data: { identity, secret },
});
}
export async function verify2FA(challengeToken: string, code: string): Promise<TokenResponse> {
return await api.post({
url: "/tokens/2fa",
data: { challengeToken, code },
});
}

View File

@@ -60,3 +60,4 @@ export * from "./updateStream";
export * from "./updateUser";
export * from "./uploadCertificate";
export * from "./validateCertificate";
export * from "./twoFactor";

View File

@@ -25,3 +25,22 @@ export interface VersionCheckResponse {
latest: string | null;
updateAvailable: boolean;
}
export interface TwoFactorChallengeResponse {
requires2fa: boolean;
challengeToken: string;
}
export interface TwoFactorStatusResponse {
enabled: boolean;
backupCodesRemaining: number;
}
export interface TwoFactorSetupResponse {
secret: string;
otpauthUrl: string;
}
export interface TwoFactorEnableResponse {
backupCodes: string[];
}

View File

@@ -0,0 +1,37 @@
import * as api from "./base";
import type { TwoFactorEnableResponse, TwoFactorSetupResponse, TwoFactorStatusResponse } from "./responseTypes";
export async function get2FAStatus(userId: number | "me"): Promise<TwoFactorStatusResponse> {
return await api.get({
url: `/users/${userId}/2fa`,
});
}
export async function start2FASetup(userId: number | "me"): Promise<TwoFactorSetupResponse> {
return await api.post({
url: `/users/${userId}/2fa`,
});
}
export async function enable2FA(userId: number | "me", code: string): Promise<TwoFactorEnableResponse> {
return await api.post({
url: `/users/${userId}/2fa/enable`,
data: { code },
});
}
export async function disable2FA(userId: number | "me", code: string): Promise<boolean> {
return await api.del({
url: `/users/${userId}/2fa`,
params: {
code,
},
});
}
export async function regenerateBackupCodes(userId: number | "me", code: string): Promise<TwoFactorEnableResponse> {
return await api.post({
url: `/users/${userId}/2fa/backup-codes`,
data: { code },
});
}

View File

@@ -3,6 +3,7 @@ import { Field, useFormikContext } from "formik";
import type { ReactNode } from "react";
import Select, { type ActionMeta, components, type OptionProps } from "react-select";
import type { AccessList } from "src/api/backend";
import { useLocaleState } from "src/context";
import { useAccessLists } from "src/hooks";
import { formatDateTime, intl, T } from "src/locale";
@@ -32,6 +33,7 @@ interface Props {
label?: string;
}
export function AccessField({ name = "accessListId", label = "access-list", id = "accessListId" }: Props) {
const { locale } = useLocaleState();
const { isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]);
const { setFieldValue } = useFormikContext();
@@ -48,7 +50,7 @@ export function AccessField({ name = "accessListId", label = "access-list", id =
{
users: item?.items?.length,
rules: item?.clients?.length,
date: item?.createdOn ? formatDateTime(item?.createdOn) : "N/A",
date: item?.createdOn ? formatDateTime(item?.createdOn, locale) : "N/A",
},
),
icon: <IconLock size={14} className="text-lime" />,

View File

@@ -2,6 +2,7 @@ import { IconShield } from "@tabler/icons-react";
import { Field, useFormikContext } from "formik";
import Select, { type ActionMeta, components, type OptionProps } from "react-select";
import type { Certificate } from "src/api/backend";
import { useLocaleState } from "src/context";
import { useCertificates } from "src/hooks";
import { formatDateTime, intl, T } from "src/locale";
@@ -41,6 +42,7 @@ export function SSLCertificateField({
allowNew,
forHttp = true,
}: Props) {
const { locale } = useLocaleState();
const { isLoading, isError, error, data } = useCertificates();
const { values, setFieldValue } = useFormikContext();
const v: any = values || {};
@@ -75,7 +77,7 @@ export function SSLCertificateField({
data?.map((cert: Certificate) => ({
value: cert.id,
label: cert.niceName,
subLabel: `${cert.provider === "letsencrypt" ? intl.formatMessage({ id: "lets-encrypt" }) : cert.provider} — ${intl.formatMessage({ id: "expires.on" }, { date: cert.expiresOn ? formatDateTime(cert.expiresOn) : "N/A" })}`,
subLabel: `${cert.provider === "letsencrypt" ? intl.formatMessage({ id: "lets-encrypt" }) : cert.provider} — ${intl.formatMessage({ id: "expires.on" }, { date: cert.expiresOn ? formatDateTime(cert.expiresOn, locale) : "N/A" })}`,
icon: <IconShield size={14} className="text-pink" />,
})) || [];

View File

@@ -1,9 +1,9 @@
import { IconLock, IconLogout, IconUser } from "@tabler/icons-react";
import { IconLock, IconLogout, IconShieldLock, IconUser } from "@tabler/icons-react";
import { LocalePicker, NavLink, ThemeSwitcher } from "src/components";
import { useAuthState } from "src/context";
import { useUser } from "src/hooks";
import { T } from "src/locale";
import { showChangePasswordModal, showUserModal } from "src/modals";
import { showChangePasswordModal, showTwoFactorModal, showUserModal } from "src/modals";
import styles from "./SiteHeader.module.css";
export function SiteHeader() {
@@ -108,6 +108,17 @@ export function SiteHeader() {
<IconLock width={18} />
<T id="user.change-password" />
</a>
<a
href="?"
className="dropdown-item"
onClick={(e) => {
e.preventDefault();
showTwoFactorModal("me");
}}
>
<IconShieldLock width={18} />
<T id="user.two-factor" />
</a>
<div className="dropdown-divider" />
<a
href="?"

View File

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

View File

@@ -1,5 +1,6 @@
import cn from "classnames";
import type { ReactNode } from "react";
import { useLocaleState } from "src/context";
import { formatDateTime, T } from "src/locale";
interface Props {
@@ -37,7 +38,9 @@ const DomainLink = ({ domain, color }: { domain?: string; color?: string }) => {
};
export function DomainsFormatter({ domains, createdOn, niceName, provider, color }: Props) {
const { locale } = useLocaleState();
const elms: ReactNode[] = [];
if ((!domains || domains.length === 0) && !niceName) {
elms.push(
<span key="nice-name" className="badge bg-danger-lt me-2">
@@ -62,7 +65,7 @@ export function DomainsFormatter({ domains, createdOn, niceName, provider, color
<div className="font-weight-medium">{...elms}</div>
{createdOn ? (
<div className="text-secondary mt-1">
<T id="created-on" data={{ date: formatDateTime(createdOn) }} />
<T id="created-on" data={{ date: formatDateTime(createdOn, locale) }} />
</div>
) : null}
</div>

View File

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

View File

@@ -1,3 +1,4 @@
import { useLocaleState } from "src/context";
import { formatDateTime, T } from "src/locale";
interface Props {
@@ -6,6 +7,7 @@ interface Props {
disabled?: boolean;
}
export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) {
const { locale } = useLocaleState();
return (
<div className="flex-fill">
<div className="font-weight-medium">
@@ -13,7 +15,7 @@ export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) {
</div>
{createdOn ? (
<div className={`text-secondary mt-1 ${disabled ? "text-red" : ""}`}>
<T id={disabled ? "disabled" : "created-on"} data={{ date: formatDateTime(createdOn) }} />
<T id={disabled ? "disabled" : "created-on"} data={{ date: formatDateTime(createdOn, locale) }} />
</div>
) : null}
</div>

View File

@@ -1,13 +1,28 @@
import { useQueryClient } from "@tanstack/react-query";
import { createContext, type ReactNode, useContext, useState } from "react";
import { useIntervalWhen } from "rooks";
import { getToken, loginAsUser, refreshToken, type TokenResponse } from "src/api/backend";
import {
getToken,
isTwoFactorChallenge,
loginAsUser,
refreshToken,
verify2FA,
type TokenResponse,
} from "src/api/backend";
import AuthStore from "src/modules/AuthStore";
// 2FA challenge state
export interface TwoFactorChallenge {
challengeToken: string;
}
// Context
export interface AuthContextType {
authenticated: boolean;
twoFactorChallenge: TwoFactorChallenge | null;
login: (username: string, password: string) => Promise<void>;
verifyTwoFactor: (code: string) => Promise<void>;
cancelTwoFactor: () => void;
loginAs: (id: number) => Promise<void>;
logout: () => void;
token?: string;
@@ -24,17 +39,35 @@ interface Props {
function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props) {
const queryClient = useQueryClient();
const [authenticated, setAuthenticated] = useState(AuthStore.hasActiveToken());
const [twoFactorChallenge, setTwoFactorChallenge] = useState<TwoFactorChallenge | null>(null);
const handleTokenUpdate = (response: TokenResponse) => {
AuthStore.set(response);
setAuthenticated(true);
setTwoFactorChallenge(null);
};
const login = async (identity: string, secret: string) => {
const response = await getToken(identity, secret);
if (isTwoFactorChallenge(response)) {
setTwoFactorChallenge({ challengeToken: response.challengeToken });
return;
}
handleTokenUpdate(response);
};
const verifyTwoFactor = async (code: string) => {
if (!twoFactorChallenge) {
throw new Error("No 2FA challenge pending");
}
const response = await verify2FA(twoFactorChallenge.challengeToken, code);
handleTokenUpdate(response);
};
const cancelTwoFactor = () => {
setTwoFactorChallenge(null);
};
const loginAs = async (id: number) => {
const response = await loginAsUser(id);
AuthStore.add(response);
@@ -69,7 +102,15 @@ function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props)
true,
);
const value = { authenticated, login, logout, loginAs };
const value = {
authenticated,
twoFactorChallenge,
login,
verifyTwoFactor,
cancelTwoFactor,
loginAs,
logout,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

View File

@@ -2,6 +2,7 @@ import { createIntl, createIntlCache } from "react-intl";
import langDe from "./lang/de.json";
import langEn from "./lang/en.json";
import langEs from "./lang/es.json";
import langGa from "./lang/ga.json";
import langIt from "./lang/it.json";
import langJa from "./lang/ja.json";
import langList from "./lang/lang-list.json";
@@ -13,6 +14,7 @@ import langVi from "./lang/vi.json";
import langZh from "./lang/zh.json";
import langKo from "./lang/ko.json";
import langBg from "./lang/bg.json";
import langId from "./lang/id.json";
// first item of each array should be the language code,
// not the country code
@@ -21,6 +23,7 @@ const localeOptions = [
["en", "en-US", langEn],
["de", "de-DE", langDe],
["es", "es-ES", langEs],
["ga", "ga-IE", langGa],
["ja", "ja-JP", langJa],
["it", "it-IT", langIt],
["nl", "nl-NL", langNl],
@@ -31,6 +34,7 @@ const localeOptions = [
["zh", "zh-CN", langZh],
["ko", "ko-KR", langKo],
["bg", "bg-BG", langBg],
["id", "id-ID", langId],
];
const loadMessages = (locale?: string): typeof langList & typeof langEn => {

View File

@@ -39,19 +39,19 @@ describe("DateFormatter", () => {
it("format date from iso date", () => {
const value = "2024-01-01T00:00:00.000Z";
const text = formatDateTime(value);
expect(text).toBe("Monday, 01/01/2024, 12:00:00 am");
expect(text).toBe("1 Jan 2024, 12:00:00 am");
});
it("format date from unix timestamp number", () => {
const value = 1762476112;
const text = formatDateTime(value);
expect(text).toBe("Friday, 07/11/2025, 12:41:52 am");
expect(text).toBe("7 Nov 2025, 12:41:52 am");
});
it("format date from unix timestamp string", () => {
const value = "1762476112";
const text = formatDateTime(value);
expect(text).toBe("Friday, 07/11/2025, 12:41:52 am");
expect(text).toBe("7 Nov 2025, 12:41:52 am");
});
it("catch bad format from string", () => {

View File

@@ -1,4 +1,9 @@
import { fromUnixTime, intlFormat, parseISO } from "date-fns";
import {
fromUnixTime,
type IntlFormatFormatOptions,
intlFormat,
parseISO,
} from "date-fns";
const isUnixTimestamp = (value: unknown): boolean => {
if (typeof value !== "number" && typeof value !== "string") return false;
@@ -20,20 +25,19 @@ const parseDate = (value: string | number): Date | null => {
}
};
const formatDateTime = (value: string | number): string => {
const formatDateTime = (value: string | number, locale = "en-US"): string => {
const d = parseDate(value);
if (!d) return `${value}`;
try {
return intlFormat(d, {
weekday: "long",
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: true,
});
return intlFormat(
d,
{
dateStyle: "medium",
timeStyle: "medium",
hourCycle: "h12",
} as IntlFormatFormatOptions,
{ locale },
);
} catch {
return `${value}`;
}

View File

@@ -31,6 +31,6 @@ for file in *.json; do
fi
echo "Sorting $file"
jq --tab --sort-keys . "$file" | sponge "$file"
tmp=$(mktemp) && jq --tab --sort-keys . "$file" > "$tmp" && mv "$tmp" "$file"
fi
done

View File

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

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

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

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

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

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

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

View File

@@ -0,0 +1,7 @@
## Apa itu Daftar Akses?
Daftar Akses menyediakan daftar hitam atau daftar putih alamat IP klien tertentu beserta autentikasi untuk Host Proxy melalui Autentikasi HTTP Basic.
Anda dapat mengonfigurasi beberapa aturan klien, nama pengguna, dan kata sandi untuk satu Daftar Akses lalu menerapkannya ke satu atau lebih _Host Proxy_.
Ini paling berguna untuk layanan web yang diteruskan yang tidak memiliki mekanisme autentikasi bawaan atau ketika Anda ingin melindungi dari klien yang tidak dikenal.

View File

@@ -0,0 +1,32 @@
## Bantuan Sertifikat
### Sertifikat HTTP
Sertifikat yang divalidasi HTTP berarti server Let's Encrypt akan
mencoba menjangkau domain Anda melalui HTTP (bukan HTTPS!) dan jika berhasil, mereka
akan menerbitkan sertifikat Anda.
Untuk metode ini, Anda harus membuat _Host Proxy_ untuk domain Anda yang
dapat diakses dengan HTTP dan mengarah ke instalasi Nginx ini. Setelah sertifikat
diberikan, Anda dapat mengubah _Host Proxy_ agar juga menggunakan sertifikat ini untuk HTTPS
koneksi. Namun, _Host Proxy_ tetap perlu dikonfigurasi untuk akses HTTP
agar sertifikat dapat diperpanjang.
Proses ini _tidak_ mendukung domain wildcard.
### Sertifikat DNS
Sertifikat yang divalidasi DNS mengharuskan Anda menggunakan plugin Penyedia DNS. Penyedia DNS ini
akan digunakan untuk membuat record sementara pada domain Anda dan kemudian Let's
Encrypt akan menanyakan record tersebut untuk memastikan Anda pemiliknya dan jika berhasil, mereka
akan menerbitkan sertifikat Anda.
Anda tidak perlu membuat _Host Proxy_ sebelum meminta jenis sertifikat ini.
Anda juga tidak perlu mengonfigurasi _Host Proxy_ untuk akses HTTP.
Proses ini _mendukung_ domain wildcard.
### Sertifikat Kustom
Gunakan opsi ini untuk mengunggah Sertifikat SSL Anda sendiri, sebagaimana disediakan oleh
Certificate Authority Anda.

View File

@@ -0,0 +1,10 @@
## Apa itu Host 404?
Host 404 adalah konfigurasi host yang menampilkan halaman 404.
Ini dapat berguna ketika domain Anda terindeks di mesin pencari dan Anda ingin
menyediakan halaman error yang lebih baik atau secara khusus memberi tahu pengindeks pencarian bahwa
halaman domain tersebut sudah tidak ada.
Manfaat lain memiliki host ini adalah melacak log untuk akses ke host tersebut dan
melihat perujuk.

View File

@@ -0,0 +1,7 @@
## Apa itu Host Proxy?
Host Proxy adalah endpoint masuk untuk layanan web yang ingin Anda teruskan.
Host ini menyediakan terminasi SSL opsional untuk layanan Anda yang mungkin tidak memiliki dukungan SSL bawaan.
Host Proxy adalah penggunaan paling umum untuk Nginx Proxy Manager.

View File

@@ -0,0 +1,5 @@
## Apa itu Host Pengalihan?
Host Pengalihan akan mengalihkan permintaan dari domain masuk dan mengarahkan pengunjung ke domain lain.
Alasan paling umum menggunakan jenis host ini adalah ketika situs Anda berpindah domain tetapi masih ada tautan mesin pencari atau perujuk yang mengarah ke domain lama.

View File

@@ -0,0 +1,6 @@
## Apa itu Stream?
Fitur yang relatif baru untuk Nginx, Stream berfungsi untuk meneruskan trafik TCP/UDP
langsung ke komputer lain di jaringan.
Jika Anda menjalankan server game, FTP, atau SSH, ini bisa sangat membantu.

View File

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

View File

@@ -1,5 +1,7 @@
import * as de from "./de/index";
import * as en from "./en/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 nl from "./nl/index";
@@ -11,7 +13,7 @@ import * as zh from "./zh/index";
import * as ko from "./ko/index";
import * as bg from "./bg/index";
const items: any = { en, de, ja, sk, zh, pl, ru, it, vi, nl, bg, ko };
const items: any = { en, de, ja, sk, zh, pl, ru, it, vi, nl, bg, ko, ga, id }
const fallbackLang = "en";

View File

@@ -170,6 +170,18 @@
"certificates.http.warning": {
"defaultMessage": "Тези домейни трябва вече да сочат към тази инсталация."
},
"certificates.key-type": {
"defaultMessage": "Тип ключ"
},
"certificates.key-type-description": {
"defaultMessage": "RSA е широко съвместим, ECDSA е по-бърз и по-сигурен, но може да не се поддържа от по-стари системи"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "с Let's Encrypt"
},

View File

@@ -155,6 +155,18 @@
"certificates.http.warning": {
"defaultMessage": "Diese Domänen müssen bereits so konfiguriert sein, dass sie auf diese Installation verweisen."
},
"certificates.key-type": {
"defaultMessage": "Schlüsseltyp"
},
"certificates.key-type-description": {
"defaultMessage": "RSA ist weit verbreitet, ECDSA ist schneller und sicherer, wird aber möglicherweise von älteren Systemen nicht unterstützt"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "Über Let's Encrypt"
},

View File

@@ -1,4 +1,61 @@
{
"2fa.backup-codes-remaining": {
"defaultMessage": "Backup codes remaining: {count}"
},
"2fa.backup-warning": {
"defaultMessage": "Save these backup codes in a secure place. Each code can only be used once."
},
"2fa.disable": {
"defaultMessage": "Disable Two-Factor Authentication"
},
"2fa.disable-confirm": {
"defaultMessage": "Disable 2FA"
},
"2fa.disable-warning": {
"defaultMessage": "Disabling two-factor authentication will make your account less secure."
},
"2fa.disabled": {
"defaultMessage": "Disabled"
},
"2fa.done": {
"defaultMessage": "I have saved my backup codes"
},
"2fa.enable": {
"defaultMessage": "Enable Two-Factor Authentication"
},
"2fa.enabled": {
"defaultMessage": "Enabled"
},
"2fa.enter-code": {
"defaultMessage": "Enter verification code"
},
"2fa.enter-code-disable": {
"defaultMessage": "Enter verification code to disable"
},
"2fa.regenerate": {
"defaultMessage": "Regenerate"
},
"2fa.regenerate-backup": {
"defaultMessage": "Regenerate Backup Codes"
},
"2fa.regenerate-instructions": {
"defaultMessage": "Enter a verification code to generate new backup codes. Your old codes will be invalidated."
},
"2fa.secret-key": {
"defaultMessage": "Secret Key"
},
"2fa.setup-instructions": {
"defaultMessage": "Scan this QR code with your authenticator app, or enter the secret manually."
},
"2fa.status": {
"defaultMessage": "Status"
},
"2fa.title": {
"defaultMessage": "Two-Factor Authentication"
},
"2fa.verify-enable": {
"defaultMessage": "Verify and Enable"
},
"access-list": {
"defaultMessage": "Access List"
},
@@ -170,6 +227,18 @@
"certificates.http.warning": {
"defaultMessage": "These domains must be already configured to point to this installation."
},
"certificates.key-type": {
"defaultMessage": "Key Type"
},
"certificates.key-type-description": {
"defaultMessage": "RSA is widely compatible, ECDSA is faster and more secure but may not be supported by older systems"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "with Let's Encrypt"
},
@@ -386,6 +455,21 @@
"loading": {
"defaultMessage": "Loading…"
},
"login.2fa-code": {
"defaultMessage": "Verification Code"
},
"login.2fa-code-placeholder": {
"defaultMessage": "Enter code"
},
"login.2fa-description": {
"defaultMessage": "Enter the code from your authenticator app"
},
"login.2fa-title": {
"defaultMessage": "Two-Factor Authentication"
},
"login.2fa-verify": {
"defaultMessage": "Verify"
},
"login.title": {
"defaultMessage": "Login to your account"
},
@@ -674,6 +758,9 @@
"user.switch-light": {
"defaultMessage": "Switch to Light mode"
},
"user.two-factor": {
"defaultMessage": "Two-Factor Auth"
},
"username": {
"defaultMessage": "Username"
},

View File

@@ -170,6 +170,18 @@
"certificates.http.warning": {
"defaultMessage": "Estos dominios ya deben estar configurados para apuntar a esta instalación."
},
"certificates.key-type": {
"defaultMessage": "Tipo de Clave"
},
"certificates.key-type-description": {
"defaultMessage": "RSA es ampliamente compatible, ECDSA es más rápido y seguro pero puede no ser compatible con sistemas antiguos"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "con Let's Encrypt"
},

View File

@@ -0,0 +1,683 @@
{
"access-list": {
"defaultMessage": "Liosta Rochtana"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {Rial} other {Rialacha}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {Úsáideoir} other {Úsáideoirí}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Nuair a bhíonn riail amháin ar a laghad ann, cuirfear an riail seo chun gach rud a dhiúltú leis an gceann deireanach."
},
"access-list.help.rules-order": {
"defaultMessage": "Tabhair faoi deara go gcuirfear na treoracha ceadaigh agus diúltaigh i bhfeidhm san ord a shainmhínítear iad."
},
"access-list.pass-auth": {
"defaultMessage": "Tabhair Údarú chuig an Sruth Uachtarach"
},
"access-list.public": {
"defaultMessage": "Inrochtana don Phobal"
},
"access-list.public.subtitle": {
"defaultMessage": "Níl aon údarú bunúsach ag teastáil"
},
"access-list.rule-source.placeholder": {
"defaultMessage": "192.168.1.100 nó 192.168.1.0/24 nó 2001:0db8::/32"
},
"access-list.satisfy-any": {
"defaultMessage": "Sásaigh Aon"
},
"access-list.subtitle": {
"defaultMessage": "{users} {users, plural, one {Úsáideoir} other {Úsáideoirí}}, {rules} {rules, plural, one {Riail} other {Rialacha}} - Cruthaithe: {date}"
},
"access-lists": {
"defaultMessage": "Liostaí Rochtana"
},
"action.add": {
"defaultMessage": "Cuir leis"
},
"action.add-location": {
"defaultMessage": "Cuir Suíomh leis"
},
"action.allow": {
"defaultMessage": "Ceadaigh"
},
"action.close": {
"defaultMessage": "Dún"
},
"action.delete": {
"defaultMessage": "Scrios"
},
"action.deny": {
"defaultMessage": "Diúltaigh"
},
"action.disable": {
"defaultMessage": "Díchumasaigh"
},
"action.download": {
"defaultMessage": "Íoslódáil"
},
"action.edit": {
"defaultMessage": "Cuir in Eagar"
},
"action.enable": {
"defaultMessage": "Cumasaigh"
},
"action.permissions": {
"defaultMessage": "Ceadanna"
},
"action.renew": {
"defaultMessage": "Athnuachan"
},
"action.view-details": {
"defaultMessage": "Féach Sonraí"
},
"auditlogs": {
"defaultMessage": "Logaí Iniúchta"
},
"auto": {
"defaultMessage": "Uath"
},
"cancel": {
"defaultMessage": "Cealaigh"
},
"certificate": {
"defaultMessage": "Teastas"
},
"certificate.custom-certificate": {
"defaultMessage": "Teastas"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Eochair Teastais"
},
"certificate.custom-intermediate": {
"defaultMessage": "Teastas Idirmheánach"
},
"certificate.in-use": {
"defaultMessage": "In Úsáid"
},
"certificate.none.subtitle": {
"defaultMessage": "Níor sannadh aon deimhniú"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Ní úsáidfidh an t-óstach seo HTTPS"
},
"certificate.none.title": {
"defaultMessage": "Dada"
},
"certificate.not-in-use": {
"defaultMessage": "Níor Úsáideadh"
},
"certificate.renew": {
"defaultMessage": "Athnuachan an Teastais"
},
"certificates": {
"defaultMessage": "Teastais"
},
"certificates.custom": {
"defaultMessage": "Teastas Saincheaptha"
},
"certificates.custom.warning": {
"defaultMessage": "Ní thacaítear le comhaid eochair atá cosanta le frása faire."
},
"certificates.dns.credentials": {
"defaultMessage": "Ábhar Comhaid Dintiúir"
},
"certificates.dns.credentials-note": {
"defaultMessage": "Éilíonn an breiseán seo comhad cumraíochta ina bhfuil comhartha API nó dintiúir eile do do sholáthraí."
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Stórálfar an fhaisnéis seo mar théacs simplí sa bhunachar sonraí agus i gcomhad!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Soicindí Iolraithe"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Fág folamh chun luach réamhshocraithe na mbreiseán a úsáid. Líon na soicindí le fanacht le haghaidh iomadú DNS."
},
"certificates.dns.provider": {
"defaultMessage": "Soláthraí DNS"
},
"certificates.dns.provider.placeholder": {
"defaultMessage": "Roghnaigh Soláthraí..."
},
"certificates.dns.warning": {
"defaultMessage": "Éilíonn an chuid seo roinnt eolais faoi Certbot agus a bhreiseáin DNS. Féach ar dhoiciméadacht na mbreiseán faoi seach, le do thoil."
},
"certificates.http.reachability-404": {
"defaultMessage": "Tá freastalaí aimsithe ag an bhfearann seo ach ní cosúil gur Bainisteoir Proxy Nginx atá ann. Déan cinnte go bhfuil do fhearann ag pointeáil chuig an seoladh IP ina bhfuil d'eispéireas NPM ag rith."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Theip ar sheiceáil an inrochtaineachta mar gheall ar earráid chumarsáide le site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"defaultMessage": "Níl aon fhreastalaí ar fáil ag an bhfearann seo. Cinntigh le do thoil go bhfuil do fhearann ann agus go bhfuil sé ag pointeáil chuig an seoladh IP ina bhfuil d'eispéireas NPM ag rith agus más gá, go bhfuil port 80 curtha ar aghaidh i do ródaire."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Tá rochtain ar do fhreastalaí agus ba cheart go mbeadh sé indéanta deimhnithe a chruthú."
},
"certificates.http.reachability-other": {
"defaultMessage": "Tá freastalaí aimsithe ag an bhfearann seo ach thug sé cód stádais gan choinne {code} ar ais. An é an freastalaí NPM atá ann? Déan cinnte go bhfuil do fhearann ag pointeáil chuig an seoladh IP ina bhfuil d'eispéireas NPM ag rith."
},
"certificates.http.reachability-wrong-data": {
"defaultMessage": "Tá freastalaí aimsithe ag an bhfearann seo ach thug sé sonraí gan choinne ar ais. An é an freastalaí NPM atá ann? Déan cinnte go bhfuil do fhearann ag pointeáil chuig an seoladh IP ina bhfuil d'eispéireas NPM ag rith."
},
"certificates.http.test-results": {
"defaultMessage": "Torthaí Tástála"
},
"certificates.http.warning": {
"defaultMessage": "Ní mór na fearainn seo a bheith cumraithe cheana féin chun pointeáil chuig an suiteáil seo."
},
"certificates.request.subtitle": {
"defaultMessage": "le Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Iarr Teastas nua"
},
"column.access": {
"defaultMessage": "Rochtain"
},
"column.authorization": {
"defaultMessage": "Údarú"
},
"column.authorizations": {
"defaultMessage": "Údaruithe"
},
"column.custom-locations": {
"defaultMessage": "Suíomhanna Saincheaptha"
},
"column.destination": {
"defaultMessage": "Ceann Scríbe"
},
"column.details": {
"defaultMessage": "Sonraí"
},
"column.email": {
"defaultMessage": "Ríomhphost"
},
"column.event": {
"defaultMessage": "Imeacht"
},
"column.expires": {
"defaultMessage": "Éagaíonn"
},
"column.http-code": {
"defaultMessage": "Cód HTTP"
},
"column.incoming-port": {
"defaultMessage": "Port Isteach"
},
"column.name": {
"defaultMessage": "Ainm"
},
"column.protocol": {
"defaultMessage": "Prótacal"
},
"column.provider": {
"defaultMessage": "Soláthraí"
},
"column.roles": {
"defaultMessage": "Róil"
},
"column.rules": {
"defaultMessage": "Rialacha"
},
"column.satisfy": {
"defaultMessage": "Sásamh"
},
"column.satisfy-all": {
"defaultMessage": "Gach"
},
"column.satisfy-any": {
"defaultMessage": "Aon"
},
"column.scheme": {
"defaultMessage": "Scéim"
},
"column.source": {
"defaultMessage": "Foinse"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Stádas"
},
"created-on": {
"defaultMessage": "Cruthaithe: {date}"
},
"dashboard": {
"defaultMessage": "Painéal Rialaithe"
},
"dead-host": {
"defaultMessage": "Óstach 404"
},
"dead-hosts": {
"defaultMessage": "404 Óstaigh"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Óstach 404} other {Óstaigh 404}}"
},
"disabled": {
"defaultMessage": "Míchumasaithe"
},
"domain-names": {
"defaultMessage": "Ainmneacha Fearainn"
},
"domain-names.max": {
"defaultMessage": "Uasmhéid d'ainmneacha fearainn: {count}"
},
"domain-names.placeholder": {
"defaultMessage": "Tosaigh ag clóscríobh chun fearann a chur leis..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Ní cheadaítear cártaí fiáine don chineál seo"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Ní thacaítear le cártaí fiáine don ÚD seo"
},
"domains.force-ssl": {
"defaultMessage": "Fórsáil SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "Cumasaithe HSTS"
},
"domains.hsts-subdomains": {
"defaultMessage": "Fo-fhearainn HSTS"
},
"domains.http2-support": {
"defaultMessage": "Tacaíocht HTTP/2"
},
"domains.use-dns": {
"defaultMessage": "Úsáid Dúshlán DNS"
},
"email-address": {
"defaultMessage": "Seoladh ríomhphoist"
},
"empty-search": {
"defaultMessage": "Níor aimsíodh aon torthaí"
},
"empty-subtitle": {
"defaultMessage": "Cén fáth nach gcruthaíonn tú ceann?"
},
"enabled": {
"defaultMessage": "Cumasaithe"
},
"error.access.at-least-one": {
"defaultMessage": "Tá Údarú amháin nó Riail Rochtana amháin ag teastáil"
},
"error.access.duplicate-usernames": {
"defaultMessage": "Ní mór dainmneacha úsáideora údaraithe a bheith uathúil"
},
"error.invalid-auth": {
"defaultMessage": "Ríomhphost nó pasfhocal neamhbhailí"
},
"error.invalid-domain": {
"defaultMessage": "Fearann neamhbhailí: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Seoladh ríomhphoist neamhbhailí"
},
"error.max-character-length": {
"defaultMessage": "Is é an fad uasta ná {max} carachtar{max, plural, one {} other {anna}}"
},
"error.max-domains": {
"defaultMessage": "An iomarca fearainn, is é {max} an t-uasmhéid"
},
"error.maximum": {
"defaultMessage": "Is é {max} an t-uasmhéid"
},
"error.min-character-length": {
"defaultMessage": "Is é an fad íosta ná {min} carachtar{min, plural, one {} other {anna}}"
},
"error.minimum": {
"defaultMessage": "Is é {min} an t-íosmhéid"
},
"error.passwords-must-match": {
"defaultMessage": "Ní mór pasfhocail a bheith mar a chéile"
},
"error.required": {
"defaultMessage": "Tá sé seo riachtanach"
},
"expires.on": {
"defaultMessage": "Éagaíonn: {date}"
},
"footer.github-fork": {
"defaultMessage": "Forc mé ar Github"
},
"host.flags.block-exploits": {
"defaultMessage": "Blocáil Easnaimh Choitianta"
},
"host.flags.cache-assets": {
"defaultMessage": "Sócmhainní Taisce"
},
"host.flags.preserve-path": {
"defaultMessage": "Cosán a Chaomhnú"
},
"host.flags.protocols": {
"defaultMessage": "Prótacail"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Tacaíocht Websockets"
},
"host.forward-port": {
"defaultMessage": "Port Ar Aghaidh"
},
"host.forward-scheme": {
"defaultMessage": "Scéim"
},
"hosts": {
"defaultMessage": "Óstaigh"
},
"http-only": {
"defaultMessage": "HTTP Amháin"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt trí DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt trí HTTP"
},
"loading": {
"defaultMessage": "Ag lódáil…"
},
"login.title": {
"defaultMessage": "Logáil isteach i do chuntas"
},
"nginx-config.label": {
"defaultMessage": "Cumraíocht Nginx Saincheaptha"
},
"nginx-config.placeholder": {
"defaultMessage": "# Cuir isteach do chumraíocht saincheaptha Nginx anseo ar do phriacal féin!"
},
"no-permission-error": {
"defaultMessage": "Níl rochtain agat chun seo a fheiceáil."
},
"notfound.action": {
"defaultMessage": "Tabhair abhaile mé"
},
"notfound.content": {
"defaultMessage": "Tá brón orainn ach níor aimsíodh an leathanach atá á lorg agat"
},
"notfound.title": {
"defaultMessage": "Úps… Fuair tú leathanach earráide díreach anois."
},
"notification.error": {
"defaultMessage": "Earráid"
},
"notification.object-deleted": {
"defaultMessage": "Scriosadh {object}"
},
"notification.object-disabled": {
"defaultMessage": "Tá {object} díchumasaithe"
},
"notification.object-enabled": {
"defaultMessage": "Tá {object} cumasaithe"
},
"notification.object-renewed": {
"defaultMessage": "Tá {object} athnuaite"
},
"notification.object-saved": {
"defaultMessage": "Tá {object} sábháilte"
},
"notification.success": {
"defaultMessage": "Rath"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "Cuir {object} leis"
},
"object.delete": {
"defaultMessage": "Scrios {object}"
},
"object.delete.content": {
"defaultMessage": "An bhfuil tú cinnte gur mian leat an {object} seo a scriosadh?"
},
"object.edit": {
"defaultMessage": "Cuir in eagar {object}"
},
"object.empty": {
"defaultMessage": "Níl aon {objects} ann"
},
"object.event.created": {
"defaultMessage": "Cruthaithe {object}"
},
"object.event.deleted": {
"defaultMessage": "Scriosadh {object}"
},
"object.event.disabled": {
"defaultMessage": "Díchumasaithe {object}"
},
"object.event.enabled": {
"defaultMessage": "Cumasaithe {object}"
},
"object.event.renewed": {
"defaultMessage": "Athnuaite {object}"
},
"object.event.updated": {
"defaultMessage": "Nuashonraithe {object}"
},
"offline": {
"defaultMessage": "As líne"
},
"online": {
"defaultMessage": "Ar líne"
},
"options": {
"defaultMessage": "Roghanna"
},
"password": {
"defaultMessage": "Pasfhocal"
},
"password.generate": {
"defaultMessage": "Gin pasfhocal randamach"
},
"password.hide": {
"defaultMessage": "Folaigh Pasfhocal"
},
"password.show": {
"defaultMessage": "Taispeáin Pasfhocal"
},
"permissions.hidden": {
"defaultMessage": "I bhfolach"
},
"permissions.manage": {
"defaultMessage": "Bainistigh"
},
"permissions.view": {
"defaultMessage": "Amharc Amháin"
},
"permissions.visibility.all": {
"defaultMessage": "Gach Míreanna"
},
"permissions.visibility.title": {
"defaultMessage": "Infheictheacht Míre"
},
"permissions.visibility.user": {
"defaultMessage": "Míreanna Cruthaithe Amháin"
},
"proxy-host": {
"defaultMessage": "Óstach Seachfhreastalaí"
},
"proxy-host.forward-host": {
"defaultMessage": "Ainm Óstach / IP Ar Aghaidh"
},
"proxy-hosts": {
"defaultMessage": "Óstaigh Seachfhreastalaí"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Óstach Seachfhreastalaí} other {Óstaigh Seachfhreastalaí}}"
},
"public": {
"defaultMessage": "Poiblí"
},
"redirection-host": {
"defaultMessage": "Óstach Athsheolta"
},
"redirection-host.forward-domain": {
"defaultMessage": "Fearann Ar Aghaidh"
},
"redirection-host.forward-http-code": {
"defaultMessage": "Cód HTTP"
},
"redirection-hosts": {
"defaultMessage": "Óstaigh Athsheolta"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Athsheoladh Óstach} other {Athsheoladh Óstaigh}}"
},
"redirection-hosts.http-code.300": {
"defaultMessage": "300 Rogha Ilghnéitheach"
},
"redirection-hosts.http-code.301": {
"defaultMessage": "301 Bogtha go buan"
},
"redirection-hosts.http-code.302": {
"defaultMessage": "302 Bogtha go sealadach"
},
"redirection-hosts.http-code.303": {
"defaultMessage": "303 Féach eile"
},
"redirection-hosts.http-code.307": {
"defaultMessage": "307 Atreorú sealadach"
},
"redirection-hosts.http-code.308": {
"defaultMessage": "308 Athsheoladh buan"
},
"role.admin": {
"defaultMessage": "Riarthóir"
},
"role.standard-user": {
"defaultMessage": "Úsáideoir Caighdeánach"
},
"save": {
"defaultMessage": "Sábháil"
},
"setting": {
"defaultMessage": "Socrú"
},
"settings": {
"defaultMessage": "Socruithe"
},
"settings.default-site": {
"defaultMessage": "Suíomh Réamhshocraithe"
},
"settings.default-site.404": {
"defaultMessage": "Leathanach 404"
},
"settings.default-site.444": {
"defaultMessage": "Gan Freagra (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Leathanach Comhghairdeas"
},
"settings.default-site.description": {
"defaultMessage": "Cad atá le taispeáint nuair a bhuaileann óstach anaithnid Nginx"
},
"settings.default-site.html": {
"defaultMessage": "HTML saincheaptha"
},
"settings.default-site.html.placeholder": {
"defaultMessage": "<!-- Cuir isteach dábhar HTML saincheaptha anseo -->"
},
"settings.default-site.redirect": {
"defaultMessage": "Atreorú"
},
"setup.preamble": {
"defaultMessage": "Tosaigh trí do chuntas riarthóra a chruthú."
},
"setup.title": {
"defaultMessage": "Fáilte!"
},
"sign-in": {
"defaultMessage": "Sínigh isteach"
},
"ssl-certificate": {
"defaultMessage": "Teastas SSL"
},
"stream": {
"defaultMessage": "Sruth"
},
"stream.forward-host": {
"defaultMessage": "Óstach Ar Aghaidh"
},
"stream.forward-host.placeholder": {
"defaultMessage": "example.com nó 10.0.0.1 nó 2001:db8:3333:4444:5555:6666:7777:8888"
},
"stream.incoming-port": {
"defaultMessage": "Port Isteach"
},
"streams": {
"defaultMessage": "Sruthanna"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Sruth} other {Sruthanna}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Tástáil"
},
"update-available": {
"defaultMessage": "Nuashonrú ar Fáil: {latestVersion}"
},
"user": {
"defaultMessage": "Úsáideoir"
},
"user.change-password": {
"defaultMessage": "Athraigh Pasfhocal"
},
"user.confirm-password": {
"defaultMessage": "Deimhnigh Pasfhocal"
},
"user.current-password": {
"defaultMessage": "Pasfhocal Reatha"
},
"user.edit-profile": {
"defaultMessage": "Cuir Próifíl in Eagar"
},
"user.full-name": {
"defaultMessage": "Ainm Iomlán"
},
"user.login-as": {
"defaultMessage": "Sínigh isteach mar {name}"
},
"user.logout": {
"defaultMessage": "Logáil Amach"
},
"user.new-password": {
"defaultMessage": "Pasfhocal Nua"
},
"user.nickname": {
"defaultMessage": "Leasainm"
},
"user.set-password": {
"defaultMessage": "Socraigh Pasfhocal"
},
"user.set-permissions": {
"defaultMessage": "Socraigh Ceadanna do {name}"
},
"user.switch-dark": {
"defaultMessage": "Athraigh go Mód Dorcha"
},
"user.switch-light": {
"defaultMessage": "Athraigh go mód Solais"
},
"username": {
"defaultMessage": "Ainm úsáideora"
},
"users": {
"defaultMessage": "Úsáideoirí"
}
}

View File

@@ -0,0 +1,683 @@
{
"access-list": {
"defaultMessage": "Daftar Akses"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {Aturan} other {Aturan}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {Pengguna} other {Pengguna}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Jika setidaknya 1 aturan ada, aturan tolak semua ini akan ditambahkan paling akhir"
},
"access-list.help.rules-order": {
"defaultMessage": "Perhatikan bahwa direktif izinkan dan tolak akan diterapkan sesuai urutan yang didefinisikan."
},
"access-list.pass-auth": {
"defaultMessage": "Teruskan Auth ke Upstream"
},
"access-list.public": {
"defaultMessage": "Dapat Diakses Publik"
},
"access-list.public.subtitle": {
"defaultMessage": "Tidak perlu basic auth"
},
"access-list.rule-source.placeholder": {
"defaultMessage": "192.168.1.100 atau 192.168.1.0/24 atau 2001:0db8::/32"
},
"access-list.satisfy-any": {
"defaultMessage": "Penuhi Salah Satu"
},
"access-list.subtitle": {
"defaultMessage": "{users} {users, plural, one {Pengguna} other {Pengguna}}, {rules} {rules, plural, one {Aturan} other {Aturan}} - Dibuat: {date}"
},
"access-lists": {
"defaultMessage": "Daftar Akses"
},
"action.add": {
"defaultMessage": "Tambah"
},
"action.add-location": {
"defaultMessage": "Tambah Lokasi"
},
"action.allow": {
"defaultMessage": "Izinkan"
},
"action.close": {
"defaultMessage": "Tutup"
},
"action.delete": {
"defaultMessage": "Hapus"
},
"action.deny": {
"defaultMessage": "Tolak"
},
"action.disable": {
"defaultMessage": "Nonaktifkan"
},
"action.download": {
"defaultMessage": "Unduh"
},
"action.edit": {
"defaultMessage": "Edit"
},
"action.enable": {
"defaultMessage": "Aktifkan"
},
"action.permissions": {
"defaultMessage": "Izin"
},
"action.renew": {
"defaultMessage": "Perpanjang"
},
"action.view-details": {
"defaultMessage": "Lihat Detail"
},
"auditlogs": {
"defaultMessage": "Log Audit"
},
"auto": {
"defaultMessage": "Otomatis"
},
"cancel": {
"defaultMessage": "Batal"
},
"certificate": {
"defaultMessage": "Sertifikat"
},
"certificate.custom-certificate": {
"defaultMessage": "Sertifikat"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Kunci Sertifikat"
},
"certificate.custom-intermediate": {
"defaultMessage": "Sertifikat Intermediate"
},
"certificate.in-use": {
"defaultMessage": "Digunakan"
},
"certificate.none.subtitle": {
"defaultMessage": "Tidak ada sertifikat yang ditetapkan"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Host ini tidak akan menggunakan HTTPS"
},
"certificate.none.title": {
"defaultMessage": "Tidak Ada"
},
"certificate.not-in-use": {
"defaultMessage": "Tidak Digunakan"
},
"certificate.renew": {
"defaultMessage": "Perpanjang Sertifikat"
},
"certificates": {
"defaultMessage": "Sertifikat"
},
"certificates.custom": {
"defaultMessage": "Sertifikat Kustom"
},
"certificates.custom.warning": {
"defaultMessage": "Berkas kunci yang dilindungi frasa sandi tidak didukung."
},
"certificates.dns.credentials": {
"defaultMessage": "Konten File Kredensial"
},
"certificates.dns.credentials-note": {
"defaultMessage": "Plugin ini memerlukan file konfigurasi yang berisi token API atau kredensial lain untuk penyedia Anda"
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Data ini akan disimpan sebagai teks biasa di database dan dalam file!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Detik Propagasi"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Biarkan kosong untuk menggunakan nilai baku plugin. Jumlah detik menunggu propagasi DNS."
},
"certificates.dns.provider": {
"defaultMessage": "Penyedia DNS"
},
"certificates.dns.provider.placeholder": {
"defaultMessage": "Pilih Penyedia..."
},
"certificates.dns.warning": {
"defaultMessage": "Bagian ini memerlukan pengetahuan tentang Certbot dan plugin DNS-nya. Silakan merujuk dokumentasi plugin terkait."
},
"certificates.http.reachability-404": {
"defaultMessage": "Ada server yang ditemukan pada domain ini tetapi tampaknya bukan Nginx Proxy Manager. Pastikan domain Anda mengarah ke IP tempat instance NPM berjalan."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Gagal memeriksa keterjangkauan karena kesalahan komunikasi dengan site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"defaultMessage": "Tidak ada server yang tersedia pada domain ini. Pastikan domain Anda ada dan mengarah ke IP tempat instance NPM berjalan dan bila perlu port 80 diteruskan di router Anda."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Server Anda dapat dijangkau dan pembuatan sertifikat seharusnya memungkinkan."
},
"certificates.http.reachability-other": {
"defaultMessage": "Ada server yang ditemukan pada domain ini tetapi mengembalikan kode status tak terduga {code}. Apakah itu server NPM? Pastikan domain Anda mengarah ke IP tempat instance NPM berjalan."
},
"certificates.http.reachability-wrong-data": {
"defaultMessage": "Ada server yang ditemukan pada domain ini tetapi mengembalikan data yang tidak terduga. Apakah itu server NPM? Pastikan domain Anda mengarah ke IP tempat instance NPM berjalan."
},
"certificates.http.test-results": {
"defaultMessage": "Hasil Uji"
},
"certificates.http.warning": {
"defaultMessage": "Domain ini harus sudah dikonfigurasi agar mengarah ke instalasi ini."
},
"certificates.request.subtitle": {
"defaultMessage": "dengan Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Minta Sertifikat Baru"
},
"column.access": {
"defaultMessage": "Akses"
},
"column.authorization": {
"defaultMessage": "Otorisasi"
},
"column.authorizations": {
"defaultMessage": "Otorisasi"
},
"column.custom-locations": {
"defaultMessage": "Lokasi Kustom"
},
"column.destination": {
"defaultMessage": "Tujuan"
},
"column.details": {
"defaultMessage": "Detail"
},
"column.email": {
"defaultMessage": "Email"
},
"column.event": {
"defaultMessage": "Peristiwa"
},
"column.expires": {
"defaultMessage": "Kedaluwarsa"
},
"column.http-code": {
"defaultMessage": "Kode HTTP"
},
"column.incoming-port": {
"defaultMessage": "Port Masuk"
},
"column.name": {
"defaultMessage": "Nama"
},
"column.protocol": {
"defaultMessage": "Protokol"
},
"column.provider": {
"defaultMessage": "Penyedia"
},
"column.roles": {
"defaultMessage": "Peran"
},
"column.rules": {
"defaultMessage": "Aturan"
},
"column.satisfy": {
"defaultMessage": "Pemenuhan"
},
"column.satisfy-all": {
"defaultMessage": "Semua"
},
"column.satisfy-any": {
"defaultMessage": "Salah Satu"
},
"column.scheme": {
"defaultMessage": "Skema"
},
"column.source": {
"defaultMessage": "Sumber"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Status"
},
"created-on": {
"defaultMessage": "Dibuat: {date}"
},
"dashboard": {
"defaultMessage": "Dasbor"
},
"dead-host": {
"defaultMessage": "Host 404"
},
"dead-hosts": {
"defaultMessage": "Host 404"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host 404} other {Host 404}}"
},
"disabled": {
"defaultMessage": "Nonaktif"
},
"domain-names": {
"defaultMessage": "Nama Domain"
},
"domain-names.max": {
"defaultMessage": "Maksimum {count} nama domain"
},
"domain-names.placeholder": {
"defaultMessage": "Mulai mengetik untuk menambahkan domain..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Wildcard tidak diizinkan untuk tipe ini"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Wildcard tidak didukung untuk CA ini"
},
"domains.force-ssl": {
"defaultMessage": "Paksa SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS Diaktifkan"
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS Subdomain"
},
"domains.http2-support": {
"defaultMessage": "Dukungan HTTP/2"
},
"domains.use-dns": {
"defaultMessage": "Gunakan DNS Challenge"
},
"email-address": {
"defaultMessage": "Alamat email"
},
"empty-search": {
"defaultMessage": "Tidak ada hasil"
},
"empty-subtitle": {
"defaultMessage": "Mengapa tidak membuatnya?"
},
"enabled": {
"defaultMessage": "Aktif"
},
"error.access.at-least-one": {
"defaultMessage": "Setidaknya satu Otorisasi atau satu Aturan Akses diperlukan"
},
"error.access.duplicate-usernames": {
"defaultMessage": "Nama pengguna otorisasi harus unik"
},
"error.invalid-auth": {
"defaultMessage": "Email atau kata sandi tidak valid"
},
"error.invalid-domain": {
"defaultMessage": "Domain tidak valid: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Alamat email tidak valid"
},
"error.max-character-length": {
"defaultMessage": "Panjang maksimum adalah {max} karakter{max, plural, one {} other {}}"
},
"error.max-domains": {
"defaultMessage": "Terlalu banyak domain, maksimum {max}"
},
"error.maximum": {
"defaultMessage": "Maksimum adalah {max}"
},
"error.min-character-length": {
"defaultMessage": "Panjang minimum adalah {min} karakter{min, plural, one {} other {}}"
},
"error.minimum": {
"defaultMessage": "Minimum adalah {min}"
},
"error.passwords-must-match": {
"defaultMessage": "Kata sandi harus cocok"
},
"error.required": {
"defaultMessage": "Ini wajib diisi"
},
"expires.on": {
"defaultMessage": "Kedaluwarsa: {date}"
},
"footer.github-fork": {
"defaultMessage": "Fork saya di GitHub"
},
"host.flags.block-exploits": {
"defaultMessage": "Blokir Eksploit Umum"
},
"host.flags.cache-assets": {
"defaultMessage": "Cache Aset"
},
"host.flags.preserve-path": {
"defaultMessage": "Pertahankan Path"
},
"host.flags.protocols": {
"defaultMessage": "Protokol"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Dukungan Websocket"
},
"host.forward-port": {
"defaultMessage": "Port Terusan"
},
"host.forward-scheme": {
"defaultMessage": "Skema"
},
"hosts": {
"defaultMessage": "Host"
},
"http-only": {
"defaultMessage": "HTTP Saja"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt via DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt via HTTP"
},
"loading": {
"defaultMessage": "Memuat…"
},
"login.title": {
"defaultMessage": "Masuk ke akun Anda"
},
"nginx-config.label": {
"defaultMessage": "Konfigurasi Nginx Kustom"
},
"nginx-config.placeholder": {
"defaultMessage": "# Masukkan konfigurasi Nginx kustom Anda di sini dengan risiko Anda sendiri!"
},
"no-permission-error": {
"defaultMessage": "Anda tidak memiliki akses untuk melihat ini."
},
"notfound.action": {
"defaultMessage": "Bawa saya pulang"
},
"notfound.content": {
"defaultMessage": "Maaf, halaman yang Anda cari tidak ditemukan"
},
"notfound.title": {
"defaultMessage": "Ups… Anda baru saja menemukan halaman error"
},
"notification.error": {
"defaultMessage": "Kesalahan"
},
"notification.object-deleted": {
"defaultMessage": "{object} telah dihapus"
},
"notification.object-disabled": {
"defaultMessage": "{object} telah dinonaktifkan"
},
"notification.object-enabled": {
"defaultMessage": "{object} telah diaktifkan"
},
"notification.object-renewed": {
"defaultMessage": "{object} telah diperpanjang"
},
"notification.object-saved": {
"defaultMessage": "{object} telah disimpan"
},
"notification.success": {
"defaultMessage": "Berhasil"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "Tambah {object}"
},
"object.delete": {
"defaultMessage": "Hapus {object}"
},
"object.delete.content": {
"defaultMessage": "Apakah Anda yakin ingin menghapus {object} ini?"
},
"object.edit": {
"defaultMessage": "Edit {object}"
},
"object.empty": {
"defaultMessage": "Tidak ada {objects}"
},
"object.event.created": {
"defaultMessage": "{object} dibuat"
},
"object.event.deleted": {
"defaultMessage": "{object} dihapus"
},
"object.event.disabled": {
"defaultMessage": "{object} dinonaktifkan"
},
"object.event.enabled": {
"defaultMessage": "{object} diaktifkan"
},
"object.event.renewed": {
"defaultMessage": "{object} diperpanjang"
},
"object.event.updated": {
"defaultMessage": "{object} diperbarui"
},
"offline": {
"defaultMessage": "Offline"
},
"online": {
"defaultMessage": "Online"
},
"options": {
"defaultMessage": "Opsi"
},
"password": {
"defaultMessage": "Kata sandi"
},
"password.generate": {
"defaultMessage": "Buat kata sandi acak"
},
"password.hide": {
"defaultMessage": "Sembunyikan Kata Sandi"
},
"password.show": {
"defaultMessage": "Tampilkan Kata Sandi"
},
"permissions.hidden": {
"defaultMessage": "Tersembunyi"
},
"permissions.manage": {
"defaultMessage": "Kelola"
},
"permissions.view": {
"defaultMessage": "Hanya Lihat"
},
"permissions.visibility.all": {
"defaultMessage": "Semua Item"
},
"permissions.visibility.title": {
"defaultMessage": "Visibilitas Item"
},
"permissions.visibility.user": {
"defaultMessage": "Hanya Item yang Dibuat"
},
"proxy-host": {
"defaultMessage": "Host Proxy"
},
"proxy-host.forward-host": {
"defaultMessage": "Hostname / IP Terusan"
},
"proxy-hosts": {
"defaultMessage": "Host Proxy"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host Proxy} other {Host Proxy}}"
},
"public": {
"defaultMessage": "Publik"
},
"redirection-host": {
"defaultMessage": "Host Pengalihan"
},
"redirection-host.forward-domain": {
"defaultMessage": "Domain Terusan"
},
"redirection-host.forward-http-code": {
"defaultMessage": "Kode HTTP"
},
"redirection-hosts": {
"defaultMessage": "Host Pengalihan"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host Pengalihan} other {Host Pengalihan}}"
},
"redirection-hosts.http-code.300": {
"defaultMessage": "300 Banyak Pilihan"
},
"redirection-hosts.http-code.301": {
"defaultMessage": "301 Pindah permanen"
},
"redirection-hosts.http-code.302": {
"defaultMessage": "302 Pindah sementara"
},
"redirection-hosts.http-code.303": {
"defaultMessage": "303 Lihat lainnya"
},
"redirection-hosts.http-code.307": {
"defaultMessage": "307 Pengalihan sementara"
},
"redirection-hosts.http-code.308": {
"defaultMessage": "308 Pengalihan permanen"
},
"role.admin": {
"defaultMessage": "Administrator"
},
"role.standard-user": {
"defaultMessage": "Pengguna Standar"
},
"save": {
"defaultMessage": "Simpan"
},
"setting": {
"defaultMessage": "Pengaturan"
},
"settings": {
"defaultMessage": "Pengaturan"
},
"settings.default-site": {
"defaultMessage": "Situs Default"
},
"settings.default-site.404": {
"defaultMessage": "Halaman 404"
},
"settings.default-site.444": {
"defaultMessage": "Tidak Ada Respons (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Halaman Ucapan Selamat"
},
"settings.default-site.description": {
"defaultMessage": "Apa yang ditampilkan saat Nginx diakses dengan Host yang tidak dikenal"
},
"settings.default-site.html": {
"defaultMessage": "HTML Kustom"
},
"settings.default-site.html.placeholder": {
"defaultMessage": "<!-- Masukkan konten HTML kustom Anda di sini -->"
},
"settings.default-site.redirect": {
"defaultMessage": "Alihkan"
},
"setup.preamble": {
"defaultMessage": "Mulai dengan membuat akun admin Anda."
},
"setup.title": {
"defaultMessage": "Selamat datang!"
},
"sign-in": {
"defaultMessage": "Masuk"
},
"ssl-certificate": {
"defaultMessage": "Sertifikat SSL"
},
"stream": {
"defaultMessage": "Stream"
},
"stream.forward-host": {
"defaultMessage": "Host Terusan"
},
"stream.forward-host.placeholder": {
"defaultMessage": "example.com atau 10.0.0.1 atau 2001:db8:3333:4444:5555:6666:7777:8888"
},
"stream.incoming-port": {
"defaultMessage": "Port Masuk"
},
"streams": {
"defaultMessage": "Stream"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Stream} other {Stream}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Uji"
},
"update-available": {
"defaultMessage": "Pembaruan Tersedia: {latestVersion}"
},
"user": {
"defaultMessage": "Pengguna"
},
"user.change-password": {
"defaultMessage": "Ubah Kata Sandi"
},
"user.confirm-password": {
"defaultMessage": "Konfirmasi Kata Sandi"
},
"user.current-password": {
"defaultMessage": "Kata Sandi Saat Ini"
},
"user.edit-profile": {
"defaultMessage": "Edit Profil"
},
"user.full-name": {
"defaultMessage": "Nama Lengkap"
},
"user.login-as": {
"defaultMessage": "Masuk sebagai {name}"
},
"user.logout": {
"defaultMessage": "Keluar"
},
"user.new-password": {
"defaultMessage": "Kata Sandi Baru"
},
"user.nickname": {
"defaultMessage": "Nama Panggilan"
},
"user.set-password": {
"defaultMessage": "Atur Kata Sandi"
},
"user.set-permissions": {
"defaultMessage": "Atur Izin untuk {name}"
},
"user.switch-dark": {
"defaultMessage": "Beralih ke mode gelap"
},
"user.switch-light": {
"defaultMessage": "Beralih ke mode terang"
},
"username": {
"defaultMessage": "Nama pengguna"
},
"users": {
"defaultMessage": "Pengguna"
}
}

View File

@@ -155,6 +155,18 @@
"certificates.http.warning": {
"defaultMessage": "Questi domini devono già essere configurati per puntare a questa installazione."
},
"certificates.key-type": {
"defaultMessage": "Tipo di Chiave"
},
"certificates.key-type-description": {
"defaultMessage": "RSA è ampiamente compatibile, ECDSA è più veloce e sicuro ma potrebbe non essere supportato da sistemi più vecchi"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "con Let's Encrypt"
},
@@ -429,7 +441,7 @@
"defaultMessage": "Modifica {object}"
},
"object.empty": {
"defaultMessage": "Nessun {objects} presente"
"defaultMessage": "Non ci sono {objects} presenti"
},
"object.event.created": {
"defaultMessage": "{object} creato"

View File

@@ -155,6 +155,18 @@
"certificates.http.warning": {
"defaultMessage": "これらのドメインは、すでにこのインストール先を指すように設定されている必要がありますあ."
},
"certificates.key-type": {
"defaultMessage": "鍵タイプ"
},
"certificates.key-type-description": {
"defaultMessage": "RSAは広く互換性があり、ECDSAはより高速で安全ですが、古いシステムではサポートされていない場合があります"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "Let's Encryptを使用する"
},
@@ -570,7 +582,7 @@
"defaultMessage": "ストリーム"
},
"stream.forward-host": {
"defaultMessage": "転送ポート"
"defaultMessage": "転送ホスト"
},
"stream.incoming-port": {
"defaultMessage": "受信ポート"

View File

@@ -170,6 +170,18 @@
"certificates.http.warning": {
"defaultMessage": "도메인이 이 서버를 가리키도록 설정되어 있어야 합니다."
},
"certificates.key-type": {
"defaultMessage": "키 유형"
},
"certificates.key-type-description": {
"defaultMessage": "RSA는 호환성이 넓고, ECDSA는 더 빠르고 안전하지만 오래된 시스템에서 지원되지 않을 수 있습니다"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "Let's Encrypt 사용"
},
@@ -218,7 +230,7 @@
"column.provider": {
"defaultMessage": "공급자"
},
"column.roles": {
"column.roles": {
"defaultMessage": "권한"
},
"column.rules": {

84
frontend/src/locale/src/lang-list.json Executable file → Normal file
View File

@@ -1,41 +1,47 @@
{
"locale-en-US": {
"defaultMessage": "English"
},
"locale-es-ES": {
"defaultMessage": "Español"
},
"locale-de-DE": {
"defaultMessage": "German"
},
"locale-ja-JP": {
"defaultMessage": "日本語"
},
"locale-ru-RU": {
"defaultMessage": "Русский"
},
"locale-sk-SK": {
"defaultMessage": "Slovenčina"
},
"locale-zh-CN": {
"defaultMessage": "中文"
},
"locale-pl-PL": {
"defaultMessage": "Polski"
},
"locale-it-IT": {
"defaultMessage": "Italiano"
},
"locale-vi-VN": {
"defaultMessage": "Tiếng Việt"
},
"locale-nl-NL": {
"defaultMessage": "Nederlands"
},
"locale-ko-KR": {
"defaultMessage": "한국어"
},
"locale-bg-BG": {
"defaultMessage": "Български"
}
"locale-en-US": {
"defaultMessage": "English"
},
"locale-es-ES": {
"defaultMessage": "Español"
},
"locale-ie-GA": {
"defaultMessage": "Gaeilge"
},
"locale-de-DE": {
"defaultMessage": "German"
},
"locale-id-ID": {
"defaultMessage": "Bahasa Indonesia"
},
"locale-ja-JP": {
"defaultMessage": "日本語"
},
"locale-ru-RU": {
"defaultMessage": "Русский"
},
"locale-sk-SK": {
"defaultMessage": "Slovenčina"
},
"locale-zh-CN": {
"defaultMessage": "中文"
},
"locale-pl-PL": {
"defaultMessage": "Polski"
},
"locale-it-IT": {
"defaultMessage": "Italiano"
},
"locale-vi-VN": {
"defaultMessage": "Tiếng Việt"
},
"locale-nl-NL": {
"defaultMessage": "Nederlands"
},
"locale-ko-KR": {
"defaultMessage": "한국어"
},
"locale-bg-BG": {
"defaultMessage": "Български"
}
}

View File

@@ -155,6 +155,18 @@
"certificates.http.warning": {
"defaultMessage": "Deze domeinen moeten al worden geconfigureerd om naar deze installatie te wijzen."
},
"certificates.key-type": {
"defaultMessage": "Sleuteltype"
},
"certificates.key-type-description": {
"defaultMessage": "RSA is breed compatibel, ECDSA is sneller en veiliger maar wordt mogelijk niet ondersteund door oudere systemen"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "met Let's Encrypt"
},
@@ -644,4 +656,4 @@
"users": {
"defaultMessage": "Gebruikers"
}
}
}

View File

@@ -1,12 +1,12 @@
{
"access-list": {
"defaultMessage": "Lista dostępu"
"defaultMessage": "wpis listy dostępu"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {Reguła} few {Reguły} other {Reguł}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {Użytkownik} few {Użytkowników} other {Użytkowników}}"
"defaultMessage": "{count} {count, plural, one {Użytkownik} few {Użytkownicy} other {Użytkowników}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Gdy istnieje co najmniej 1 reguła, ta reguła blokująca wszystko zostanie dodana na końcu"
@@ -38,12 +38,18 @@
"action.add-location": {
"defaultMessage": "Dodaj lokalizację"
},
"action.allow": {
"defaultMessage": "Zezwól"
},
"action.close": {
"defaultMessage": "Zamknij"
},
"action.delete": {
"defaultMessage": "Usuń"
},
"action.deny": {
"defaultMessage": "Odrzuć"
},
"action.disable": {
"defaultMessage": "Wyłącz"
},
@@ -66,13 +72,13 @@
"defaultMessage": "Pokaż szczegóły"
},
"auditlogs": {
"defaultMessage": "Logi audytu"
"defaultMessage": "Logi"
},
"cancel": {
"defaultMessage": "Anuluj"
},
"certificate": {
"defaultMessage": "Certyfikat"
"defaultMessage": "certyfikat"
},
"certificate.custom-certificate": {
"defaultMessage": "Certyfikat"
@@ -105,7 +111,7 @@
"defaultMessage": "Certyfikaty"
},
"certificates.custom": {
"defaultMessage": "Certyfikat własny"
"defaultMessage": "Własny certyfikat"
},
"certificates.custom.warning": {
"defaultMessage": "Pliki kluczy chronione hasłem nie są obsługiwane."
@@ -153,13 +159,25 @@
"defaultMessage": "Wyniki testu"
},
"certificates.http.warning": {
"defaultMessage": "Te domeny muszą być już skonfigurowane tak, aby wskazywały na ten serwer www"
"defaultMessage": "Te domeny muszą być już skonfigurowane tak, aby wskazywały na ten serwer"
},
"certificates.key-type": {
"defaultMessage": "Typ klucza"
},
"certificates.key-type-description": {
"defaultMessage": "RSA jest szeroko kompatybilny, ECDSA jest szybszy i bezpieczniejszy, ale może nie być obsługiwany przez starsze systemy"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "z Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Zamów nowy certyfikat"
"defaultMessage": "Wygeneruj nowy certyfikat"
},
"column.access": {
"defaultMessage": "Dostęp"
@@ -171,7 +189,7 @@
"defaultMessage": "Autoryzacje"
},
"column.custom-locations": {
"defaultMessage": "Własne lokalizacje"
"defaultMessage": "Własne ustawienia lokalizacji"
},
"column.destination": {
"defaultMessage": "Cel"
@@ -237,13 +255,13 @@
"defaultMessage": "Panel"
},
"dead-host": {
"defaultMessage": "Host 404"
"defaultMessage": "host 404"
},
"dead-hosts": {
"defaultMessage": "Hosty 404"
"defaultMessage": "404"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host 404} few {Hosty 404} other {Hostów 404}}"
"defaultMessage": "{count} {count, plural, one {host 404} few {hosty 404} other {hostów 404}}"
},
"disabled": {
"defaultMessage": "Wyłączone"
@@ -267,7 +285,7 @@
"defaultMessage": "Wymuś SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS włączone"
"defaultMessage": "Włącz HSTS "
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS dla subdomen"
@@ -336,7 +354,7 @@
"defaultMessage": "Blokuj typowe exploity"
},
"host.flags.cache-assets": {
"defaultMessage": "Buforuj zasoby"
"defaultMessage": "Buforuj zasoby statyczne (ang. cache)"
},
"host.flags.preserve-path": {
"defaultMessage": "Zachowaj ścieżkę"
@@ -348,7 +366,7 @@
"defaultMessage": "Obsługa WebSockets"
},
"host.forward-port": {
"defaultMessage": "Port przekierowania"
"defaultMessage": "Port docelowy"
},
"host.forward-scheme": {
"defaultMessage": "Schemat"
@@ -489,22 +507,22 @@
"defaultMessage": "Tylko utworzone elementy"
},
"proxy-host": {
"defaultMessage": "Host proxy"
"defaultMessage": "host proxy"
},
"proxy-host.forward-host": {
"defaultMessage": "Przekieruj na hostname / IP"
},
"proxy-hosts": {
"defaultMessage": "Hosty proxy"
"defaultMessage": "Proxy"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host proxy} few {Hosty proxy} other {Hostów proxy}}"
"defaultMessage": "{count} {count, plural, one {host proxy} few {hosty proxy} many {hostów proxy} other {hostów proxy}}"
},
"public": {
"defaultMessage": "Publiczne"
},
"redirection-host": {
"defaultMessage": "Host przekierowania"
"defaultMessage": "adres przekierowania"
},
"redirection-host.forward-domain": {
"defaultMessage": "Domena docelowa"
@@ -513,10 +531,10 @@
"defaultMessage": "Kod HTTP"
},
"redirection-hosts": {
"defaultMessage": "Hosty przekierowań"
"defaultMessage": "Przekierowania"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host przekierowania} few {Hosty przekierowań} other {Hostów przekierowań}}"
"defaultMessage": "{count} {count, plural, one {przekierowanie} few {przekierowania} many {przekierowań} other {przekierowań}}"
},
"role.admin": {
"defaultMessage": "Administrator"
@@ -570,7 +588,7 @@
"defaultMessage": "Certyfikat SSL"
},
"stream": {
"defaultMessage": "Strumień"
"defaultMessage": "strumień"
},
"stream.forward-host": {
"defaultMessage": "Host docelowy"
@@ -582,7 +600,7 @@
"defaultMessage": "Strumienie"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Strumień} few {Strumienie} other {Strumieni}}"
"defaultMessage": "{count} {count, plural, one {strumień} few {strumienie} many {strumieni} other {strumieni}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
@@ -594,13 +612,13 @@
"defaultMessage": "Test"
},
"user": {
"defaultMessage": "Użytkownik"
"defaultMessage": "użytkownik"
},
"user.change-password": {
"defaultMessage": "Zmień hasło"
},
"user.confirm-password": {
"defaultMessage": "Potwierdź hasło"
"defaultMessage": "Potwierdź nowe hasło"
},
"user.current-password": {
"defaultMessage": "Aktualne hasło"
@@ -609,7 +627,7 @@
"defaultMessage": "Edytuj profil"
},
"user.full-name": {
"defaultMessage": "Pełne imię i nazwisko"
"defaultMessage": "Imię / Nazwisko"
},
"user.login-as": {
"defaultMessage": "Zaloguj jako {name}"

View File

@@ -155,6 +155,18 @@
"certificates.http.warning": {
"defaultMessage": "Эти домены должны быть настроены и указывать на этот экземпляр."
},
"certificates.key-type": {
"defaultMessage": "Тип ключа"
},
"certificates.key-type-description": {
"defaultMessage": "RSA широко совместим, ECDSA быстрее и безопаснее, но может не поддерживаться старыми системами"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "через Let's Encrypt"
},

View File

@@ -155,6 +155,18 @@
"certificates.http.warning": {
"defaultMessage": "Tieto domény musia byť už nakonfigurované tak, aby smerovali na túto inštaláciu."
},
"certificates.key-type": {
"defaultMessage": "Typ kľúča"
},
"certificates.key-type-description": {
"defaultMessage": "RSA je široko kompatibilný, ECDSA je rýchlejší a bezpečnejší, ale nemusí byť podporovaný staršími systémami"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "pomocou Let's Encrypt"
},

View File

@@ -155,6 +155,18 @@
"certificates.http.warning": {
"defaultMessage": "Các miền này phải được cấu hình sẵn để trỏ đến cài đặt này."
},
"certificates.key-type": {
"defaultMessage": "Loại khóa"
},
"certificates.key-type-description": {
"defaultMessage": "RSA tương thích rộng rãi, ECDSA nhanh hơn và an toàn hơn nhưng có thể không được hỗ trợ bởi các hệ thống cũ"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "bằng Let's Encrypt"
},

12
frontend/src/locale/src/zh.json Executable file → Normal file
View File

@@ -155,6 +155,18 @@
"certificates.http.warning": {
"defaultMessage": "这些域名必须配置为指向本设备。"
},
"certificates.key-type": {
"defaultMessage": "密钥类型"
},
"certificates.key-type-description": {
"defaultMessage": "RSA 兼容性更好ECDSA 更快更安全但旧系统可能不支持"
},
"certificates.key-type-ecdsa": {
"defaultMessage": "ECDSA 256"
},
"certificates.key-type-rsa": {
"defaultMessage": "RSA 2048"
},
"certificates.request.subtitle": {
"defaultMessage": "使用 Let's Encrypt"
},

View File

@@ -1,6 +1,6 @@
import { useQueryClient } from "@tanstack/react-query";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Form, Formik } from "formik";
import { Form, Formik, Field } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
@@ -44,6 +44,7 @@ const DNSCertificateModal = EasyModal.create(({ visible, remove }: InnerModalPro
provider: "letsencrypt",
meta: {
dnsChallenge: true,
keyType: "ecdsa",
},
} as any
}
@@ -63,6 +64,30 @@ const DNSCertificateModal = EasyModal.create(({ visible, remove }: InnerModalPro
<div className="card m-0 border-0">
<div className="card-body">
<DomainNamesField isWildcardPermitted dnsProviderWildcardSupported />
<Field name="meta.keyType">
{({ field }: any) => (
<div className="mb-3">
<label htmlFor="keyType" className="form-label">
<T id="certificates.key-type" />
</label>
<select
id="keyType"
className="form-select"
{...field}
>
<option value="rsa">
<T id="certificates.key-type-rsa" />
</option>
<option value="ecdsa">
<T id="certificates.key-type-ecdsa" />
</option>
</select>
<small className="form-text text-muted">
<T id="certificates.key-type-description" />
</small>
</div>
)}
</Field>
<DNSProviderFields />
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { IconAlertTriangle } from "@tabler/icons-react";
import { useQueryClient } from "@tanstack/react-query";
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Form, Formik } from "formik";
import { Form, Formik, Field } from "formik";
import { type ReactNode, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
@@ -115,6 +115,9 @@ const HTTPCertificateModal = EasyModal.create(({ visible, remove }: InnerModalPr
{
domainNames: [],
provider: "letsencrypt",
meta: {
keyType: "ecdsa",
},
} as any
}
onSubmit={onSubmit}
@@ -142,6 +145,30 @@ const HTTPCertificateModal = EasyModal.create(({ visible, remove }: InnerModalPr
setTestResults(null);
}}
/>
<Field name="meta.keyType">
{({ field }: any) => (
<div className="mb-3">
<label htmlFor="keyType" className="form-label">
<T id="certificates.key-type" />
</label>
<select
id="keyType"
className="form-select"
{...field}
>
<option value="rsa">
<T id="certificates.key-type-rsa" />
</option>
<option value="ecdsa">
<T id="certificates.key-type-ecdsa" />
</option>
</select>
<small className="form-text text-muted">
<T id="certificates.key-type-description" />
</small>
</div>
)}
</Field>
</div>
{testResults ? (
<div className="card-footer">

View File

@@ -0,0 +1,368 @@
import EasyModal, { type InnerModalProps } from "ez-modal-react";
import { Field, Form, Formik } from "formik";
import { type ReactNode, useCallback, useEffect, useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import {
disable2FA,
enable2FA,
get2FAStatus,
regenerateBackupCodes,
start2FASetup,
} from "src/api/backend";
import { Button } from "src/components";
import { T } from "src/locale";
import { validateString } from "src/modules/Validations";
type Step = "loading" | "status" | "setup" | "verify" | "backup" | "disable";
const showTwoFactorModal = (id: number | "me") => {
EasyModal.show(TwoFactorModal, { id });
};
interface Props extends InnerModalProps {
id: number | "me";
}
const TwoFactorModal = EasyModal.create(({ id, visible, remove }: Props) => {
const [error, setError] = useState<ReactNode | null>(null);
const [step, setStep] = useState<Step>("loading");
const [isEnabled, setIsEnabled] = useState(false);
const [backupCodesRemaining, setBackupCodesRemaining] = useState(0);
const [setupData, setSetupData] = useState<{ secret: string; otpauthUrl: string } | null>(null);
const [backupCodes, setBackupCodes] = useState<string[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const loadStatus = useCallback(async () => {
try {
const status = await get2FAStatus(id);
setIsEnabled(status.enabled);
setBackupCodesRemaining(status.backupCodesRemaining);
setStep("status");
} catch (err: any) {
setError(err.message || "Failed to load 2FA status");
setStep("status");
}
}, [id]);
useEffect(() => {
loadStatus();
}, [loadStatus]);
const handleStartSetup = async () => {
setError(null);
setIsSubmitting(true);
try {
const data = await start2FASetup(id);
setSetupData(data);
setStep("setup");
} catch (err: any) {
setError(err.message || "Failed to start 2FA setup");
}
setIsSubmitting(false);
};
const handleVerify = async (values: { code: string }) => {
setError(null);
setIsSubmitting(true);
try {
const result = await enable2FA(id, values.code);
setBackupCodes(result.backupCodes);
setStep("backup");
} catch (err: any) {
setError(err.message || "Failed to enable 2FA");
}
setIsSubmitting(false);
};
const handleDisable = async (values: { code: string }) => {
setError(null);
setIsSubmitting(true);
try {
await disable2FA(id, values.code);
setIsEnabled(false);
setStep("status");
} catch (err: any) {
setError(err.message || "Failed to disable 2FA");
}
setIsSubmitting(false);
};
const handleRegenerateBackup = async (values: { code: string }) => {
setError(null);
setIsSubmitting(true);
try {
const result = await regenerateBackupCodes(id, values.code);
setBackupCodes(result.backupCodes);
setStep("backup");
} catch (err: any) {
setError(err.message || "Failed to regenerate backup codes");
}
setIsSubmitting(false);
};
const handleBackupDone = () => {
setIsEnabled(true);
setBackupCodes([]);
loadStatus();
};
const renderContent = () => {
if (step === "loading") {
return (
<div className="text-center py-4">
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
);
}
if (step === "status") {
return (
<div className="py-2">
<div className="mb-4">
<div className="d-flex align-items-center justify-content-between mb-2">
<span className="fw-bold">
<T id="2fa.status" />
</span>
<span className={`badge text-white ${isEnabled ? "bg-success" : "bg-secondary"}`}>
{isEnabled ? <T id="2fa.enabled" /> : <T id="2fa.disabled" />}
</span>
</div>
{isEnabled && (
<p className="text-muted small mb-0">
<T id="2fa.backup-codes-remaining" data={{ count: backupCodesRemaining }} />
</p>
)}
</div>
{!isEnabled ? (
<Button
fullWidth
color="azure"
onClick={handleStartSetup}
isLoading={isSubmitting}
>
<T id="2fa.enable" />
</Button>
) : (
<div className="d-flex flex-column gap-2">
<Button fullWidth onClick={() => setStep("disable")}>
<T id="2fa.disable" />
</Button>
<Button fullWidth onClick={() => setStep("verify")}>
<T id="2fa.regenerate-backup" />
</Button>
</div>
)}
</div>
);
}
if (step === "setup" && setupData) {
return (
<div className="py-2">
<p className="text-muted mb-3">
<T id="2fa.setup-instructions" />
</p>
<div className="text-center mb-3">
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(setupData.otpauthUrl)}`}
alt="QR Code"
className="img-fluid"
style={{ maxWidth: "200px" }}
/>
</div>
<label className="mb-3 d-block">
<span className="form-label small text-muted">
<T id="2fa.secret-key" />
</span>
<input
type="text"
className="form-control font-monospace"
value={setupData.secret}
readOnly
onClick={(e) => (e.target as HTMLInputElement).select()}
/>
</label>
<Formik initialValues={{ code: "" }} onSubmit={handleVerify}>
{() => (
<Form>
<Field name="code" validate={validateString(6, 6)}>
{({ field, form }: any) => (
<label className="mb-3 d-block">
<span className="form-label">
<T id="2fa.enter-code" />
</span>
<input
{...field}
type="text"
inputMode="numeric"
autoComplete="one-time-code"
className={`form-control ${form.errors.code && form.touched.code ? "is-invalid" : ""}`}
placeholder="000000"
maxLength={6}
/>
<div className="invalid-feedback">{form.errors.code}</div>
</label>
)}
</Field>
<div className="d-flex gap-2">
<Button
type="button"
fullWidth
onClick={() => setStep("status")}
disabled={isSubmitting}
>
<T id="cancel" />
</Button>
<Button type="submit" fullWidth color="azure" isLoading={isSubmitting}>
<T id="2fa.verify-enable" />
</Button>
</div>
</Form>
)}
</Formik>
</div>
);
}
if (step === "backup") {
return (
<div className="py-2">
<Alert variant="warning">
<T id="2fa.backup-warning" />
</Alert>
<div className="mb-3">
<div className="row g-2">
{backupCodes.map((code, index) => (
<div key={index} className="col-6">
<code className="d-block p-2 bg-light rounded text-center">{code}</code>
</div>
))}
</div>
</div>
<Button fullWidth color="azure" onClick={handleBackupDone}>
<T id="2fa.done" />
</Button>
</div>
);
}
if (step === "disable") {
return (
<div className="py-2">
<Alert variant="warning">
<T id="2fa.disable-warning" />
</Alert>
<Formik initialValues={{ code: "" }} onSubmit={handleDisable}>
{() => (
<Form>
<Field name="code" validate={validateString(6, 6)}>
{({ field, form }: any) => (
<label className="mb-3 d-block">
<span className="form-label">
<T id="2fa.enter-code-disable" />
</span>
<input
{...field}
type="text"
inputMode="numeric"
autoComplete="one-time-code"
className={`form-control ${form.errors.code && form.touched.code ? "is-invalid" : ""}`}
placeholder="000000"
maxLength={6}
/>
<div className="invalid-feedback">{form.errors.code}</div>
</label>
)}
</Field>
<div className="d-flex gap-2">
<Button
type="button"
fullWidth
onClick={() => setStep("status")}
disabled={isSubmitting}
>
<T id="cancel" />
</Button>
<Button type="submit" fullWidth color="red" isLoading={isSubmitting}>
<T id="2fa.disable-confirm" />
</Button>
</div>
</Form>
)}
</Formik>
</div>
);
}
if (step === "verify") {
return (
<div className="py-2">
<p className="text-muted mb-3">
<T id="2fa.regenerate-instructions" />
</p>
<Formik initialValues={{ code: "" }} onSubmit={handleRegenerateBackup}>
{() => (
<Form>
<Field name="code" validate={validateString(6, 6)}>
{({ field, form }: any) => (
<label className="mb-3 d-block">
<span className="form-label">
<T id="2fa.enter-code" />
</span>
<input
{...field}
type="text"
inputMode="numeric"
autoComplete="one-time-code"
className={`form-control ${form.errors.code && form.touched.code ? "is-invalid" : ""}`}
placeholder="000000"
maxLength={6}
/>
<div className="invalid-feedback">{form.errors.code}</div>
</label>
)}
</Field>
<div className="d-flex gap-2">
<Button
type="button"
fullWidth
onClick={() => setStep("status")}
disabled={isSubmitting}
>
<T id="cancel" />
</Button>
<Button type="submit" fullWidth color="azure" isLoading={isSubmitting}>
<T id="2fa.regenerate" />
</Button>
</div>
</Form>
)}
</Formik>
</div>
);
}
return null;
};
return (
<Modal show={visible} onHide={remove}>
<Modal.Header closeButton>
<Modal.Title>
<T id="2fa.title" />
</Modal.Title>
</Modal.Header>
<Modal.Body>
<Alert variant="danger" show={!!error} onClose={() => setError(null)} dismissible>
{error}
</Alert>
{renderContent()}
</Modal.Body>
</Modal>
);
});
export { showTwoFactorModal };

View File

@@ -13,4 +13,5 @@ export * from "./RedirectionHostModal";
export * from "./RenewCertificateModal";
export * from "./SetPasswordModal";
export * from "./StreamModal";
export * from "./TwoFactorModal";
export * from "./UserModal";

View File

@@ -8,8 +8,77 @@ import { intl, T } from "src/locale";
import { validateEmail, validateString } from "src/modules/Validations";
import styles from "./index.module.css";
export default function Login() {
const emailRef = useRef(null);
function TwoFactorForm() {
const codeRef = useRef<HTMLInputElement>(null);
const [formErr, setFormErr] = useState("");
const { verifyTwoFactor, cancelTwoFactor } = useAuthState();
const onSubmit = async (values: any, { setSubmitting }: any) => {
setFormErr("");
try {
await verifyTwoFactor(values.code);
} catch (err) {
if (err instanceof Error) {
setFormErr(err.message);
}
}
setSubmitting(false);
};
useEffect(() => {
codeRef.current?.focus();
}, []);
return (
<>
<h2 className="h2 text-center mb-4">
<T id="login.2fa-title" />
</h2>
<p className="text-secondary text-center mb-4">
<T id="login.2fa-description" />
</p>
{formErr !== "" && <Alert variant="danger">{formErr}</Alert>}
<Formik initialValues={{ code: "" }} onSubmit={onSubmit}>
{({ isSubmitting }) => (
<Form>
<div className="mb-3">
<Field name="code" validate={validateString(6, 20)}>
{({ field, form }: any) => (
<label className="form-label">
<T id="login.2fa-code" />
<input
{...field}
ref={codeRef}
type="text"
inputMode="numeric"
autoComplete="one-time-code"
required
maxLength={20}
className={`form-control ${form.errors.code && form.touched.code ? "is-invalid" : ""}`}
placeholder={intl.formatMessage({ id: "login.2fa-code-placeholder" })}
/>
<div className="invalid-feedback">{form.errors.code}</div>
</label>
)}
</Field>
</div>
<div className="form-footer d-flex gap-2">
<Button type="button" fullWidth onClick={cancelTwoFactor} disabled={isSubmitting}>
<T id="cancel" />
</Button>
<Button type="submit" fullWidth color="azure" isLoading={isSubmitting}>
<T id="login.2fa-verify" />
</Button>
</div>
</Form>
)}
</Formik>
</>
);
}
function LoginForm() {
const emailRef = useRef<HTMLInputElement>(null);
const [formErr, setFormErr] = useState("");
const { login } = useAuthState();
@@ -26,10 +95,79 @@ export default function Login() {
};
useEffect(() => {
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
emailRef.current.focus();
emailRef.current?.focus();
}, []);
return (
<>
<h2 className="h2 text-center mb-4">
<T id="login.title" />
</h2>
{formErr !== "" && <Alert variant="danger">{formErr}</Alert>}
<Formik
initialValues={
{
email: "",
password: "",
} as any
}
onSubmit={onSubmit}
>
{({ isSubmitting }) => (
<Form>
<div className="mb-3">
<Field name="email" validate={validateEmail()}>
{({ field, form }: any) => (
<label className="form-label">
<T id="email-address" />
<input
{...field}
ref={emailRef}
type="email"
required
className={`form-control ${form.errors.email && form.touched.email ? " is-invalid" : ""}`}
placeholder={intl.formatMessage({ id: "email-address" })}
/>
<div className="invalid-feedback">{form.errors.email}</div>
</label>
)}
</Field>
</div>
<div className="mb-2">
<Field name="password" validate={validateString(8, 255)}>
{({ field, form }: any) => (
<>
<label className="form-label">
<T id="password" />
<input
{...field}
type="password"
autoComplete="current-password"
required
maxLength={255}
className={`form-control ${form.errors.password && form.touched.password ? " is-invalid" : ""}`}
placeholder={intl.formatMessage({ id: "password" })}
/>
<div className="invalid-feedback">{form.errors.password}</div>
</label>
</>
)}
</Field>
</div>
<div className="form-footer">
<Button type="submit" fullWidth color="azure" isLoading={isSubmitting}>
<T id="sign-in" />
</Button>
</div>
</Form>
)}
</Formik>
</>
);
}
export default function Login() {
const { twoFactorChallenge } = useAuthState();
const health = useHealth();
const getVersion = () => {
@@ -56,68 +194,7 @@ export default function Login() {
</div>
<div className="card card-md">
<div className="card-body">
<h2 className="h2 text-center mb-4">
<T id="login.title" />
</h2>
{formErr !== "" && <Alert variant="danger">{formErr}</Alert>}
<Formik
initialValues={
{
email: "",
password: "",
} as any
}
onSubmit={onSubmit}
>
{({ isSubmitting }) => (
<Form>
<div className="mb-3">
<Field name="email" validate={validateEmail()}>
{({ field, form }: any) => (
<label className="form-label">
<T id="email-address" />
<input
{...field}
ref={emailRef}
type="email"
required
className={`form-control ${form.errors.email && form.touched.email ? " is-invalid" : ""}`}
placeholder={intl.formatMessage({ id: "email-address" })}
/>
<div className="invalid-feedback">{form.errors.email}</div>
</label>
)}
</Field>
</div>
<div className="mb-2">
<Field name="password" validate={validateString(8, 255)}>
{({ field, form }: any) => (
<>
<label className="form-label">
<T id="password" />
<input
{...field}
type="password"
autoComplete="current-password"
required
maxLength={255}
className={`form-control ${form.errors.password && form.touched.password ? " is-invalid" : ""}`}
placeholder={intl.formatMessage({ id: "password" })}
/>
<div className="invalid-feedback">{form.errors.password}</div>
</label>
</>
)}
</Field>
</div>
<div className="form-footer">
<Button type="submit" fullWidth color="azure" isLoading={isSubmitting}>
<T id="sign-in" />
</Button>
</div>
</Form>
)}
</Formik>
{twoFactorChallenge ? <TwoFactorForm /> : <LoginForm />}
</div>
</div>
<div className="text-center text-secondary mt-3">{getVersion()}</div>

File diff suppressed because it is too large Load Diff

View File

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