mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2026-01-21 19:25:43 +00:00
@@ -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>
|
||||
|
||||
@@ -255,6 +255,14 @@
|
||||
"credentials": "dns_gcore_apitoken = 0123456789abcdef0123456789abcdef01234567",
|
||||
"full_plugin_name": "dns-gcore"
|
||||
},
|
||||
"glesys": {
|
||||
"name": "Glesys",
|
||||
"package_name": "certbot-dns-glesys",
|
||||
"version": "~=2.1.0",
|
||||
"dependencies": "",
|
||||
"credentials": "dns_glesys_user = CL00000\ndns_glesys_password = apikeyvalue",
|
||||
"full_plugin_name": "dns-glesys"
|
||||
},
|
||||
"godaddy": {
|
||||
"name": "GoDaddy",
|
||||
"package_name": "certbot-dns-godaddy",
|
||||
@@ -287,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",
|
||||
@@ -367,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",
|
||||
@@ -527,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
288
backend/internal/2fa.js
Normal file
@@ -0,0 +1,288 @@
|
||||
import crypto from "node:crypto";
|
||||
import bcrypt from "bcrypt";
|
||||
import { authenticator } from "otplib";
|
||||
import errs from "../lib/error.js";
|
||||
import authModel from "../models/auth.js";
|
||||
import internalUser from "./user.js";
|
||||
|
||||
const APP_NAME = "Nginx Proxy Manager";
|
||||
const BACKUP_CODE_COUNT = 8;
|
||||
|
||||
/**
|
||||
* Generate backup codes
|
||||
* @returns {Promise<{plain: string[], hashed: string[]}>}
|
||||
*/
|
||||
const generateBackupCodes = async () => {
|
||||
const plain = [];
|
||||
const hashed = [];
|
||||
|
||||
for (let i = 0; i < BACKUP_CODE_COUNT; i++) {
|
||||
const code = crypto.randomBytes(4).toString("hex").toUpperCase();
|
||||
plain.push(code);
|
||||
const hash = await bcrypt.hash(code, 10);
|
||||
hashed.push(hash);
|
||||
}
|
||||
|
||||
return { plain, hashed };
|
||||
};
|
||||
|
||||
const internal2fa = {
|
||||
|
||||
/**
|
||||
* Check if user has 2FA enabled
|
||||
* @param {number} userId
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
isEnabled: async (userId) => {
|
||||
const auth = await internal2fa.getUserPasswordAuth(userId);
|
||||
return auth?.meta?.totp_enabled === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get 2FA status for user
|
||||
* @param {Access} access
|
||||
* @param {number} userId
|
||||
* @returns {Promise<{enabled: boolean, backup_codes_remaining: number}>}
|
||||
*/
|
||||
getStatus: async (access, userId) => {
|
||||
await access.can("users:password", userId);
|
||||
await internalUser.get(access, { id: userId });
|
||||
const auth = await internal2fa.getUserPasswordAuth(userId);
|
||||
const enabled = auth?.meta?.totp_enabled === true;
|
||||
let backup_codes_remaining = 0;
|
||||
|
||||
if (enabled) {
|
||||
const backupCodes = auth.meta.backup_codes || [];
|
||||
backup_codes_remaining = backupCodes.length;
|
||||
}
|
||||
|
||||
return {
|
||||
enabled,
|
||||
backup_codes_remaining,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Start 2FA setup - store pending secret
|
||||
*
|
||||
* @param {Access} access
|
||||
* @param {number} userId
|
||||
* @returns {Promise<{secret: string, otpauth_url: string}>}
|
||||
*/
|
||||
startSetup: async (access, userId) => {
|
||||
await access.can("users:password", userId);
|
||||
const user = await internalUser.get(access, { id: userId });
|
||||
const secret = authenticator.generateSecret();
|
||||
const otpauth_url = authenticator.keyuri(user.email, APP_NAME, secret);
|
||||
const auth = await internal2fa.getUserPasswordAuth(userId);
|
||||
|
||||
// ensure user isn't already setup for 2fa
|
||||
const enabled = auth?.meta?.totp_enabled === true;
|
||||
if (enabled) {
|
||||
throw new errs.ValidationError("2FA is already enabled");
|
||||
}
|
||||
|
||||
const meta = auth.meta || {};
|
||||
meta.totp_pending_secret = secret;
|
||||
|
||||
await authModel.query()
|
||||
.where("id", auth.id)
|
||||
.andWhere("user_id", userId)
|
||||
.andWhere("type", "password")
|
||||
.patch({ meta });
|
||||
|
||||
return { secret, otpauth_url };
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable 2FA after verifying code
|
||||
*
|
||||
* @param {Access} access
|
||||
* @param {number} userId
|
||||
* @param {string} code
|
||||
* @returns {Promise<{backup_codes: string[]}>}
|
||||
*/
|
||||
enable: async (access, userId, code) => {
|
||||
await access.can("users:password", userId);
|
||||
await internalUser.get(access, { id: userId });
|
||||
const auth = await internal2fa.getUserPasswordAuth(userId);
|
||||
const secret = auth?.meta?.totp_pending_secret || false;
|
||||
|
||||
if (!secret) {
|
||||
throw new errs.ValidationError("No pending 2FA setup found");
|
||||
}
|
||||
|
||||
const valid = authenticator.verify({ token: code, secret });
|
||||
if (!valid) {
|
||||
throw new errs.ValidationError("Invalid verification code");
|
||||
}
|
||||
|
||||
const { plain, hashed } = await generateBackupCodes();
|
||||
|
||||
const meta = {
|
||||
...auth.meta,
|
||||
totp_secret: secret,
|
||||
totp_enabled: true,
|
||||
totp_enabled_at: new Date().toISOString(),
|
||||
backup_codes: hashed,
|
||||
};
|
||||
delete meta.totp_pending_secret;
|
||||
|
||||
await authModel
|
||||
.query()
|
||||
.where("id", auth.id)
|
||||
.andWhere("user_id", userId)
|
||||
.andWhere("type", "password")
|
||||
.patch({ meta });
|
||||
|
||||
return { backup_codes: plain };
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable 2FA
|
||||
*
|
||||
* @param {Access} access
|
||||
* @param {number} userId
|
||||
* @param {string} code
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
disable: async (access, userId, code) => {
|
||||
await access.can("users:password", userId);
|
||||
await internalUser.get(access, { id: userId });
|
||||
const auth = await internal2fa.getUserPasswordAuth(userId);
|
||||
|
||||
const enabled = auth?.meta?.totp_enabled === true;
|
||||
if (!enabled) {
|
||||
throw new errs.ValidationError("2FA is not enabled");
|
||||
}
|
||||
|
||||
const valid = authenticator.verify({
|
||||
token: code,
|
||||
secret: auth.meta.totp_secret,
|
||||
});
|
||||
|
||||
if (!valid) {
|
||||
throw new errs.AuthError("Invalid verification code");
|
||||
}
|
||||
|
||||
const meta = { ...auth.meta };
|
||||
delete meta.totp_secret;
|
||||
delete meta.totp_enabled;
|
||||
delete meta.totp_enabled_at;
|
||||
delete meta.backup_codes;
|
||||
|
||||
await authModel
|
||||
.query()
|
||||
.where("id", auth.id)
|
||||
.andWhere("user_id", userId)
|
||||
.andWhere("type", "password")
|
||||
.patch({ meta });
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify 2FA code for login
|
||||
*
|
||||
* @param {number} userId
|
||||
* @param {string} token
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
verifyForLogin: async (userId, token) => {
|
||||
const auth = await internal2fa.getUserPasswordAuth(userId);
|
||||
const secret = auth?.meta?.totp_secret || false;
|
||||
|
||||
if (!secret) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try TOTP code first
|
||||
const valid = authenticator.verify({
|
||||
token,
|
||||
secret,
|
||||
});
|
||||
|
||||
if (valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try backup codes
|
||||
const backupCodes = auth?.meta?.backup_codes || [];
|
||||
for (let i = 0; i < backupCodes.length; i++) {
|
||||
const match = await bcrypt.compare(code.toUpperCase(), backupCodes[i]);
|
||||
if (match) {
|
||||
// Remove used backup code
|
||||
const updatedCodes = [...backupCodes];
|
||||
updatedCodes.splice(i, 1);
|
||||
const meta = { ...auth.meta, backup_codes: updatedCodes };
|
||||
await authModel
|
||||
.query()
|
||||
.where("id", auth.id)
|
||||
.andWhere("user_id", userId)
|
||||
.andWhere("type", "password")
|
||||
.patch({ meta });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Regenerate backup codes
|
||||
*
|
||||
* @param {Access} access
|
||||
* @param {number} userId
|
||||
* @param {string} token
|
||||
* @returns {Promise<{backup_codes: string[]}>}
|
||||
*/
|
||||
regenerateBackupCodes: async (access, userId, token) => {
|
||||
await access.can("users:password", userId);
|
||||
await internalUser.get(access, { id: userId });
|
||||
const auth = await internal2fa.getUserPasswordAuth(userId);
|
||||
const enabled = auth?.meta?.totp_enabled === true;
|
||||
const secret = auth?.meta?.totp_secret || false;
|
||||
|
||||
if (!enabled) {
|
||||
throw new errs.ValidationError("2FA is not enabled");
|
||||
}
|
||||
if (!secret) {
|
||||
throw new errs.ValidationError("No 2FA secret found");
|
||||
}
|
||||
|
||||
const valid = authenticator.verify({
|
||||
token,
|
||||
secret,
|
||||
});
|
||||
|
||||
if (!valid) {
|
||||
throw new errs.ValidationError("Invalid verification code");
|
||||
}
|
||||
|
||||
const { plain, hashed } = await generateBackupCodes();
|
||||
|
||||
const meta = { ...auth.meta, backup_codes: hashed };
|
||||
await authModel
|
||||
.query()
|
||||
.where("id", auth.id)
|
||||
.andWhere("user_id", userId)
|
||||
.andWhere("type", "password")
|
||||
.patch({ meta });
|
||||
|
||||
return { backup_codes: plain };
|
||||
},
|
||||
|
||||
getUserPasswordAuth: async (userId) => {
|
||||
const auth = await authModel
|
||||
.query()
|
||||
.where("user_id", userId)
|
||||
.andWhere("type", "password")
|
||||
.first();
|
||||
|
||||
if (!auth) {
|
||||
throw new errs.ItemNotFoundError("Auth not found");
|
||||
}
|
||||
|
||||
return auth;
|
||||
},
|
||||
};
|
||||
|
||||
export default internal2fa;
|
||||
@@ -798,6 +798,11 @@ const internalCertificate = {
|
||||
certificate.domain_names.join(","),
|
||||
];
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -71,6 +71,11 @@
|
||||
"propagation_seconds": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"key_type": {
|
||||
"type": "string",
|
||||
"enum": ["rsa", "ecdsa"],
|
||||
"default": "rsa"
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
|
||||
18
backend/schema/components/token-challenge.json
Normal file
18
backend/schema/components/token-challenge.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Token object",
|
||||
"required": ["requires_2fa", "challenge_token"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"requires_2fa": {
|
||||
"description": "Whether this token request requires two-factor authentication",
|
||||
"example": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"challenge_token": {
|
||||
"description": "Challenge Token used in subsequent 2FA verification",
|
||||
"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
55
backend/schema/paths/tokens/2fa/post.json
Normal file
55
backend/schema/paths/tokens/2fa/post.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"operationId": "loginWith2FA",
|
||||
"summary": "Verify 2FA code and get full token",
|
||||
"tags": ["tokens"],
|
||||
"requestBody": {
|
||||
"description": "2fa Challenge Payload",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"challenge_token": {
|
||||
"minLength": 1,
|
||||
"type": "string",
|
||||
"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
|
||||
},
|
||||
"code": {
|
||||
"minLength": 6,
|
||||
"maxLength": 6,
|
||||
"type": "string",
|
||||
"example": "012345"
|
||||
}
|
||||
},
|
||||
"required": ["challenge_token", "code"],
|
||||
"type": "object"
|
||||
},
|
||||
"example": {
|
||||
"challenge_token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
|
||||
"code": "012345"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"examples": {
|
||||
"default": {
|
||||
"value": {
|
||||
"expires": "2025-02-04T20:40:46.340Z",
|
||||
"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"$ref": "../../../components/token-object.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "200 response"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,14 @@
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"$ref": "../../components/token-object.json"
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "../../components/token-object.json"
|
||||
},
|
||||
{
|
||||
"$ref": "../../components/token-challenge.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
92
backend/schema/paths/users/userID/2fa/backup-codes/post.json
Normal file
92
backend/schema/paths/users/userID/2fa/backup-codes/post.json
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"operationId": "regenUser2faCodes",
|
||||
"summary": "Regenerate 2FA backup codes",
|
||||
"tags": ["users"],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "userID",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"required": true,
|
||||
"description": "User ID",
|
||||
"example": 2
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Verififcation Payload",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"code": {
|
||||
"minLength": 6,
|
||||
"maxLength": 6,
|
||||
"type": "string",
|
||||
"example": "123456"
|
||||
}
|
||||
},
|
||||
"required": ["code"],
|
||||
"type": "object"
|
||||
},
|
||||
"example": {
|
||||
"code": "123456"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"examples": {
|
||||
"default": {
|
||||
"value": {
|
||||
"backup_codes": [
|
||||
"6CD7CB06",
|
||||
"495302F3",
|
||||
"D8037852",
|
||||
"A6FFC956",
|
||||
"BC1A1851",
|
||||
"A05E644F",
|
||||
"A406D2E8",
|
||||
"0AE3C522"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["backup_codes"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"backup_codes": {
|
||||
"description": "Backup codes",
|
||||
"example": [
|
||||
"6CD7CB06",
|
||||
"495302F3",
|
||||
"D8037852",
|
||||
"A6FFC956",
|
||||
"BC1A1851",
|
||||
"A05E644F",
|
||||
"A406D2E8",
|
||||
"0AE3C522"
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"example": "6CD7CB06"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "200 response"
|
||||
}
|
||||
}
|
||||
}
|
||||
48
backend/schema/paths/users/userID/2fa/delete.json
Normal file
48
backend/schema/paths/users/userID/2fa/delete.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"operationId": "disableUser2fa",
|
||||
"summary": "Disable 2fa for user",
|
||||
"tags": ["users"],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "userID",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"required": true,
|
||||
"description": "User ID",
|
||||
"example": 2
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "code",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 6,
|
||||
"maxLength": 6,
|
||||
"example": "012345"
|
||||
},
|
||||
"required": true,
|
||||
"description": "2fa Code",
|
||||
"example": "012345"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"examples": {
|
||||
"default": {
|
||||
"value": true
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "200 response"
|
||||
}
|
||||
}
|
||||
}
|
||||
92
backend/schema/paths/users/userID/2fa/enable/post.json
Normal file
92
backend/schema/paths/users/userID/2fa/enable/post.json
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"operationId": "enableUser2fa",
|
||||
"summary": "Verify code and enable 2FA",
|
||||
"tags": ["users"],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "userID",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"required": true,
|
||||
"description": "User ID",
|
||||
"example": 2
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Verififcation Payload",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"code": {
|
||||
"minLength": 6,
|
||||
"maxLength": 6,
|
||||
"type": "string",
|
||||
"example": "123456"
|
||||
}
|
||||
},
|
||||
"required": ["code"],
|
||||
"type": "object"
|
||||
},
|
||||
"example": {
|
||||
"code": "123456"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"examples": {
|
||||
"default": {
|
||||
"value": {
|
||||
"backup_codes": [
|
||||
"6CD7CB06",
|
||||
"495302F3",
|
||||
"D8037852",
|
||||
"A6FFC956",
|
||||
"BC1A1851",
|
||||
"A05E644F",
|
||||
"A406D2E8",
|
||||
"0AE3C522"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["backup_codes"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"backup_codes": {
|
||||
"description": "Backup codes",
|
||||
"example": [
|
||||
"6CD7CB06",
|
||||
"495302F3",
|
||||
"D8037852",
|
||||
"A6FFC956",
|
||||
"BC1A1851",
|
||||
"A05E644F",
|
||||
"A406D2E8",
|
||||
"0AE3C522"
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"example": "6CD7CB06"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "200 response"
|
||||
}
|
||||
}
|
||||
}
|
||||
57
backend/schema/paths/users/userID/2fa/get.json
Normal file
57
backend/schema/paths/users/userID/2fa/get.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"operationId": "getUser2faStatus",
|
||||
"summary": "Get user 2fa Status",
|
||||
"tags": ["users"],
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "userID",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"required": true,
|
||||
"description": "User ID",
|
||||
"example": 2
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "200 response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"examples": {
|
||||
"default": {
|
||||
"value": {
|
||||
"enabled": false,
|
||||
"backup_codes_remaining": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["enabled", "backup_codes_remaining"],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Is 2FA enabled for this user",
|
||||
"example": true
|
||||
},
|
||||
"backup_codes_remaining": {
|
||||
"type": "integer",
|
||||
"description": "Number of remaining backup codes for this user",
|
||||
"example": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
backend/schema/paths/users/userID/2fa/post.json
Normal file
52
backend/schema/paths/users/userID/2fa/post.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"operationId": "setupUser2fa",
|
||||
"summary": "Start 2FA setup, returns QR code URL",
|
||||
"tags": ["users"],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "userID",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"required": true,
|
||||
"description": "User ID",
|
||||
"example": 2
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"examples": {
|
||||
"default": {
|
||||
"value": {
|
||||
"secret": "JZYCEBIEEJYUGPQM",
|
||||
"otpauth_url": "otpauth://totp/Nginx%20Proxy%20Manager:jc%40jc21.com?secret=JZYCEBIEEJYUGPQM&period=30&digits=6&algorithm=SHA1&issuer=Nginx%20Proxy%20Manager"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["secret", "otpauth_url"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"secret": {
|
||||
"description": "TOTP Secret",
|
||||
"example": "JZYCEBIEEJYUGPQM",
|
||||
"type": "string"
|
||||
},
|
||||
"otpauth_url": {
|
||||
"description": "OTP Auth URL for QR Code generation",
|
||||
"example": "otpauth://totp/Nginx%20Proxy%20Manager:jc%40jc21.com?secret=JZYCEBIEEJYUGPQM&period=30&digits=6&algorithm=SHA1&issuer=Nginx%20Proxy%20Manager",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "200 response"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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==
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
3
docker/rootfs/etc/nginx/conf.d/include/log-stream.conf
Normal file
3
docker/rootfs/etc/nginx/conf.d/include/log-stream.conf
Normal file
@@ -0,0 +1,3 @@
|
||||
log_format stream '[$time_local] [Client $remote_addr:$remote_port] $protocol $status $bytes_sent $bytes_received $session_time [Sent-to $upstream_addr] [Sent $upstream_bytes_sent] [Received $upstream_bytes_received] [Time $upstream_connect_time] $ssl_protocol $ssl_cipher';
|
||||
|
||||
access_log /data/logs/fallback_stream_access.log stream;
|
||||
@@ -47,7 +47,7 @@ http {
|
||||
proxy_cache_path /var/lib/nginx/cache/private levels=1:2 keys_zone=private-cache:5m max_size=1024m;
|
||||
|
||||
# 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;
|
||||
|
||||
|
||||
946
docs/yarn.lock
946
docs/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,9 @@ const allLocales = [
|
||||
["sk", "sk-SK"],
|
||||
["vi", "vi-VN"],
|
||||
["zh", "zh-CN"],
|
||||
["ko", "ko-KR"],
|
||||
["bg", "bg-BG"],
|
||||
["id", "id-ID"],
|
||||
];
|
||||
|
||||
const ignoreUnused = [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,3 +60,4 @@ export * from "./updateStream";
|
||||
export * from "./updateUser";
|
||||
export * from "./uploadCertificate";
|
||||
export * from "./validateCertificate";
|
||||
export * from "./twoFactor";
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
37
frontend/src/api/backend/twoFactor.ts
Normal file
37
frontend/src/api/backend/twoFactor.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import * as api from "./base";
|
||||
import type { TwoFactorEnableResponse, TwoFactorSetupResponse, TwoFactorStatusResponse } from "./responseTypes";
|
||||
|
||||
export async function get2FAStatus(userId: number | "me"): Promise<TwoFactorStatusResponse> {
|
||||
return await api.get({
|
||||
url: `/users/${userId}/2fa`,
|
||||
});
|
||||
}
|
||||
|
||||
export async function start2FASetup(userId: number | "me"): Promise<TwoFactorSetupResponse> {
|
||||
return await api.post({
|
||||
url: `/users/${userId}/2fa`,
|
||||
});
|
||||
}
|
||||
|
||||
export async function enable2FA(userId: number | "me", code: string): Promise<TwoFactorEnableResponse> {
|
||||
return await api.post({
|
||||
url: `/users/${userId}/2fa/enable`,
|
||||
data: { code },
|
||||
});
|
||||
}
|
||||
|
||||
export async function disable2FA(userId: number | "me", code: string): Promise<boolean> {
|
||||
return await api.del({
|
||||
url: `/users/${userId}/2fa`,
|
||||
params: {
|
||||
code,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function regenerateBackupCodes(userId: number | "me", code: string): Promise<TwoFactorEnableResponse> {
|
||||
return await api.post({
|
||||
url: `/users/${userId}/2fa/backup-codes`,
|
||||
data: { code },
|
||||
});
|
||||
}
|
||||
@@ -3,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" />,
|
||||
|
||||
@@ -116,7 +116,7 @@ export function DNSProviderFields({ showBoundaryBox = false }: Props) {
|
||||
type="number"
|
||||
className="form-control"
|
||||
min={0}
|
||||
max={600}
|
||||
max={7200}
|
||||
{...field}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
|
||||
@@ -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" />,
|
||||
})) || [];
|
||||
|
||||
|
||||
@@ -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="?"
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }} />
|
||||
— <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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -11,6 +12,9 @@ import langRu from "./lang/ru.json";
|
||||
import langSk from "./lang/sk.json";
|
||||
import langVi from "./lang/vi.json";
|
||||
import langZh from "./lang/zh.json";
|
||||
import langKo from "./lang/ko.json";
|
||||
import langBg from "./lang/bg.json";
|
||||
import langId from "./lang/id.json";
|
||||
|
||||
// first item of each array should be the language code,
|
||||
// not the country code
|
||||
@@ -19,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],
|
||||
@@ -27,6 +32,9 @@ const localeOptions = [
|
||||
["sk", "sk-SK", langSk],
|
||||
["vi", "vi-VN", langVi],
|
||||
["zh", "zh-CN", langZh],
|
||||
["ko", "ko-KR", langKo],
|
||||
["bg", "bg-BG", langBg],
|
||||
["id", "id-ID", langId],
|
||||
];
|
||||
|
||||
const loadMessages = (locale?: string): typeof langList & typeof langEn => {
|
||||
@@ -47,6 +55,8 @@ const getFlagCodeForLocale = (locale?: string) => {
|
||||
const specialCases: Record<string, string> = {
|
||||
ja: "jp", // Japan
|
||||
zh: "cn", // China
|
||||
vi: "vn", // Vietnam
|
||||
ko: "kr", // Korea
|
||||
};
|
||||
|
||||
if (specialCases[thisLocale]) {
|
||||
|
||||
@@ -40,6 +40,7 @@ not be complete by the time you're reading this:
|
||||
- frontend/src/locale/src/[yourlang].json
|
||||
- frontend/src/locale/src/lang-list.json
|
||||
- frontend/src/locale/src/HelpDoc/[yourlang]/*
|
||||
- frontend/src/locale/src/HelpDoc/index.tsx
|
||||
- frontend/src/locale/IntlProvider.tsx
|
||||
- frontend/check-locales.cjs
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
7
frontend/src/locale/src/HelpDoc/bg/AccessLists.md
Normal file
7
frontend/src/locale/src/HelpDoc/bg/AccessLists.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## Какво представлява Списъкът за достъп?
|
||||
|
||||
Списъците за достъп предоставят черен или бял списък от конкретни клиентски IP адреси, както и удостоверяване за Прокси хостове чрез базова HTTP автентикация.
|
||||
|
||||
Можете да конфигурирате множество клиентски правила, потребителски имена и пароли в един Списък за достъп и след това да го приложите към един или повече _Прокси хостове_.
|
||||
|
||||
Това е най-полезно при препращани уеб услуги, които нямат вградени механизми за удостоверяване, или когато искате да защитите достъпа от неизвестни клиенти.
|
||||
21
frontend/src/locale/src/HelpDoc/bg/Certificates.md
Normal file
21
frontend/src/locale/src/HelpDoc/bg/Certificates.md
Normal file
@@ -0,0 +1,21 @@
|
||||
## Помощ за сертификати
|
||||
|
||||
### HTTP сертификат
|
||||
|
||||
HTTP валидираният сертификат означава, че сървърите на Let’s Encrypt ще се опитат да достигнат вашите домейни по HTTP (не по HTTPS!) и ако успеят, ще издадат сертификата.
|
||||
|
||||
За този метод трябва да имате създаден _Прокси хост_ за вашия/вашите домейни, който да е достъпен по HTTP и да сочи към тази Nginx инсталация. След като бъде издаден сертификат, можете да промените _Прокси хоста_ така, че да използва сертификата и за HTTPS връзки. Въпреки това, _Прокси хостът_ трябва да остане конфигуриран за достъп по HTTP, за да може сертификатът да се подновява.
|
||||
|
||||
Този процес _не_ поддържа wildcard домейни.
|
||||
|
||||
### DNS сертификат
|
||||
|
||||
DNS валидираният сертификат изисква използването на DNS Provider плъгин. Този DNS Provider ще бъде използван за временно създаване на записи във вашия домейн, след което Let’s Encrypt ще ги провери, за да се увери, че сте собственикът, и при успех ще издаде сертификата.
|
||||
|
||||
Не е необходимо да имате _Прокси хост_, създаден предварително, за да заявите този тип сертификат. Нито е нужно вашият _Прокси хост_ да бъде конфигуриран за достъп по HTTP.
|
||||
|
||||
Този процес _поддържа_ wildcard домейни.
|
||||
|
||||
### Персонализиран сертификат
|
||||
|
||||
Използвайте тази опция, за да качите собствен SSL сертификат, предоставен от ваша сертификатна агенция.
|
||||
10
frontend/src/locale/src/HelpDoc/bg/DeadHosts.md
Normal file
10
frontend/src/locale/src/HelpDoc/bg/DeadHosts.md
Normal file
@@ -0,0 +1,10 @@
|
||||
## Какво представлява 404 хост?
|
||||
|
||||
404 хост е просто конфигурация на хост, който показва страница с грешка 404.
|
||||
|
||||
Това може да е полезно, когато вашият домейн е индексиран в търсачките и искате
|
||||
да предоставите по-приятна страница за грешка или да уведомите индексиращите системи,
|
||||
че страниците на домейна вече не съществуват.
|
||||
|
||||
Допълнително предимство на този хост е възможността да проследявате логовете на заявките
|
||||
към него и да виждате реферерите.
|
||||
7
frontend/src/locale/src/HelpDoc/bg/ProxyHosts.md
Normal file
7
frontend/src/locale/src/HelpDoc/bg/ProxyHosts.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## Какво представлява Прокси хост?
|
||||
|
||||
Прокси хост е входна точка за уеб услуга, която искате да препращате.
|
||||
|
||||
Той предоставя възможност за SSL терминaция на услуга, която може да няма вградена поддръжка на SSL.
|
||||
|
||||
Прокси хостовете са най-често използваната функция в Nginx Proxy Manager.
|
||||
7
frontend/src/locale/src/HelpDoc/bg/RedirectionHosts.md
Normal file
7
frontend/src/locale/src/HelpDoc/bg/RedirectionHosts.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## Какво представлява Хост за пренасочване?
|
||||
|
||||
Хостът за пренасочване пренасочва заявките от входящия домейн и прехвърля
|
||||
потребителя към друг домейн.
|
||||
|
||||
Най-честата причина за използване на този тип хост е, когато вашият уебсайт
|
||||
промени домейна си, но все още има линкове от търсачки или реферери, които сочат към стария домейн.
|
||||
6
frontend/src/locale/src/HelpDoc/bg/Streams.md
Normal file
6
frontend/src/locale/src/HelpDoc/bg/Streams.md
Normal file
@@ -0,0 +1,6 @@
|
||||
## Какво представлява Потокът (Stream)?
|
||||
|
||||
Относително нова функция за Nginx, Потокът позволява препращане на TCP/UDP
|
||||
трафик директно към друг компютър в мрежата.
|
||||
|
||||
Това е полезно, ако хоствате игрови сървъри, FTP или SSH сървъри.
|
||||
6
frontend/src/locale/src/HelpDoc/bg/index.ts
Normal file
6
frontend/src/locale/src/HelpDoc/bg/index.ts
Normal 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";
|
||||
7
frontend/src/locale/src/HelpDoc/ga/AccessLists.md
Normal file
7
frontend/src/locale/src/HelpDoc/ga/AccessLists.md
Normal 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.
|
||||
21
frontend/src/locale/src/HelpDoc/ga/Certificates.md
Normal file
21
frontend/src/locale/src/HelpDoc/ga/Certificates.md
Normal 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 é.
|
||||
7
frontend/src/locale/src/HelpDoc/ga/DeadHosts.md
Normal file
7
frontend/src/locale/src/HelpDoc/ga/DeadHosts.md
Normal 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í.
|
||||
7
frontend/src/locale/src/HelpDoc/ga/ProxyHosts.md
Normal file
7
frontend/src/locale/src/HelpDoc/ga/ProxyHosts.md
Normal 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.
|
||||
5
frontend/src/locale/src/HelpDoc/ga/RedirectionHosts.md
Normal file
5
frontend/src/locale/src/HelpDoc/ga/RedirectionHosts.md
Normal 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.
|
||||
5
frontend/src/locale/src/HelpDoc/ga/Streams.md
Normal file
5
frontend/src/locale/src/HelpDoc/ga/Streams.md
Normal 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, d’fhéadfadh sé seo a bheith úsáideach.
|
||||
6
frontend/src/locale/src/HelpDoc/ga/index.ts
Normal file
6
frontend/src/locale/src/HelpDoc/ga/index.ts
Normal 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";
|
||||
7
frontend/src/locale/src/HelpDoc/id/AccessLists.md
Normal file
7
frontend/src/locale/src/HelpDoc/id/AccessLists.md
Normal 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.
|
||||
32
frontend/src/locale/src/HelpDoc/id/Certificates.md
Normal file
32
frontend/src/locale/src/HelpDoc/id/Certificates.md
Normal 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.
|
||||
10
frontend/src/locale/src/HelpDoc/id/DeadHosts.md
Normal file
10
frontend/src/locale/src/HelpDoc/id/DeadHosts.md
Normal 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.
|
||||
7
frontend/src/locale/src/HelpDoc/id/ProxyHosts.md
Normal file
7
frontend/src/locale/src/HelpDoc/id/ProxyHosts.md
Normal 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.
|
||||
5
frontend/src/locale/src/HelpDoc/id/RedirectionHosts.md
Normal file
5
frontend/src/locale/src/HelpDoc/id/RedirectionHosts.md
Normal 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.
|
||||
6
frontend/src/locale/src/HelpDoc/id/Streams.md
Normal file
6
frontend/src/locale/src/HelpDoc/id/Streams.md
Normal 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.
|
||||
6
frontend/src/locale/src/HelpDoc/id/index.ts
Normal file
6
frontend/src/locale/src/HelpDoc/id/index.ts
Normal 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";
|
||||
@@ -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";
|
||||
@@ -8,8 +10,10 @@ import * as ru from "./ru/index";
|
||||
import * as sk from "./sk/index";
|
||||
import * as vi from "./vi/index";
|
||||
import * as zh from "./zh/index";
|
||||
import * as ko from "./ko/index";
|
||||
import * as bg from "./bg/index";
|
||||
|
||||
const items: any = { en, de, ja, sk, zh, pl, ru, it, vi, nl };
|
||||
const items: any = { en, de, ja, sk, zh, pl, ru, it, vi, nl, bg, ko, ga, id }
|
||||
|
||||
const fallbackLang = "en";
|
||||
|
||||
|
||||
11
frontend/src/locale/src/HelpDoc/ko/AccessLists.md
Normal file
11
frontend/src/locale/src/HelpDoc/ko/AccessLists.md
Normal file
@@ -0,0 +1,11 @@
|
||||
## 접근 정책이란?
|
||||
|
||||
접근 정책은 특정 클라이언트 IP 주소를 허용하거나 거부할 수 있으며,
|
||||
프록시 호스트에 기본 HTTP 인증(Basic Auth) 을 적용할 수 있는 기능입니다.
|
||||
|
||||
하나의 접근 목록에 여러 클라이언트 규칙과 사용자 이름, 비밀번호를 추가한 뒤
|
||||
이를 하나 이상의 프록시 호스트에 적용할 수 있습니다.
|
||||
|
||||
이 기능은 인증 기능이 없는 웹 서비스에 인증을 추가하거나,
|
||||
알 수 없는 클라이언트로부터 서비스를 보호할 때 유용합니다.
|
||||
|
||||
28
frontend/src/locale/src/HelpDoc/ko/Certificates.md
Normal file
28
frontend/src/locale/src/HelpDoc/ko/Certificates.md
Normal file
@@ -0,0 +1,28 @@
|
||||
## 인증서 도움말
|
||||
|
||||
### HTTP 인증서
|
||||
|
||||
HTTP 검증 방식의 인증서는 Let's Encrypt 서버가 **HTTPS가 아닌 HTTP로** 해당 도메인에 접속을 시도해 응답이 확인되면 인증서를 발급하는 방식입니다.
|
||||
|
||||
이 방식을 사용하려면 도메인에 대한 **프록시 호스트가 미리 생성되어 있어야 하며**, HTTP로 접근할 수 있어야 하고 Nginx Proxy Manager가 설치된 서버를 가리켜야 합니다. 인증서가 발급된 이후에는 해당 프록시 호스트에 HTTPS용 인증서를 적용할 수 있습니다.
|
||||
|
||||
다만, **인증서 자동 갱신을 위해서는 HTTP 접근이 계속 필요합니다.**
|
||||
|
||||
이 방식은 **와일드카드 도메인을 지원하지 않습니다.**
|
||||
|
||||
---
|
||||
|
||||
### DNS 인증서
|
||||
|
||||
DNS 검증 방식의 인증서는 DNS 공급자 플러그인을 사용해야 합니다. 이 플러그인은 도메인에 임시 DNS 레코드를 생성하며, Let's Encrypt는 해당 레코드를 조회해 도메인 소유 여부를 확인합니다. 검증이 성공하면 인증서가 발급됩니다.
|
||||
|
||||
이 방식은 인증서를 요청하기 전에 **프록시 호스트를 생성할 필요가 없으며**, 프록시 호스트에 HTTP 접근을 설정할 필요도 없습니다.
|
||||
|
||||
이 방식은 **와일드카드 도메인을 지원합니다.**
|
||||
|
||||
---
|
||||
|
||||
### 사용자 지정 인증서
|
||||
|
||||
이 옵션을 사용하면 직접 보유한 인증 기관(CA)에서 발급한 SSL 인증서를 직접 업로드하여 사용할 수 있습니다.
|
||||
|
||||
10
frontend/src/locale/src/HelpDoc/ko/DeadHosts.md
Normal file
10
frontend/src/locale/src/HelpDoc/ko/DeadHosts.md
Normal file
@@ -0,0 +1,10 @@
|
||||
## 404 호스트란?
|
||||
|
||||
404 호스트는 404 오류 페이지를 표시하도록 구성된 호스트입니다.
|
||||
|
||||
이 기능은 도메인이 검색 엔진에 이미 색인되어 있을 때,
|
||||
더 깔끔한 오류 페이지를 제공하거나 해당 페이지가 더 이상 존재하지 않음을
|
||||
검색 엔진에게 명확하게 알려야 할 때 유용합니다.
|
||||
|
||||
또한 404 호스트를 사용하면 접근 로그를 확인하고, 어떤 경로(Referrer)를 통해 들어왔는지 추적할 수 있다는 장점도 있습니다.
|
||||
|
||||
8
frontend/src/locale/src/HelpDoc/ko/ProxyHosts.md
Normal file
8
frontend/src/locale/src/HelpDoc/ko/ProxyHosts.md
Normal file
@@ -0,0 +1,8 @@
|
||||
## 프록시 호스트란?
|
||||
|
||||
프록시 호스트는 외부에서 들어오는 웹 요청을 받아 지정한 전달 대상으로 전달하는 역할을 합니다.
|
||||
|
||||
원래 SSL을 지원하지 않는 대상이라도, 프록시 호스트를 통해 SSL(HTTPS) 연결을 적용할 수 있습니다.
|
||||
|
||||
프록시 호스트는 Nginx Proxy Manager에서 가장 일반적으로 사용되는 기능입니다.
|
||||
|
||||
7
frontend/src/locale/src/HelpDoc/ko/RedirectionHosts.md
Normal file
7
frontend/src/locale/src/HelpDoc/ko/RedirectionHosts.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## 리다이렉션 호스트란?
|
||||
|
||||
리다이렉션 호스트는 외부에서 들어오는 도메인 요청을 다른 도메인으로 자동 이동(리다이렉트)시키는 역할을 합니다.
|
||||
|
||||
이 유형의 호스트는 주로 웹사이트의 도메인이 변경되었지만,
|
||||
검색 엔진이나 다른 사이트에 이전 도메인 링크가 남아 있을 때 사용하면 가장 효과적입니다.
|
||||
|
||||
7
frontend/src/locale/src/HelpDoc/ko/Streams.md
Normal file
7
frontend/src/locale/src/HelpDoc/ko/Streams.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## 호스트 스트림이란?
|
||||
|
||||
호스트 스트림은 비교적 최근에 Nginx에 추가된 기능으로,
|
||||
TCP/UDP 트래픽을 네트워크 내의 다른 컴퓨터로 직접 전달하는 데 사용됩니다.
|
||||
|
||||
게임 서버나 FTP, SSH 서버 등을 운영할 때 유용하게 사용할 수 있습니다.
|
||||
|
||||
7
frontend/src/locale/src/HelpDoc/ko/index.ts
Normal file
7
frontend/src/locale/src/HelpDoc/ko/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
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";
|
||||
|
||||
695
frontend/src/locale/src/bg.json
Normal file
695
frontend/src/locale/src/bg.json
Normal file
@@ -0,0 +1,695 @@
|
||||
{
|
||||
"access-list": {
|
||||
"defaultMessage": "Списък за достъп"
|
||||
},
|
||||
"access-list.access-count": {
|
||||
"defaultMessage": "{count} {count, plural, one {правило} other {правила}}"
|
||||
},
|
||||
"access-list.auth-count": {
|
||||
"defaultMessage": "{count} {count, plural, one {потребител} other {потребители}}"
|
||||
},
|
||||
"access-list.help-rules-last": {
|
||||
"defaultMessage": "Когато съществува поне 1 правило, това правило за отказ се добавя последно"
|
||||
},
|
||||
"access-list.help.rules-order": {
|
||||
"defaultMessage": "Обърнете внимание, че правилата Позволяване и Отказване се прилагат в реда, в който са зададени."
|
||||
},
|
||||
"access-list.pass-auth": {
|
||||
"defaultMessage": "Предаване на автентикация към Upstream"
|
||||
},
|
||||
"access-list.public": {
|
||||
"defaultMessage": "Публичен достъп"
|
||||
},
|
||||
"access-list.public.subtitle": {
|
||||
"defaultMessage": "Без базова автентикация"
|
||||
},
|
||||
"access-list.rule-source.placeholder": {
|
||||
"defaultMessage": "192.168.1.100 или 192.168.1.0/24 или 2001:0db8::/32"
|
||||
},
|
||||
"access-list.satisfy-any": {
|
||||
"defaultMessage": "Удовлетворяване на което и да е"
|
||||
},
|
||||
"access-list.subtitle": {
|
||||
"defaultMessage": "{users} {users, plural, one {потребител} other {потребители}}, {rules} {rules, plural, one {правило} other {правила}} - Създадено: {date}"
|
||||
},
|
||||
"access-lists": {
|
||||
"defaultMessage": "Списъци за достъп"
|
||||
},
|
||||
"action.add": {
|
||||
"defaultMessage": "Добавяне"
|
||||
},
|
||||
"action.add-location": {
|
||||
"defaultMessage": "Добавяне на маршрут"
|
||||
},
|
||||
"action.allow": {
|
||||
"defaultMessage": "Разрешаване"
|
||||
},
|
||||
"action.close": {
|
||||
"defaultMessage": "Затваряне"
|
||||
},
|
||||
"action.delete": {
|
||||
"defaultMessage": "Изтриване"
|
||||
},
|
||||
"action.deny": {
|
||||
"defaultMessage": "Отказване"
|
||||
},
|
||||
"action.disable": {
|
||||
"defaultMessage": "Деактивиране"
|
||||
},
|
||||
"action.download": {
|
||||
"defaultMessage": "Изтегляне"
|
||||
},
|
||||
"action.edit": {
|
||||
"defaultMessage": "Редактиране"
|
||||
},
|
||||
"action.enable": {
|
||||
"defaultMessage": "Активиране"
|
||||
},
|
||||
"action.permissions": {
|
||||
"defaultMessage": "Права"
|
||||
},
|
||||
"action.renew": {
|
||||
"defaultMessage": "Подновяване"
|
||||
},
|
||||
"action.view-details": {
|
||||
"defaultMessage": "Преглед на детайли"
|
||||
},
|
||||
"auditlogs": {
|
||||
"defaultMessage": "Журнали за одит"
|
||||
},
|
||||
"auto": {
|
||||
"defaultMessage": "Автоматично"
|
||||
},
|
||||
"cancel": {
|
||||
"defaultMessage": "Отказ"
|
||||
},
|
||||
"certificate": {
|
||||
"defaultMessage": "Сертификат"
|
||||
},
|
||||
"certificate.custom-certificate": {
|
||||
"defaultMessage": "Сертификат"
|
||||
},
|
||||
"certificate.custom-certificate-key": {
|
||||
"defaultMessage": "Ключ на сертификата"
|
||||
},
|
||||
"certificate.custom-intermediate": {
|
||||
"defaultMessage": "Междинен сертификат"
|
||||
},
|
||||
"certificate.in-use": {
|
||||
"defaultMessage": "Използва се"
|
||||
},
|
||||
"certificate.none.subtitle": {
|
||||
"defaultMessage": "Не е назначен сертификат"
|
||||
},
|
||||
"certificate.none.subtitle.for-http": {
|
||||
"defaultMessage": "Този хост няма да използва HTTPS"
|
||||
},
|
||||
"certificate.none.title": {
|
||||
"defaultMessage": "Без сертификат"
|
||||
},
|
||||
"certificate.not-in-use": {
|
||||
"defaultMessage": "Не се използва"
|
||||
},
|
||||
"certificate.renew": {
|
||||
"defaultMessage": "Подновяване на сертификат"
|
||||
},
|
||||
"certificates": {
|
||||
"defaultMessage": "Сертификати"
|
||||
},
|
||||
"certificates.custom": {
|
||||
"defaultMessage": "Потребителски сертификат"
|
||||
},
|
||||
"certificates.custom.warning": {
|
||||
"defaultMessage": "Ключове, защитени с парола, не се поддържат."
|
||||
},
|
||||
"certificates.dns.credentials": {
|
||||
"defaultMessage": "Съдържание на файл с удостоверения"
|
||||
},
|
||||
"certificates.dns.credentials-note": {
|
||||
"defaultMessage": "Този плъгин изисква конфигурационен файл с API токен или други идентификационни данни."
|
||||
},
|
||||
"certificates.dns.credentials-warning": {
|
||||
"defaultMessage": "Тези данни ще бъдат съхранени като обикновен текст в базата и във файл!"
|
||||
},
|
||||
"certificates.dns.propagation-seconds": {
|
||||
"defaultMessage": "Секунди за разпространение"
|
||||
},
|
||||
"certificates.dns.propagation-seconds-note": {
|
||||
"defaultMessage": "Оставете празно, за да се използва стойността по подразбиране. Брой секунди за изчакване на DNS разпространение."
|
||||
},
|
||||
"certificates.dns.provider": {
|
||||
"defaultMessage": "DNS доставчик"
|
||||
},
|
||||
"certificates.dns.provider.placeholder": {
|
||||
"defaultMessage": "Изберете доставчик..."
|
||||
},
|
||||
"certificates.dns.warning": {
|
||||
"defaultMessage": "Този раздел изисква познания за Certbot и неговите DNS плъгини. Моля, консултирайте се с документацията."
|
||||
},
|
||||
"certificates.http.reachability-404": {
|
||||
"defaultMessage": "Сървър е намерен на този домейн, но не изглежда да е Nginx Proxy Manager. Уверете се, че домейнът сочи към IP адреса, където работи NPM."
|
||||
},
|
||||
"certificates.http.reachability-failed-to-check": {
|
||||
"defaultMessage": "Неуспешна проверка поради грешка в комуникацията със site24x7.com."
|
||||
},
|
||||
"certificates.http.reachability-not-resolved": {
|
||||
"defaultMessage": "Няма достъпен сървър на този домейн. Проверете, че домейнът съществува и сочи към IP-та, където се изпълнява NPM, и ако е необходимо, че порт 80 е пренасочен."
|
||||
},
|
||||
"certificates.http.reachability-ok": {
|
||||
"defaultMessage": "Вашият сървър е достъпен и създаването на сертификати е възможно."
|
||||
},
|
||||
"certificates.http.reachability-other": {
|
||||
"defaultMessage": "Намерен е сървър, но върна неочакван код {code}. Това NPM ли е? Уверете се, че домейнът сочи към вашия NPM сървър."
|
||||
},
|
||||
"certificates.http.reachability-wrong-data": {
|
||||
"defaultMessage": "Намерен е сървър, но върна неочаквани данни. Това NPM ли е? Уверете се, че домейнът сочи към вашия NPM сървър."
|
||||
},
|
||||
"certificates.http.test-results": {
|
||||
"defaultMessage": "Резултати от теста"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"certificates.request.title": {
|
||||
"defaultMessage": "Заявка за нов сертификат"
|
||||
},
|
||||
"column.access": {
|
||||
"defaultMessage": "Достъп"
|
||||
},
|
||||
"column.authorization": {
|
||||
"defaultMessage": "Автентикация"
|
||||
},
|
||||
"column.authorizations": {
|
||||
"defaultMessage": "Автентикации"
|
||||
},
|
||||
"column.custom-locations": {
|
||||
"defaultMessage": "Персонализирани маршрути"
|
||||
},
|
||||
"column.destination": {
|
||||
"defaultMessage": "Дестинация"
|
||||
},
|
||||
"column.details": {
|
||||
"defaultMessage": "Детайли"
|
||||
},
|
||||
"column.email": {
|
||||
"defaultMessage": "Имейл"
|
||||
},
|
||||
"column.event": {
|
||||
"defaultMessage": "Събитие"
|
||||
},
|
||||
"column.expires": {
|
||||
"defaultMessage": "Изтича"
|
||||
},
|
||||
"column.http-code": {
|
||||
"defaultMessage": "HTTP код"
|
||||
},
|
||||
"column.incoming-port": {
|
||||
"defaultMessage": "Входящ порт"
|
||||
},
|
||||
"column.name": {
|
||||
"defaultMessage": "Име"
|
||||
},
|
||||
"column.protocol": {
|
||||
"defaultMessage": "Протокол"
|
||||
},
|
||||
"column.provider": {
|
||||
"defaultMessage": "Доставчик"
|
||||
},
|
||||
"column.roles": {
|
||||
"defaultMessage": "Роли"
|
||||
},
|
||||
"column.rules": {
|
||||
"defaultMessage": "Правила"
|
||||
},
|
||||
"column.satisfy": {
|
||||
"defaultMessage": "Удовлетворяване"
|
||||
},
|
||||
"column.satisfy-all": {
|
||||
"defaultMessage": "Всички"
|
||||
},
|
||||
"column.satisfy-any": {
|
||||
"defaultMessage": "Кое и да е"
|
||||
},
|
||||
"column.scheme": {
|
||||
"defaultMessage": "Схема"
|
||||
},
|
||||
"column.source": {
|
||||
"defaultMessage": "Източник"
|
||||
},
|
||||
"column.ssl": {
|
||||
"defaultMessage": "SSL"
|
||||
},
|
||||
"column.status": {
|
||||
"defaultMessage": "Статус"
|
||||
},
|
||||
"created-on": {
|
||||
"defaultMessage": "Създадено: {date}"
|
||||
},
|
||||
"dashboard": {
|
||||
"defaultMessage": "Табло"
|
||||
},
|
||||
"dead-host": {
|
||||
"defaultMessage": "404 хост"
|
||||
},
|
||||
"dead-hosts": {
|
||||
"defaultMessage": "404 хостове"
|
||||
},
|
||||
"dead-hosts.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {404 хост} other {404 хостове}}"
|
||||
},
|
||||
"disabled": {
|
||||
"defaultMessage": "Деактивиран"
|
||||
},
|
||||
"domain-names": {
|
||||
"defaultMessage": "Домейн имена"
|
||||
},
|
||||
"domain-names.max": {
|
||||
"defaultMessage": "Максимум {count} домейна"
|
||||
},
|
||||
"domain-names.placeholder": {
|
||||
"defaultMessage": "Започнете да въвеждате, за да добавите домейн..."
|
||||
},
|
||||
"domain-names.wildcards-not-permitted": {
|
||||
"defaultMessage": "Wildcard не е разрешен за този тип"
|
||||
},
|
||||
"domain-names.wildcards-not-supported": {
|
||||
"defaultMessage": "Wildcard не се поддържа от това CA"
|
||||
},
|
||||
"domains.force-ssl": {
|
||||
"defaultMessage": "Принудително SSL"
|
||||
},
|
||||
"domains.hsts-enabled": {
|
||||
"defaultMessage": "HSTS активирано"
|
||||
},
|
||||
"domains.hsts-subdomains": {
|
||||
"defaultMessage": "HSTS за поддомейни"
|
||||
},
|
||||
"domains.http2-support": {
|
||||
"defaultMessage": "Поддръжка на HTTP/2"
|
||||
},
|
||||
"domains.use-dns": {
|
||||
"defaultMessage": "Използване на DNS Challenge"
|
||||
},
|
||||
"email-address": {
|
||||
"defaultMessage": "Имейл адрес"
|
||||
},
|
||||
"empty-search": {
|
||||
"defaultMessage": "Няма резултати"
|
||||
},
|
||||
"empty-subtitle": {
|
||||
"defaultMessage": "Защо не създадете един?"
|
||||
},
|
||||
"enabled": {
|
||||
"defaultMessage": "Активиран"
|
||||
},
|
||||
"error.access.at-least-one": {
|
||||
"defaultMessage": "Необходимо е поне една Автентикация или едно Правило за достъп"
|
||||
},
|
||||
"error.access.duplicate-usernames": {
|
||||
"defaultMessage": "Потребителските имена за достъп трябва да са уникални"
|
||||
},
|
||||
"error.invalid-auth": {
|
||||
"defaultMessage": "Невалиден имейл или парола"
|
||||
},
|
||||
"error.invalid-domain": {
|
||||
"defaultMessage": "Невалиден домейн: {domain}"
|
||||
},
|
||||
"error.invalid-email": {
|
||||
"defaultMessage": "Невалиден имейл адрес"
|
||||
},
|
||||
"error.max-character-length": {
|
||||
"defaultMessage": "Максималната дължина е {max} знак{max, plural, one {} other {а}}"
|
||||
},
|
||||
"error.max-domains": {
|
||||
"defaultMessage": "Твърде много домейни, максимум {max}"
|
||||
},
|
||||
"error.maximum": {
|
||||
"defaultMessage": "Максимум {max}"
|
||||
},
|
||||
"error.min-character-length": {
|
||||
"defaultMessage": "Минималната дължина е {min} знак{min, plural, one {} other {а}}"
|
||||
},
|
||||
"error.minimum": {
|
||||
"defaultMessage": "Минимум e {min}"
|
||||
},
|
||||
"error.passwords-must-match": {
|
||||
"defaultMessage": "Паролите трябва да съвпадат"
|
||||
},
|
||||
"error.required": {
|
||||
"defaultMessage": "Това поле е задължително"
|
||||
},
|
||||
"expires.on": {
|
||||
"defaultMessage": "Изтича: {date}"
|
||||
},
|
||||
"footer.github-fork": {
|
||||
"defaultMessage": "Fork в GitHub"
|
||||
},
|
||||
"host.flags.block-exploits": {
|
||||
"defaultMessage": "Блокиране на често срещани експлойти"
|
||||
},
|
||||
"host.flags.cache-assets": {
|
||||
"defaultMessage": "Кеширане на ресурси"
|
||||
},
|
||||
"host.flags.preserve-path": {
|
||||
"defaultMessage": "Запазване на пътя"
|
||||
},
|
||||
"host.flags.protocols": {
|
||||
"defaultMessage": "Протоколи"
|
||||
},
|
||||
"host.flags.websockets-upgrade": {
|
||||
"defaultMessage": "Поддръжка на WebSockets"
|
||||
},
|
||||
"host.forward-port": {
|
||||
"defaultMessage": "Порт"
|
||||
},
|
||||
"host.forward-scheme": {
|
||||
"defaultMessage": "Схема"
|
||||
},
|
||||
"hosts": {
|
||||
"defaultMessage": "Хостове"
|
||||
},
|
||||
"http-only": {
|
||||
"defaultMessage": "Само HTTP"
|
||||
},
|
||||
"lets-encrypt": {
|
||||
"defaultMessage": "Let's Encrypt"
|
||||
},
|
||||
"lets-encrypt-via-dns": {
|
||||
"defaultMessage": "Let's Encrypt чрез DNS"
|
||||
},
|
||||
"lets-encrypt-via-http": {
|
||||
"defaultMessage": "Let's Encrypt чрез HTTP"
|
||||
},
|
||||
"loading": {
|
||||
"defaultMessage": "Зареждане…"
|
||||
},
|
||||
"login.title": {
|
||||
"defaultMessage": "Вход в акаунта"
|
||||
},
|
||||
"nginx-config.label": {
|
||||
"defaultMessage": "Персонализирана Nginx конфигурация"
|
||||
},
|
||||
"nginx-config.placeholder": {
|
||||
"defaultMessage": "# Въведете вашата персонализирана Nginx конфигурация на собствен риск!"
|
||||
},
|
||||
"no-permission-error": {
|
||||
"defaultMessage": "Нямате достъп до тази страница."
|
||||
},
|
||||
"notfound.action": {
|
||||
"defaultMessage": "Към началната страница"
|
||||
},
|
||||
"notfound.content": {
|
||||
"defaultMessage": "Страницата, която търсите, не беше намерена"
|
||||
},
|
||||
"notfound.title": {
|
||||
"defaultMessage": "Упс… Намерихте грешка"
|
||||
},
|
||||
"notification.error": {
|
||||
"defaultMessage": "Грешка"
|
||||
},
|
||||
"notification.object-deleted": {
|
||||
"defaultMessage": "{object} беше изтрит"
|
||||
},
|
||||
"notification.object-disabled": {
|
||||
"defaultMessage": "{object} беше деактивиран"
|
||||
},
|
||||
"notification.object-enabled": {
|
||||
"defaultMessage": "{object} беше активиран"
|
||||
},
|
||||
"notification.object-renewed": {
|
||||
"defaultMessage": "{object} беше подновен"
|
||||
},
|
||||
"notification.object-saved": {
|
||||
"defaultMessage": "{object} беше запазен"
|
||||
},
|
||||
"notification.success": {
|
||||
"defaultMessage": "Успех"
|
||||
},
|
||||
"object.actions-title": {
|
||||
"defaultMessage": "{object} №{id}"
|
||||
},
|
||||
"object.add": {
|
||||
"defaultMessage": "Добавяне: {object}"
|
||||
},
|
||||
"object.delete": {
|
||||
"defaultMessage": "Изтриване: {object}"
|
||||
},
|
||||
"object.delete.content": {
|
||||
"defaultMessage": "Сигурни ли сте, че искате да изтриете {object}?"
|
||||
},
|
||||
"object.edit": {
|
||||
"defaultMessage": "Редактиране: {object}"
|
||||
},
|
||||
"object.empty": {
|
||||
"defaultMessage": "Няма налични {objects}"
|
||||
},
|
||||
"object.event.created": {
|
||||
"defaultMessage": "Създаден {object}"
|
||||
},
|
||||
"object.event.deleted": {
|
||||
"defaultMessage": "Изтрит {object}"
|
||||
},
|
||||
"object.event.disabled": {
|
||||
"defaultMessage": "Деактивиран {object}"
|
||||
},
|
||||
"object.event.enabled": {
|
||||
"defaultMessage": "Активиран {object}"
|
||||
},
|
||||
"object.event.renewed": {
|
||||
"defaultMessage": "Подновен {object}"
|
||||
},
|
||||
"object.event.updated": {
|
||||
"defaultMessage": "Актуализиран {object}"
|
||||
},
|
||||
"offline": {
|
||||
"defaultMessage": "Офлайн"
|
||||
},
|
||||
"online": {
|
||||
"defaultMessage": "Онлайн"
|
||||
},
|
||||
"options": {
|
||||
"defaultMessage": "Опции"
|
||||
},
|
||||
"password": {
|
||||
"defaultMessage": "Парола"
|
||||
},
|
||||
"password.generate": {
|
||||
"defaultMessage": "Генериране на случайна парола"
|
||||
},
|
||||
"password.hide": {
|
||||
"defaultMessage": "Скриване на паролата"
|
||||
},
|
||||
"password.show": {
|
||||
"defaultMessage": "Показване на паролата"
|
||||
},
|
||||
"permissions.hidden": {
|
||||
"defaultMessage": "Скрито"
|
||||
},
|
||||
"permissions.manage": {
|
||||
"defaultMessage": "Управление"
|
||||
},
|
||||
"permissions.view": {
|
||||
"defaultMessage": "Само преглед"
|
||||
},
|
||||
"permissions.visibility.all": {
|
||||
"defaultMessage": "Всички елементи"
|
||||
},
|
||||
"permissions.visibility.title": {
|
||||
"defaultMessage": "Видимост на елементите"
|
||||
},
|
||||
"permissions.visibility.user": {
|
||||
"defaultMessage": "Само създадените от потребителя"
|
||||
},
|
||||
"proxy-host": {
|
||||
"defaultMessage": "Прокси хост"
|
||||
},
|
||||
"proxy-host.forward-host": {
|
||||
"defaultMessage": "Хост/IP за препращане"
|
||||
},
|
||||
"proxy-hosts": {
|
||||
"defaultMessage": "Прокси хостове"
|
||||
},
|
||||
"proxy-hosts.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {прокси хост} other {прокси хостове}}"
|
||||
},
|
||||
"public": {
|
||||
"defaultMessage": "Публичен"
|
||||
},
|
||||
"redirection-host": {
|
||||
"defaultMessage": "Хост за пренасочване"
|
||||
},
|
||||
"redirection-host.forward-domain": {
|
||||
"defaultMessage": "Домейн за пренасочване"
|
||||
},
|
||||
"redirection-host.forward-http-code": {
|
||||
"defaultMessage": "HTTP код"
|
||||
},
|
||||
"redirection-hosts": {
|
||||
"defaultMessage": "Хостове за пренасочване"
|
||||
},
|
||||
"redirection-hosts.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {хост за пренасочване} other {хостове за пренасочване}}"
|
||||
},
|
||||
"redirection-hosts.http-code.300": {
|
||||
"defaultMessage": "300 Multiple Choices"
|
||||
},
|
||||
"redirection-hosts.http-code.301": {
|
||||
"defaultMessage": "301 Преместено постоянно"
|
||||
},
|
||||
"redirection-hosts.http-code.302": {
|
||||
"defaultMessage": "302 Преместено временно"
|
||||
},
|
||||
"redirection-hosts.http-code.303": {
|
||||
"defaultMessage": "303 See other"
|
||||
},
|
||||
"redirection-hosts.http-code.307": {
|
||||
"defaultMessage": "307 Временно пренасочване"
|
||||
},
|
||||
"redirection-hosts.http-code.308": {
|
||||
"defaultMessage": "308 Постоянно пренасочване"
|
||||
},
|
||||
"role.admin": {
|
||||
"defaultMessage": "Администратор"
|
||||
},
|
||||
"role.standard-user": {
|
||||
"defaultMessage": "Обикновен потребител"
|
||||
},
|
||||
"save": {
|
||||
"defaultMessage": "Запазване"
|
||||
},
|
||||
"setting": {
|
||||
"defaultMessage": "Настройка"
|
||||
},
|
||||
"settings": {
|
||||
"defaultMessage": "Настройки"
|
||||
},
|
||||
"settings.default-site": {
|
||||
"defaultMessage": "Сайт по подразбиране"
|
||||
},
|
||||
"settings.default-site.404": {
|
||||
"defaultMessage": "404 страница"
|
||||
},
|
||||
"settings.default-site.444": {
|
||||
"defaultMessage": "Без отговор (444)"
|
||||
},
|
||||
"settings.default-site.congratulations": {
|
||||
"defaultMessage": "Страница поздравление"
|
||||
},
|
||||
"settings.default-site.description": {
|
||||
"defaultMessage": "Какво да се показва при заявка към неизвестен хост"
|
||||
},
|
||||
"settings.default-site.html": {
|
||||
"defaultMessage": "Персонализиран HTML"
|
||||
},
|
||||
"settings.default-site.html.placeholder": {
|
||||
"defaultMessage": "<!-- Въведете вашето персонализирано HTML съдържание тук -->"
|
||||
},
|
||||
"settings.default-site.redirect": {
|
||||
"defaultMessage": "Пренасочване"
|
||||
},
|
||||
"setup.preamble": {
|
||||
"defaultMessage": "Започнете, като създадете администраторски акаунт."
|
||||
},
|
||||
"setup.title": {
|
||||
"defaultMessage": "Добре дошли!"
|
||||
},
|
||||
"sign-in": {
|
||||
"defaultMessage": "Вход"
|
||||
},
|
||||
"ssl-certificate": {
|
||||
"defaultMessage": "SSL сертификат"
|
||||
},
|
||||
"stream": {
|
||||
"defaultMessage": "Поток"
|
||||
},
|
||||
"stream.forward-host": {
|
||||
"defaultMessage": "Хост за препращане"
|
||||
},
|
||||
"stream.forward-host.placeholder": {
|
||||
"defaultMessage": "example.com или 10.0.0.1 или 2001:db8:3333:4444:5555:6666:7777:8888"
|
||||
},
|
||||
"stream.incoming-port": {
|
||||
"defaultMessage": "Входящ порт"
|
||||
},
|
||||
"streams": {
|
||||
"defaultMessage": "Потоци"
|
||||
},
|
||||
"streams.count": {
|
||||
"defaultMessage": "{count} {count, plural, one {поток} other {потоци}}"
|
||||
},
|
||||
"streams.tcp": {
|
||||
"defaultMessage": "TCP"
|
||||
},
|
||||
"streams.udp": {
|
||||
"defaultMessage": "UDP"
|
||||
},
|
||||
"test": {
|
||||
"defaultMessage": "Тест"
|
||||
},
|
||||
"update-available": {
|
||||
"defaultMessage": "Налична актуализация: {latestVersion}"
|
||||
},
|
||||
"user": {
|
||||
"defaultMessage": "Потребител"
|
||||
},
|
||||
"user.change-password": {
|
||||
"defaultMessage": "Смяна на парола"
|
||||
},
|
||||
"user.confirm-password": {
|
||||
"defaultMessage": "Потвърждение на парола"
|
||||
},
|
||||
"user.current-password": {
|
||||
"defaultMessage": "Текуща парола"
|
||||
},
|
||||
"user.edit-profile": {
|
||||
"defaultMessage": "Редактиране на профил"
|
||||
},
|
||||
"user.full-name": {
|
||||
"defaultMessage": "Пълно име"
|
||||
},
|
||||
"user.login-as": {
|
||||
"defaultMessage": "Вход като {name}"
|
||||
},
|
||||
"user.logout": {
|
||||
"defaultMessage": "Изход"
|
||||
},
|
||||
"user.new-password": {
|
||||
"defaultMessage": "Нова парола"
|
||||
},
|
||||
"user.nickname": {
|
||||
"defaultMessage": "Псевдоним"
|
||||
},
|
||||
"user.set-password": {
|
||||
"defaultMessage": "Задаване на парола"
|
||||
},
|
||||
"user.set-permissions": {
|
||||
"defaultMessage": "Настройка на права за {name}"
|
||||
},
|
||||
"user.switch-dark": {
|
||||
"defaultMessage": "Тъмна тема"
|
||||
},
|
||||
"user.switch-light": {
|
||||
"defaultMessage": "Светла тема"
|
||||
},
|
||||
"username": {
|
||||
"defaultMessage": "Потребителско име"
|
||||
},
|
||||
"users": {
|
||||
"defaultMessage": "Потребители"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
683
frontend/src/locale/src/ga.json
Normal file
683
frontend/src/locale/src/ga.json
Normal 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 d’ainmneacha ú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í"
|
||||
}
|
||||
}
|
||||
683
frontend/src/locale/src/id.json
Normal file
683
frontend/src/locale/src/id.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "受信ポート"
|
||||
|
||||
695
frontend/src/locale/src/ko.json
Normal file
695
frontend/src/locale/src/ko.json
Normal file
@@ -0,0 +1,695 @@
|
||||
{
|
||||
"access-list": {
|
||||
"defaultMessage": "접근 정책"
|
||||
},
|
||||
"access-list.access-count": {
|
||||
"defaultMessage": "{count}개의 정책"
|
||||
},
|
||||
"access-list.auth-count": {
|
||||
"defaultMessage": "{count}명의 사용자"
|
||||
},
|
||||
"access-list.help-rules-last": {
|
||||
"defaultMessage": "규칙이 하나라도 있으면 아래 ‘전체 거부’ 규칙이 마지막에 추가됩니다."
|
||||
},
|
||||
"access-list.help.rules-order": {
|
||||
"defaultMessage": "허용/거부 규칙은 정의된 순서대로 적용됩니다."
|
||||
},
|
||||
"access-list.pass-auth": {
|
||||
"defaultMessage": "인증 정보를 원본 서버로 전달"
|
||||
},
|
||||
"access-list.public": {
|
||||
"defaultMessage": "누구나 접근 가능"
|
||||
},
|
||||
"access-list.public.subtitle": {
|
||||
"defaultMessage": "기본 인증 필요 없음"
|
||||
},
|
||||
"access-list.rule-source.placeholder": {
|
||||
"defaultMessage": "192.168.1.100 / 192.168.1.0/24 / IPv6"
|
||||
},
|
||||
"access-list.satisfy-any": {
|
||||
"defaultMessage": "조건 중 하나라도 충족"
|
||||
},
|
||||
"access-list.subtitle": {
|
||||
"defaultMessage": "{users}명 {users, plural, one {사용자} other {사용자}}, {rules}개 {rules, plural, one {규칙} other {규칙}} - 생성일: {date}"
|
||||
},
|
||||
"access-lists": {
|
||||
"defaultMessage": "접근 정책"
|
||||
},
|
||||
"action.add": {
|
||||
"defaultMessage": "추가"
|
||||
},
|
||||
"action.add-location": {
|
||||
"defaultMessage": "경로 추가"
|
||||
},
|
||||
"action.allow": {
|
||||
"defaultMessage": "허용"
|
||||
},
|
||||
"action.close": {
|
||||
"defaultMessage": "닫기"
|
||||
},
|
||||
"action.delete": {
|
||||
"defaultMessage": "삭제"
|
||||
},
|
||||
"action.deny": {
|
||||
"defaultMessage": "거부"
|
||||
},
|
||||
"action.disable": {
|
||||
"defaultMessage": "비활성화"
|
||||
},
|
||||
"action.download": {
|
||||
"defaultMessage": "다운로드"
|
||||
},
|
||||
"action.edit": {
|
||||
"defaultMessage": "편집"
|
||||
},
|
||||
"action.enable": {
|
||||
"defaultMessage": "활성화"
|
||||
},
|
||||
"action.permissions": {
|
||||
"defaultMessage": "권한"
|
||||
},
|
||||
"action.renew": {
|
||||
"defaultMessage": "갱신"
|
||||
},
|
||||
"action.view-details": {
|
||||
"defaultMessage": "자세히 보기"
|
||||
},
|
||||
"auditlogs": {
|
||||
"defaultMessage": "감사 로그"
|
||||
},
|
||||
"auto": {
|
||||
"defaultMessage": "자동"
|
||||
},
|
||||
"cancel": {
|
||||
"defaultMessage": "취소"
|
||||
},
|
||||
"certificate": {
|
||||
"defaultMessage": "인증서"
|
||||
},
|
||||
"certificate.custom-certificate": {
|
||||
"defaultMessage": "인증서"
|
||||
},
|
||||
"certificate.custom-certificate-key": {
|
||||
"defaultMessage": "인증서 키"
|
||||
},
|
||||
"certificate.custom-intermediate": {
|
||||
"defaultMessage": "중간 인증서"
|
||||
},
|
||||
"certificate.in-use": {
|
||||
"defaultMessage": "사용 중"
|
||||
},
|
||||
"certificate.none.subtitle": {
|
||||
"defaultMessage": "지정된 인증서 없음"
|
||||
},
|
||||
"certificate.none.subtitle.for-http": {
|
||||
"defaultMessage": "이 호스트는 HTTPS를 사용하지 않습니다."
|
||||
},
|
||||
"certificate.none.title": {
|
||||
"defaultMessage": "없음"
|
||||
},
|
||||
"certificate.not-in-use": {
|
||||
"defaultMessage": "사용 안 함"
|
||||
},
|
||||
"certificate.renew": {
|
||||
"defaultMessage": "인증서 갱신"
|
||||
},
|
||||
"certificates": {
|
||||
"defaultMessage": "인증서"
|
||||
},
|
||||
"certificates.custom": {
|
||||
"defaultMessage": "사용자 지정 인증서"
|
||||
},
|
||||
"certificates.custom.warning": {
|
||||
"defaultMessage": "비밀번호로 보호된 키 파일은 지원되지 않습니다."
|
||||
},
|
||||
"certificates.dns.credentials": {
|
||||
"defaultMessage": "DNS 자격 증명 입력"
|
||||
},
|
||||
"certificates.dns.credentials-note": {
|
||||
"defaultMessage": "이 플러그인은 API 토큰 등이 포함된 설정 파일이 필요합니다."
|
||||
},
|
||||
"certificates.dns.credentials-warning": {
|
||||
"defaultMessage": "입력한 정보는 데이터베이스와 파일에 평문으로 저장됩니다."
|
||||
},
|
||||
"certificates.dns.propagation-seconds": {
|
||||
"defaultMessage": "DNS 전파 시간"
|
||||
},
|
||||
"certificates.dns.propagation-seconds-note": {
|
||||
"defaultMessage": "비워두면 기본값을 사용합니다. DNS 전파를 기다리는 시간(초)입니다."
|
||||
},
|
||||
"certificates.dns.provider": {
|
||||
"defaultMessage": "DNS 공급자"
|
||||
},
|
||||
"certificates.dns.provider.placeholder": {
|
||||
"defaultMessage": "공급자를 선택하세요..."
|
||||
},
|
||||
"certificates.dns.warning": {
|
||||
"defaultMessage": "이 기능을 사용하려면 Certbot과 DNS 플러그인에 대한 기본적인 이해가 필요합니다. 자세한 내용은 관련 문서를 참고해 주세요."
|
||||
},
|
||||
"certificates.http.reachability-404": {
|
||||
"defaultMessage": "해당 도메인에서 서버가 탐지되었지만 Nginx Proxy Manager가 아닌 것으로 보입니다. 도메인이 NPM이 실행 중인 IP를 가리키는지 확인하세요."
|
||||
},
|
||||
"certificates.http.reachability-failed-to-check": {
|
||||
"defaultMessage": "site24x7.com과의 통신 오류로 인해 도달 가능 여부를 확인할 수 없습니다."
|
||||
},
|
||||
"certificates.http.reachability-not-resolved": {
|
||||
"defaultMessage": "해당 도메인에 접근 가능한 서버가 없습니다. 도메인이 존재하며 NPM이 실행되는 IP를 가리키고, 필요하면 라우터에서 80포트가 포워딩되어 있는지 확인하세요."
|
||||
},
|
||||
"certificates.http.reachability-ok": {
|
||||
"defaultMessage": "서버에 정상적으로 접근할 수 있으며 인증서 발급이 가능합니다."
|
||||
},
|
||||
"certificates.http.reachability-other": {
|
||||
"defaultMessage": "해당 도메인에서 서버가 발견되었지만 예상치 못한 상태 코드 {code}를 반환했습니다. NPM 서버가 맞는지 확인하세요."
|
||||
},
|
||||
"certificates.http.reachability-wrong-data": {
|
||||
"defaultMessage": "서버가 응답했지만 예상치 못한 데이터를 반환했습니다. NPM 서버가 맞는지 확인하세요."
|
||||
},
|
||||
"certificates.http.test-results": {
|
||||
"defaultMessage": "테스트 결과"
|
||||
},
|
||||
"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 사용"
|
||||
},
|
||||
"certificates.request.title": {
|
||||
"defaultMessage": "새 인증서 요청"
|
||||
},
|
||||
"column.access": {
|
||||
"defaultMessage": "접근 정책"
|
||||
},
|
||||
"column.authorization": {
|
||||
"defaultMessage": "인증 사용자"
|
||||
},
|
||||
"column.authorizations": {
|
||||
"defaultMessage": "인증 사용자"
|
||||
},
|
||||
"column.custom-locations": {
|
||||
"defaultMessage": "사용자 지정 경로"
|
||||
},
|
||||
"column.destination": {
|
||||
"defaultMessage": "전달 대상"
|
||||
},
|
||||
"column.details": {
|
||||
"defaultMessage": "기본 설정"
|
||||
},
|
||||
"column.email": {
|
||||
"defaultMessage": "이메일"
|
||||
},
|
||||
"column.event": {
|
||||
"defaultMessage": "이벤트"
|
||||
},
|
||||
"column.expires": {
|
||||
"defaultMessage": "만료일"
|
||||
},
|
||||
"column.http-code": {
|
||||
"defaultMessage": "HTTP 코드"
|
||||
},
|
||||
"column.incoming-port": {
|
||||
"defaultMessage": "수신 포트"
|
||||
},
|
||||
"column.name": {
|
||||
"defaultMessage": "이름"
|
||||
},
|
||||
"column.protocol": {
|
||||
"defaultMessage": "프로토콜"
|
||||
},
|
||||
"column.provider": {
|
||||
"defaultMessage": "공급자"
|
||||
},
|
||||
"column.roles": {
|
||||
"defaultMessage": "권한"
|
||||
},
|
||||
"column.rules": {
|
||||
"defaultMessage": "IP 정책"
|
||||
},
|
||||
"column.satisfy": {
|
||||
"defaultMessage": "조건 방식"
|
||||
},
|
||||
"column.satisfy-all": {
|
||||
"defaultMessage": "모두 충족"
|
||||
},
|
||||
"column.satisfy-any": {
|
||||
"defaultMessage": "하나라도 충족"
|
||||
},
|
||||
"column.scheme": {
|
||||
"defaultMessage": "프로토콜"
|
||||
},
|
||||
"column.source": {
|
||||
"defaultMessage": "도메인"
|
||||
},
|
||||
"column.ssl": {
|
||||
"defaultMessage": "SSL"
|
||||
},
|
||||
"column.status": {
|
||||
"defaultMessage": "상태"
|
||||
},
|
||||
"created-on": {
|
||||
"defaultMessage": "생성일: {date}"
|
||||
},
|
||||
"dashboard": {
|
||||
"defaultMessage": "대시보드"
|
||||
},
|
||||
"dead-host": {
|
||||
"defaultMessage": "404 호스트"
|
||||
},
|
||||
"dead-hosts": {
|
||||
"defaultMessage": "404 호스트"
|
||||
},
|
||||
"dead-hosts.count": {
|
||||
"defaultMessage": "{count}개의 404 호스트"
|
||||
},
|
||||
"disabled": {
|
||||
"defaultMessage": "비활성화"
|
||||
},
|
||||
"domain-names": {
|
||||
"defaultMessage": "도메인 이름"
|
||||
},
|
||||
"domain-names.max": {
|
||||
"defaultMessage": "최대 {count}개의 도메인 이름"
|
||||
},
|
||||
"domain-names.placeholder": {
|
||||
"defaultMessage": "도메인을 입력해주세요."
|
||||
},
|
||||
"domain-names.wildcards-not-permitted": {
|
||||
"defaultMessage": "HTTP 방식으로는 와일드카드 인증서를 발급할 수 없습니다."
|
||||
},
|
||||
"domain-names.wildcards-not-supported": {
|
||||
"defaultMessage": "이 인증 기관(CA)은 와일드카드를 지원하지 않습니다."
|
||||
},
|
||||
"domains.force-ssl": {
|
||||
"defaultMessage": "SSL 강제 적용"
|
||||
},
|
||||
"domains.hsts-enabled": {
|
||||
"defaultMessage": "HSTS 활성화"
|
||||
},
|
||||
"domains.hsts-subdomains": {
|
||||
"defaultMessage": "HSTS 서브도메인 포함"
|
||||
},
|
||||
"domains.http2-support": {
|
||||
"defaultMessage": "HTTP/2 지원"
|
||||
},
|
||||
"domains.use-dns": {
|
||||
"defaultMessage": "DNS 챌린지 사용"
|
||||
},
|
||||
"email-address": {
|
||||
"defaultMessage": "이메일 주소"
|
||||
},
|
||||
"empty-search": {
|
||||
"defaultMessage": "검색 결과 없음"
|
||||
},
|
||||
"empty-subtitle": {
|
||||
"defaultMessage": "하나 만들어 보는 건 어떨까요?"
|
||||
},
|
||||
"enabled": {
|
||||
"defaultMessage": "활성화"
|
||||
},
|
||||
"error.access.at-least-one": {
|
||||
"defaultMessage": "인증 또는 접근 규칙 중 하나는 반드시 필요합니다."
|
||||
},
|
||||
"error.access.duplicate-usernames": {
|
||||
"defaultMessage": "인증 사용자 이름은 중복될 수 없습니다."
|
||||
},
|
||||
"error.invalid-auth": {
|
||||
"defaultMessage": "이메일 또는 비밀번호가 잘못되었습니다."
|
||||
},
|
||||
"error.invalid-domain": {
|
||||
"defaultMessage": "잘못된 도메인: {domain}"
|
||||
},
|
||||
"error.invalid-email": {
|
||||
"defaultMessage": "잘못된 이메일 주소입니다."
|
||||
},
|
||||
"error.max-character-length": {
|
||||
"defaultMessage": "최대 길이는 {max}자입니다."
|
||||
},
|
||||
"error.max-domains": {
|
||||
"defaultMessage": "도메인이 너무 많습니다. 최대 {max}개까지 가능합니다."
|
||||
},
|
||||
"error.maximum": {
|
||||
"defaultMessage": "최댓값은 {max}입니다."
|
||||
},
|
||||
"error.min-character-length": {
|
||||
"defaultMessage": "최소 길이는 {min}자입니다."
|
||||
},
|
||||
"error.minimum": {
|
||||
"defaultMessage": "최솟값은 {min}입니다."
|
||||
},
|
||||
"error.passwords-must-match": {
|
||||
"defaultMessage": "비밀번호가 일치해야 합니다."
|
||||
},
|
||||
"error.required": {
|
||||
"defaultMessage": "필수 항목입니다."
|
||||
},
|
||||
"expires.on": {
|
||||
"defaultMessage": "만료일: {date}"
|
||||
},
|
||||
"footer.github-fork": {
|
||||
"defaultMessage": "GitHub에서 포크하기"
|
||||
},
|
||||
"host.flags.block-exploits": {
|
||||
"defaultMessage": "일반적인 공격 차단"
|
||||
},
|
||||
"host.flags.cache-assets": {
|
||||
"defaultMessage": "정적 에셋 캐싱"
|
||||
},
|
||||
"host.flags.preserve-path": {
|
||||
"defaultMessage": "요청 경로 유지"
|
||||
},
|
||||
"host.flags.protocols": {
|
||||
"defaultMessage": "프로토콜"
|
||||
},
|
||||
"host.flags.websockets-upgrade": {
|
||||
"defaultMessage": "웹소켓 지원"
|
||||
},
|
||||
"host.forward-port": {
|
||||
"defaultMessage": "전달할 포트"
|
||||
},
|
||||
"host.forward-scheme": {
|
||||
"defaultMessage": "프로토콜"
|
||||
},
|
||||
"hosts": {
|
||||
"defaultMessage": "호스트 목록"
|
||||
},
|
||||
"http-only": {
|
||||
"defaultMessage": "HTTP 전용"
|
||||
},
|
||||
"lets-encrypt": {
|
||||
"defaultMessage": "Let's Encrypt"
|
||||
},
|
||||
"lets-encrypt-via-dns": {
|
||||
"defaultMessage": "Let's Encrypt (DNS 방식)"
|
||||
},
|
||||
"lets-encrypt-via-http": {
|
||||
"defaultMessage": "Let's Encrypt (HTTP 방식)"
|
||||
},
|
||||
"loading": {
|
||||
"defaultMessage": "불러오는 중…"
|
||||
},
|
||||
"login.title": {
|
||||
"defaultMessage": "로그인"
|
||||
},
|
||||
"nginx-config.label": {
|
||||
"defaultMessage": "사용자 지정 Nginx 설정"
|
||||
},
|
||||
"nginx-config.placeholder": {
|
||||
"defaultMessage": "# 위험을 감수하고 여기에 사용자 지정 Nginx 설정을 입력하세요!"
|
||||
},
|
||||
"no-permission-error": {
|
||||
"defaultMessage": "이 내용을 볼 권한이 없습니다."
|
||||
},
|
||||
"notfound.action": {
|
||||
"defaultMessage": "홈으로 이동"
|
||||
},
|
||||
"notfound.content": {
|
||||
"defaultMessage": "죄송합니다. 찾으시는 페이지를 찾을 수 없습니다."
|
||||
},
|
||||
"notfound.title": {
|
||||
"defaultMessage": "이런… 오류 페이지에 도착했습니다."
|
||||
},
|
||||
"notification.error": {
|
||||
"defaultMessage": "오류"
|
||||
},
|
||||
"notification.object-deleted": {
|
||||
"defaultMessage": "{object}이(가) 삭제되었습니다."
|
||||
},
|
||||
"notification.object-disabled": {
|
||||
"defaultMessage": "{object}이(가) 비활성화되었습니다."
|
||||
},
|
||||
"notification.object-enabled": {
|
||||
"defaultMessage": "{object}이(가) 활성화되었습니다."
|
||||
},
|
||||
"notification.object-renewed": {
|
||||
"defaultMessage": "{object}이(가) 갱신되었습니다."
|
||||
},
|
||||
"notification.object-saved": {
|
||||
"defaultMessage": "{object}이(가) 저장되었습니다."
|
||||
},
|
||||
"notification.success": {
|
||||
"defaultMessage": "성공"
|
||||
},
|
||||
"object.actions-title": {
|
||||
"defaultMessage": "{object} #{id}"
|
||||
},
|
||||
"object.add": {
|
||||
"defaultMessage": "{object} 추가"
|
||||
},
|
||||
"object.delete": {
|
||||
"defaultMessage": "{object} 삭제"
|
||||
},
|
||||
"object.delete.content": {
|
||||
"defaultMessage": "이 {object}을(를) 정말 삭제하시겠습니까?"
|
||||
},
|
||||
"object.edit": {
|
||||
"defaultMessage": "{object} 편집"
|
||||
},
|
||||
"object.empty": {
|
||||
"defaultMessage": "{objects}이(가) 없습니다."
|
||||
},
|
||||
"object.event.created": {
|
||||
"defaultMessage": "{object}이(가) 생성됨"
|
||||
},
|
||||
"object.event.deleted": {
|
||||
"defaultMessage": "{object}이(가) 삭제됨"
|
||||
},
|
||||
"object.event.disabled": {
|
||||
"defaultMessage": "{object}이(가) 비활성화됨"
|
||||
},
|
||||
"object.event.enabled": {
|
||||
"defaultMessage": "{object}이(가) 활성화됨"
|
||||
},
|
||||
"object.event.renewed": {
|
||||
"defaultMessage": "{object}이(가) 갱신됨"
|
||||
},
|
||||
"object.event.updated": {
|
||||
"defaultMessage": "{object}이(가) 업데이트됨"
|
||||
},
|
||||
"offline": {
|
||||
"defaultMessage": "비활성화"
|
||||
},
|
||||
"online": {
|
||||
"defaultMessage": "활성화"
|
||||
},
|
||||
"options": {
|
||||
"defaultMessage": "옵션"
|
||||
},
|
||||
"password": {
|
||||
"defaultMessage": "비밀번호"
|
||||
},
|
||||
"password.generate": {
|
||||
"defaultMessage": "무작위 비밀번호 생성"
|
||||
},
|
||||
"password.hide": {
|
||||
"defaultMessage": "비밀번호 숨기기"
|
||||
},
|
||||
"password.show": {
|
||||
"defaultMessage": "비밀번호 표시"
|
||||
},
|
||||
"permissions.hidden": {
|
||||
"defaultMessage": "숨김"
|
||||
},
|
||||
"permissions.manage": {
|
||||
"defaultMessage": "관리"
|
||||
},
|
||||
"permissions.view": {
|
||||
"defaultMessage": "보기 전용"
|
||||
},
|
||||
"permissions.visibility.all": {
|
||||
"defaultMessage": "모든 항목"
|
||||
},
|
||||
"permissions.visibility.title": {
|
||||
"defaultMessage": "항목 표시 설정"
|
||||
},
|
||||
"permissions.visibility.user": {
|
||||
"defaultMessage": "내가 만든 항목만"
|
||||
},
|
||||
"proxy-host": {
|
||||
"defaultMessage": "프록시 호스트"
|
||||
},
|
||||
"proxy-host.forward-host": {
|
||||
"defaultMessage": "전달할 호스트명 / IP"
|
||||
},
|
||||
"proxy-hosts": {
|
||||
"defaultMessage": "프록시 호스트"
|
||||
},
|
||||
"proxy-hosts.count": {
|
||||
"defaultMessage": "{count}개의 프록시 호스트"
|
||||
},
|
||||
"public": {
|
||||
"defaultMessage": "공개"
|
||||
},
|
||||
"redirection-host": {
|
||||
"defaultMessage": "리다이렉션 호스트"
|
||||
},
|
||||
"redirection-host.forward-domain": {
|
||||
"defaultMessage": "전달할 도메인"
|
||||
},
|
||||
"redirection-host.forward-http-code": {
|
||||
"defaultMessage": "HTTP 코드"
|
||||
},
|
||||
"redirection-hosts": {
|
||||
"defaultMessage": "리다이렉션 호스트"
|
||||
},
|
||||
"redirection-hosts.count": {
|
||||
"defaultMessage": "{count}개의 리다이렉션 호스트"
|
||||
},
|
||||
"redirection-hosts.http-code.300": {
|
||||
"defaultMessage": "300 Multiple Choices"
|
||||
},
|
||||
"redirection-hosts.http-code.301": {
|
||||
"defaultMessage": "301 Moved permanently"
|
||||
},
|
||||
"redirection-hosts.http-code.302": {
|
||||
"defaultMessage": "302 Moved temporarily"
|
||||
},
|
||||
"redirection-hosts.http-code.303": {
|
||||
"defaultMessage": "303 See other"
|
||||
},
|
||||
"redirection-hosts.http-code.307": {
|
||||
"defaultMessage": "307 Temporary redirect"
|
||||
},
|
||||
"redirection-hosts.http-code.308": {
|
||||
"defaultMessage": "308 Permanent redirect"
|
||||
},
|
||||
"role.admin": {
|
||||
"defaultMessage": "관리자"
|
||||
},
|
||||
"role.standard-user": {
|
||||
"defaultMessage": "일반 사용자"
|
||||
},
|
||||
"save": {
|
||||
"defaultMessage": "저장"
|
||||
},
|
||||
"setting": {
|
||||
"defaultMessage": "설정"
|
||||
},
|
||||
"settings": {
|
||||
"defaultMessage": "설정"
|
||||
},
|
||||
"settings.default-site": {
|
||||
"defaultMessage": "기본 사이트"
|
||||
},
|
||||
"settings.default-site.404": {
|
||||
"defaultMessage": "404 페이지"
|
||||
},
|
||||
"settings.default-site.444": {
|
||||
"defaultMessage": "응답 없음 (444)"
|
||||
},
|
||||
"settings.default-site.congratulations": {
|
||||
"defaultMessage": "축하 페이지"
|
||||
},
|
||||
"settings.default-site.description": {
|
||||
"defaultMessage": "알 수 없는 호스트로 요청이 들어왔을 때 표시할 내용"
|
||||
},
|
||||
"settings.default-site.html": {
|
||||
"defaultMessage": "사용자 지정 HTML"
|
||||
},
|
||||
"settings.default-site.html.placeholder": {
|
||||
"defaultMessage": "<!-- 여기에 사용자 정의 HTML 내용을 입력하세요. -->"
|
||||
},
|
||||
"settings.default-site.redirect": {
|
||||
"defaultMessage": "리다이렉트"
|
||||
},
|
||||
"setup.preamble": {
|
||||
"defaultMessage": "관리자 계정을 만들어 시작하세요."
|
||||
},
|
||||
"setup.title": {
|
||||
"defaultMessage": "환영합니다!"
|
||||
},
|
||||
"sign-in": {
|
||||
"defaultMessage": "로그인"
|
||||
},
|
||||
"ssl-certificate": {
|
||||
"defaultMessage": "SSL 인증서"
|
||||
},
|
||||
"stream": {
|
||||
"defaultMessage": "호스트 스트림"
|
||||
},
|
||||
"stream.forward-host": {
|
||||
"defaultMessage": "전달할 호스트"
|
||||
},
|
||||
"stream.forward-host.placeholder": {
|
||||
"defaultMessage": "example.com / 10.0.0.1 / IPv6"
|
||||
},
|
||||
"stream.incoming-port": {
|
||||
"defaultMessage": "수신 포트"
|
||||
},
|
||||
"streams": {
|
||||
"defaultMessage": "호스트 스트림"
|
||||
},
|
||||
"streams.count": {
|
||||
"defaultMessage": "{count}개의 호스트 스트림"
|
||||
},
|
||||
"streams.tcp": {
|
||||
"defaultMessage": "TCP"
|
||||
},
|
||||
"streams.udp": {
|
||||
"defaultMessage": "UDP"
|
||||
},
|
||||
"test": {
|
||||
"defaultMessage": "테스트"
|
||||
},
|
||||
"update-available": {
|
||||
"defaultMessage": "업데이트 가능: {latestVersion}"
|
||||
},
|
||||
"user": {
|
||||
"defaultMessage": "사용자"
|
||||
},
|
||||
"user.change-password": {
|
||||
"defaultMessage": "비밀번호 변경"
|
||||
},
|
||||
"user.confirm-password": {
|
||||
"defaultMessage": "비밀번호 확인"
|
||||
},
|
||||
"user.current-password": {
|
||||
"defaultMessage": "현재 비밀번호"
|
||||
},
|
||||
"user.edit-profile": {
|
||||
"defaultMessage": "프로필 편집"
|
||||
},
|
||||
"user.full-name": {
|
||||
"defaultMessage": "전체 이름"
|
||||
},
|
||||
"user.login-as": {
|
||||
"defaultMessage": "{name}으로 로그인"
|
||||
},
|
||||
"user.logout": {
|
||||
"defaultMessage": "로그아웃"
|
||||
},
|
||||
"user.new-password": {
|
||||
"defaultMessage": "새 비밀번호"
|
||||
},
|
||||
"user.nickname": {
|
||||
"defaultMessage": "닉네임"
|
||||
},
|
||||
"user.set-password": {
|
||||
"defaultMessage": "비밀번호 설정"
|
||||
},
|
||||
"user.set-permissions": {
|
||||
"defaultMessage": "{name}의 권한 설정"
|
||||
},
|
||||
"user.switch-dark": {
|
||||
"defaultMessage": "다크 모드로 전환"
|
||||
},
|
||||
"user.switch-light": {
|
||||
"defaultMessage": "라이트 모드로 전환"
|
||||
},
|
||||
"username": {
|
||||
"defaultMessage": "사용자 이름"
|
||||
},
|
||||
"users": {
|
||||
"defaultMessage": "사용자"
|
||||
}
|
||||
}
|
||||
78
frontend/src/locale/src/lang-list.json
Executable file → Normal file
78
frontend/src/locale/src/lang-list.json
Executable file → Normal file
@@ -1,35 +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-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": "Български"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
12
frontend/src/locale/src/zh.json
Executable file → Normal 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"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
368
frontend/src/modals/TwoFactorModal.tsx
Normal file
368
frontend/src/modals/TwoFactorModal.tsx
Normal 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 };
|
||||
@@ -13,4 +13,5 @@ export * from "./RedirectionHostModal";
|
||||
export * from "./RenewCertificateModal";
|
||||
export * from "./SetPasswordModal";
|
||||
export * from "./StreamModal";
|
||||
export * from "./TwoFactorModal";
|
||||
export * from "./UserModal";
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user